Release v0.74.0 — Audit remediation + starter-packs decomposition
Date: 2026-04-21
Tag: v0.74.0
Summary
v0.74.0 closes the verification gaps the 2026-04-18 external audit flagged and lands the editor’s largest maintainability target. It is the credibility-groundwork PR (#40) absorbing two cohesive workstreams:
- Audit-remediation Phases 1–5 (commit
b1f5b80) — green the failing tests, add Windows CI for the marketed installer/file-mode path, make every OpenAPI operation explicit, capture a real HA validation run, and harden the API test bootstrap so a developer’s local.envcan no longer poison results. - Starter-packs decomposition (commit
5b56e8c) — split the editor’s 5,725-lineapps/editor/src/lib/starter-packs.tsinto 36 sibling modules underapps/editor/src/lib/starter-packs/while keeping the public API and every consumer’s import path intact.
No breaking changes. No public API surface widened.
This release ships without CI signal. The repo’s CI and Release workflows are still in disabled_manually from the prior billing block. The release commit carries [skip ci] so no Actions fire on the push or the tag. Operators using the HA stack continue to build the image locally until the GHCR publish step is restored.
What shipped
1. Verification baseline turned from red to green
The audit’s six cited test failures all resolve. After the fixes, the editor unit suite goes 1312 → 1313/1313 (the +1 is the new registry-order guard added before the decomposition started). The pdf-renderer suite stays at 324/324. The editor E2E suite stays at 23/23. The gated pnpm --filter @pulp-engine/api test:file suite reports 1181 passed, 96 skipped, 0 failed.
Concrete fixes:
starter-packs.test.tsinvoice mapping count was 9 but commit4b08c20had addedoverdueNoticebringing it to 10. Updated.renderer.test.ts:893+915were assertingdisplayHeaderFooter: falseeven though the renderer correctly forcestruein eval-mode (so the watermark prints). Tests now scopePULP_LICENCE_KEYso the eval override doesn’t mask the sanitizer behaviour under test. Production logic unchanged.socket-render-dispatcher.ts:100was a lint failure: an outerdecoderbinding that the linter flagged as unused. Removed; each retry creates a fresh decoder.embed-editor.spec.tstemplate-autoload test was using a stale chart node shape (labelKey/valueKeyinstead ofcategoryField/valueField) and missingdocument.id.embed-editor.spec.tsdark-theme test was a real product bug:useEditorThemereadspulp-engine-editor-themefrom localStorage on mount and toggles.darkbased on what it finds, silently overriding the embed init’s class addition. Embed init now persists the requested theme to the same localStorage key the hook reads from.keyboard-reorder.spec.ts:41was a real regression from commit7bc6b0b(palette-drag refactor):SectionNodeViewregistered an inneruseDroppablefor palette drops, and the keyboard coordinate getter included thosesection-drop:<id>droppables as ArrowDown candidates. Filter now excludes droppables whosedata.current.source === 'section-drop'.
2. Windows CI for the marketed installer / file-mode path
The product ships a Windows installer and markets file mode as the fastest trial path, but CI was Linux-only. New ci-windows job in .github/workflows/ci.yml runs on windows-latest and gates on:
pnpm typecheckpnpm buildpnpm --filter @pulp-engine/api test:file- Installer staging via
pwsh ./installer/build.ps1 -SkipNsis - Runtime smoke against the staged bundle:
- Launch via
installer/scripts/start-pulpengine.ps1 - Parse
PORTfrom the generated%APPDATA%\PulpEngine\.env - Poll
GET /health/readyuntil200(60 s timeout) - Assert
GET /healthreturns200 - Assert
GET /editor/returns200withContent-Type: text/html - Tear down via
installer/scripts/stop-pulpengine.ps1
- Launch via
ci-windows-logs artifact (%APPDATA%\PulpEngine\logs/) uploaded on failure.
To make pnpm --filter @pulp-engine/api test:file reproducible on Windows, the file-mode vitest config now uses pool: 'forks' with singleFork: true on win32, plus larger test/hook timeouts to absorb cold buildServer() boot. Linux retains parallelism.
3. OpenAPI ergonomics — every operation now has an explicit operationId
Was 5/88. Is now 80/80. SDK method names no longer drift when route shapes change. /metrics, /editor, and 6 static-asset routes are hidden from the spec via hide: true.
scripts/generate-sdks.ts gained a stale-file sweep before in-place regen: reads the .openapi-generator/FILES manifest (with backslash → forward-slash normalisation for the Windows manifest) and deletes those entries, so renamed operations no longer leave orphan files in packages/sdk-dotnet/ or packages/sdk-go/. Both pnpm extract-openapi --check and pnpm sdk:generate --check are green.
4. HA validation — one real recorded run
docs/ha-validation-report.md now has Run 1 entered with the actual checks, dates, stack versions, and per-check results. Of the 6 documented checks:
- ✅ Shared asset readability — uploaded a 535,381-byte PNG via the LB, fetched same byte count from both api1 AND api2 directly. Confirms shared MinIO + Postgres asset metadata.
- ✅ Editor token cross-replica — minted on api1, verified on api2 with
GET /templatesreturning 200. HMAC secret is shared viaEDITOR_TOKEN_SECRET; tokens are replica-portable. - ✅ Graceful degradation — killed
pulpengine-api1-1, sent 5 GET /health requests through the LB; all 5 returned 200 from api2. nginx upstream failover works. - ⏸️ Schedule fires exactly once — DEFERRED (needs a 3-min wait window).
- ⏸️ API key rotation — DEFERRED (needs an env-edit + restart cycle).
- ➖ Tenant archive propagation — N/A on this single-tenant stack.
The run uncovered four real product/compose bugs that the report now documents inline:
s3-asset-binary.store.tswas using thetransformToNodeStream()mixin removed in@aws-sdk/client-s33.700+.GET /assets/<filename>was returning HTTP 500 in private mode. Replaced with a runtime check (Readabledirectly, orReadable.fromWeb(...transformToWebStream())as a web-stream fallback) plus matching unit tests..dockerignorewas excludingEVALUATION-LICENCE.mdvia the*.mdrule, breaking the website’sprebuildstep inside the Docker build. Whitelisted alongside!README.md.docker-compose.ha.ymlhad three independent env regressions: missingASSET_ACCESS_MODE: private(without it,S3_PUBLIC_URLis required whenS3_ENDPOINTis set); missingHARDEN_PRODUCTION: "false"opt-out for the demo stack; andS3_FORCE_PATH_STYLEinstead of the renamed-and-removedS3_PATH_STYLE.
5. Test bootstrap hardened against ambient env
New apps/api/src/__tests__/setup/env-isolation.setup.ts — a vitest globalSetup that strips API_KEY_*, MULTI_TENANT_ENABLED, SANDBOX_*, HARDEN_PRODUCTION, OIDC_*, ANTHROPIC_API_KEY, PULP_LICENCE_KEY, METRICS_TOKEN, and EDITOR_USERS_* from the inherited process.env before any worker boots. DATABASE_URL is preserved so postgres tests still work. Wired into both vitest.config.ts and vitest.file.config.ts.
The audit caught this surface: a developer’s local .env (with API_KEY_ADMIN / MULTI_TENANT_ENABLED=true set for normal dev) could poison the test suite — every request without an auth header would 401 even though tests assumed auth was off. CI papered over it with a minimal .env. Now it’s structurally fixed: tests run reproducibly regardless of the developer’s shell or .env.
6. File-mode WIP suite — disciplined non-blocking spillover
pnpm --filter @pulp-engine/api test:file:wip is a new informational suite that runs the small set of tests that fail for reasons unrelated to the file-mode storage contract:
- B.3 variant-resolution headers in
template-labels.route.test.ts - Plugin storage activation registration race in
plugin-integration/plugin-storage-activation.test.ts - Batch-async webhook delivery worker bootstrap in
batch-async.test.ts
Each is documented with symptom, suspect, and scope in docs/initiatives/file-mode-wip-followups.md. The gated test:file suite excludes them via a re-imported WIP_TESTS array so both configs stay in sync — when a follow-up lands, removing the entry from WIP_TESTS puts the test back in the gate. Drain target: zero. Linux + Windows CI both run the WIP set with continue-on-error: true for visibility without blocking.
7. apps/editor/src/lib/starter-packs.ts decomposed (5,725 → 19 lines)
The audit called out maintainability concentration in four large files. v0.74.0 splits the worst offender. The root file collapses to a 19-line thin named re-export of the public surface. Implementation lives under apps/editor/src/lib/starter-packs/:
apps/editor/src/lib/starter-packs/
index.ts # narrow public barrel — 6 symbols only
types.ts # StarterPackMeta, StarterPackDefinition
# (FieldMappingEntry just re-exported from upstream)
themes.ts # Theme, THEMES, h1Style, h2Style, captionStyle,
# accentDivider, themedTableStyles
render-configs.ts # FINANCIAL_RENDER_CONFIG, LETTER_RENDER_CONFIG
base.ts # section() helper, baseTemplate()
plain-theme.ts # stripStyle, stripNode, applyPlainTheme
registry.ts # PRIVATE_PACKS, STARTER_PACKS, getStarterPack,
# getPrivatePackForTesting
# (imports each pack module DIRECTLY, NOT via index)
packs/<name>.ts × 29 # one per pack — 28 default + 1 gilrose
packs/sales-pivot-shared.ts # SALES_PIVOT_MAPPINGS / SAMPLE / INPUT_SCHEMA
# shared by salesPivotV1 + salesPivotV2
Public-barrel discipline. Both starter-packs.ts and starter-packs/index.ts re-export only the six public symbols (STARTER_PACKS, getStarterPack, getPrivatePackForTesting, applyPlainTheme, StarterPackMeta, StarterPackDefinition). Pack constants (INVOICE_MAPPINGS, individual pack consts) and shared internals (THEMES, h1Style, baseTemplate, etc.) are NOT re-exported through the public barrel — registry.ts imports each pack module directly.
Registry-order guard added in starter-packs.test.ts before any extraction, pinning the full STARTER_PACKS.map(p => p.id) against the expected 28 default-mode IDs (29 with VITE_ENABLE_GILROSE_PACK). The New-Template dialog renders packs in registry order, so any silent re-shuffle would surface as a UX regression. The order test was the load-bearing contract that protected the registry split.
Consumers untouched. Verified via pnpm typecheck: NewTemplateDialog.tsx, starter-packs.test.ts, gilrose-loan-statement.integration.test.ts, the deprecated node-factory.ts aliases, and the website + doc-spike scripts (generate-playground-fixtures.ts, generate-showcase-samples.ts, plus the gilrose render spikes) all keep their existing import path. The contract is the public API, not the file path.
The other three audit-cited hotspots (render.ts, config.ts, server.ts) stay deferred — render’s preview-routes split is the next-highest-ROI target if/when that work resumes.
What’s not in this release
- The other three hotspot decompositions (
render.ts,config.ts,server.ts). - Editor build polish (
manualChunksinvite.config.ts,spawndeprecation inbuild-all.mjs). - HA Run 2 to cover schedule-fires-once + key rotation.
- GHCR image republish — depends on resolving the billing block on the repo’s Actions.
- WIP-suite drain — three tracked file-mode follow-ups in
docs/initiatives/file-mode-wip-followups.md.
These are tracked, not silently skipped. Each has a documented destination.
Verification (Windows local)
pnpm lint PASS
pnpm typecheck PASS
pnpm build PASS
pnpm extract-openapi --check PASS
pnpm sdk:generate --check PASS
pnpm --filter @pulp-engine/pdf-renderer test PASS (324/324)
pnpm --filter @pulp-engine/editor test PASS (1313/1313)
pnpm --filter @pulp-engine/editor test:e2e PASS (23/23)
pnpm --filter @pulp-engine/api test:file PASS (1181 passed, 96 skipped, 0 failed)
Linux CI confirmation deferred until the workflow billing block is resolved.
Upgrade notes
None. Drop-in replacement.
If you import from apps/editor/src/lib/starter-packs.ts directly (the editor itself, the website’s starter-pack scripts, doc-spike rendering scripts), nothing changes — the file remains and re-exports the same surface. New code can import from apps/editor/src/lib/starter-packs/index directly if you prefer.
If you have a fork that depended on the now-private starter-pack constants (INVOICE_MAPPINGS, individual pack consts, THEMES, h1Style, etc.), import them from the matching sibling module under apps/editor/src/lib/starter-packs/ — they were never part of the public API and are now correctly scoped.
If you operate the HA stack: rebuild the image locally (docker build -t ghcr.io/troycoderboy/pulp-engine:latest .) before docker compose -f docker-compose.ha.yml up -d, until the GHCR publish step is restored. The compose file’s three env corrections (above) are required for any HA stack to come up in v0.74.0 — including the previously-published v0.73.0 image.