Pulp Engine Document Rendering
Get started
Release v0.74.0

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:

  1. 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 .env can no longer poison results.
  2. Starter-packs decomposition (commit 5b56e8c) — split the editor’s 5,725-line apps/editor/src/lib/starter-packs.ts into 36 sibling modules under apps/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.ts invoice mapping count was 9 but commit 4b08c20 had added overdueNotice bringing it to 10. Updated.
  • renderer.test.ts:893+915 were asserting displayHeaderFooter: false even though the renderer correctly forces true in eval-mode (so the watermark prints). Tests now scope PULP_LICENCE_KEY so the eval override doesn’t mask the sanitizer behaviour under test. Production logic unchanged.
  • socket-render-dispatcher.ts:100 was a lint failure: an outer decoder binding that the linter flagged as unused. Removed; each retry creates a fresh decoder.
  • embed-editor.spec.ts template-autoload test was using a stale chart node shape (labelKey/valueKey instead of categoryField/valueField) and missing document.id.
  • embed-editor.spec.ts dark-theme test was a real product bug: useEditorTheme reads pulp-engine-editor-theme from localStorage on mount and toggles .dark based 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:41 was a real regression from commit 7bc6b0b (palette-drag refactor): SectionNodeView registered an inner useDroppable for palette drops, and the keyboard coordinate getter included those section-drop:<id> droppables as ArrowDown candidates. Filter now excludes droppables whose data.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:

  1. pnpm typecheck
  2. pnpm build
  3. pnpm --filter @pulp-engine/api test:file
  4. Installer staging via pwsh ./installer/build.ps1 -SkipNsis
  5. Runtime smoke against the staged bundle:
    • Launch via installer/scripts/start-pulpengine.ps1
    • Parse PORT from the generated %APPDATA%\PulpEngine\.env
    • Poll GET /health/ready until 200 (60 s timeout)
    • Assert GET /health returns 200
    • Assert GET /editor/ returns 200 with Content-Type: text/html
    • Tear down via installer/scripts/stop-pulpengine.ps1

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 /templates returning 200. HMAC secret is shared via EDITOR_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.ts was using the transformToNodeStream() mixin removed in @aws-sdk/client-s3 3.700+. GET /assets/<filename> was returning HTTP 500 in private mode. Replaced with a runtime check (Readable directly, or Readable.fromWeb(...transformToWebStream()) as a web-stream fallback) plus matching unit tests.
  • .dockerignore was excluding EVALUATION-LICENCE.md via the *.md rule, breaking the website’s prebuild step inside the Docker build. Whitelisted alongside !README.md.
  • docker-compose.ha.yml had three independent env regressions: missing ASSET_ACCESS_MODE: private (without it, S3_PUBLIC_URL is required when S3_ENDPOINT is set); missing HARDEN_PRODUCTION: "false" opt-out for the demo stack; and S3_FORCE_PATH_STYLE instead of the renamed-and-removed S3_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 (manualChunks in vite.config.ts, spawn deprecation in build-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.