Files
tech-log-frontend/docs/superpowers/plans/2026-08-01-release-boot-integrity.md
T

345 lines
13 KiB
Markdown

# Release and Boot Integrity Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make generated Release Manifest V2 artifacts verifiable and make the browser accept only coherent V1/V1 or V2/V2 boot protocol pairs.
**Architecture:** Zod schemas define artifact shapes and version-specific token projection. Runtime config preserves an exact V1/V2 discriminator through release-manifest loading, where mixed pairs fail before contract or application composition.
**Tech Stack:** TypeScript 7, Node.js 24, Zod 4, Vitest 4, Vite 8, pnpm 11.
## Global Constraints
- Preserve all pre-existing dirty-worktree changes; never reset or restore them.
- Do not stage or commit mixed existing source/test files without explicit user authorization.
- Runtime Config versions accepted by browser boot are exactly `"1"` and `"2.0"`.
- Release Manifest V1 is read-only compatibility; all writers emit V2.
- V2 never requires or emits `API_CONTRACT_VERSION`/`apiContractVersion`.
- V2 contract identity is `contractSet.setDigest` and the full package set.
- Local/development endpoints allow only HTTP or HTTPS; staging/production allow only HTTPS.
- Every production behavior change must be preceded by a failing test.
---
### Task 1: Executable release artifact schemas and token projection
**Files:**
- Create: `scripts/contracts/release-artifacts.ts`
- Create: `tests/unit/release-artifacts.test.ts`
- Modify: `src/contracts/release-tokens.ts`
**Interfaces:**
- Produces: `releaseManifestV1ArtifactSchema`, `releaseManifestV2ArtifactSchema`, `releaseManifestArtifactSchema`, `runtimeConfigV1ArtifactSchema`, `runtimeConfigV2ArtifactSchema`, `runtimeConfigArtifactSchema`, and `buildManifestArtifactSchema`.
- Produces: `parseReleaseArtifact(value)`, `parseRuntimeConfigArtifact(value)`, `parseBuildManifestArtifact(value)`.
- Produces: `projectReleaseTokens(release)` returning common tokens plus exactly one of `apiContractVersion` or `contractSetDigest`.
- Consumes: `contractSetSchema` from `src/contracts/contract-set.ts`.
- [ ] **Step 1: Write failing V2 projection tests**
```ts
it("projects the nested V2 contract-set digest without a legacy scalar", () => {
const release = parseReleaseArtifact(v2ReleaseFixture);
expect(projectReleaseTokens(release)).toMatchObject({
schemaVersion: 2,
contractSetDigest: v2ReleaseFixture.contractSet.setDigest,
});
expect(projectReleaseTokens(release)).not.toHaveProperty("apiContractVersion");
});
it("rejects a V2 release carrying the removed scalar", () => {
expect(() => parseReleaseArtifact({
...v2ReleaseFixture,
apiContractVersion: "1",
})).toThrow();
});
```
- [ ] **Step 2: Run tests and confirm RED**
Run: `corepack pnpm exec vitest run tests/unit/release-artifacts.test.ts`
Expected: module/export resolution failure because the artifact contract module does not exist.
- [ ] **Step 3: Implement discriminated artifact schemas**
```ts
export const releaseManifestArtifactSchema = z.discriminatedUnion(
"schemaVersion",
[releaseManifestV1ArtifactSchema, releaseManifestV2ArtifactSchema],
);
export type ReleaseArtifact = z.output<typeof releaseManifestArtifactSchema>;
export function projectReleaseTokens(release: ReleaseArtifact) {
const common = {
schemaVersion: release.schemaVersion,
appVersion: release.appVersion,
buildId: release.buildId,
commitSha: release.commitSha,
configSchemaVersion: release.configSchemaVersion,
assetManifestHash: release.assetManifestHash,
releaseId: release.releaseId,
builtAt: release.builtAt,
} as const;
return release.schemaVersion === 1
? { ...common, apiContractVersion: release.apiContractVersion }
: { ...common, contractSetDigest: release.contractSet.setDigest };
}
```
Build Manifest V1 must include the fields currently emitted by the generator:
`releaseId`, `moduleInventoryHash`, `buildContext.sourceDateEpoch`, and output
paths for module inventory, route chunks, and runtime-config schema.
- [ ] **Step 4: Run focused tests and confirm GREEN**
Run: `corepack pnpm exec vitest run tests/unit/release-artifacts.test.ts tests/unit/release-coherence.test.ts`
Expected: all tests pass; V1 projection retains `apiContractVersion`; V2 projection contains only `contractSetDigest`.
### Task 2: Generate and verify artifacts through the same contracts
**Files:**
- Modify: `scripts/generate-build-manifest.ts`
- Modify: `scripts/verify-release.ts`
- Modify: `schemas/artifacts/build-manifest.schema.json`
- Test: `tests/unit/release-artifacts.test.ts`
**Interfaces:**
- Consumes Task 1 parsers and token projection.
- Produces V2 release and V1 build manifest that have been parsed before write.
- [ ] **Step 1: Add failing parser/writer round-trip tests**
Assert that the exact generator shapes parse, that unknown root/output fields
fail, and that a runtime-config V2 artifact parses without
`API_CONTRACT_VERSION`.
- [ ] **Step 2: Run the tests and confirm RED**
Run: `corepack pnpm exec vitest run tests/unit/release-artifacts.test.ts`
Expected: current build-manifest schema/parser rejects emitted fields or the V2 runtime parser requires the removed scalar.
- [ ] **Step 3: Parse before every write and parse before verification**
In `generate-build-manifest.ts`, wrap the existing build-manifest object with
`buildManifestArtifactSchema.parse(...)`, wrap the existing release-manifest
object with `releaseManifestV2ArtifactSchema.parse(...)`, and replace the
runtime-config parser with `runtimeConfigV2ArtifactSchema.parse(...)`. Preserve
the exact existing values and output paths; the schema call is the only writer
boundary added in this step.
In `verify-release.ts`, replace `CompatibilityTuple` parsing and the loop over
all registry keys with version-specific projection. Keep legacy coherence
fixtures on the existing numeric compatibility policy, but do not apply that
legacy tuple parser to V2 artifacts.
- [ ] **Step 4: Generate JSON Schema from the executable build schema**
Replace the checked-in `schemas/artifacts/build-manifest.schema.json` with the
deterministic `z.toJSONSchema(buildManifestArtifactSchema)` representation.
The generated schema must use draft 2020-12 and `additionalProperties: false`.
- [ ] **Step 5: Run focused tests and confirm GREEN**
Run: `corepack pnpm exec vitest run tests/unit/release-artifacts.test.ts tests/unit/release-coherence.test.ts`
Expected: all release artifact and legacy compatibility tests pass.
- [ ] **Step 6: Run the actual release pipeline in an isolated temporary copy**
Run the existing contract generation, app build, manifest generation, and
`node scripts/verify-release.ts` with local build environment values.
Expected: release verification exits 0 and reports no
`releaseToken:apiContractVersion` or `releaseToken:contractSetDigest` mismatch.
### Task 3: Exact runtime-config version and endpoint selection
**Files:**
- Modify: `src/bootstrap/runtime-config-schema.ts`
- Modify: `tests/runtime-schema/runtime-config.test.ts`
**Interfaces:**
- Produces `RuntimeConfigValidation` with a reliable `schema: "V1" | "V2"` discriminator.
- Keeps the existing normalized `RuntimeConfig` facade for downstream callers.
- [ ] **Step 1: Add failing future-version and protocol tests**
```ts
it.each(["0", "1.0", "2.0.1", "3.0"])(
"rejects unsupported boot config version %s",
(version) => {
expect(validateRuntimeConfig({
...validV1Config,
CONFIG_SCHEMA_VERSION: version,
}).success).toBe(false);
},
);
it.each(["file:///tmp/api/", "data:text/plain,x", "blob:https://test/id"])(
"rejects non-http endpoint %s",
(API_BASE_URL) => {
expect(validateRuntimeConfig({ ...validConfig, API_BASE_URL }).success).toBe(false);
},
);
```
- [ ] **Step 2: Run tests and confirm RED**
Run: `corepack pnpm exec vitest run tests/runtime-schema/runtime-config.test.ts`
Expected: `3.0` and at least `file:`/`data:` cases are currently accepted.
- [ ] **Step 3: Implement literal version dispatch and scheme allow-list**
```ts
export const runtimeConfigV1Schema = base.extend({
CONFIG_SCHEMA_VERSION: z.literal("1"),
API_CONTRACT_VERSION: z.string().regex(VERSION_PATTERN),
}).strict().superRefine(runtimeConfigInvariants);
const selectedSchema = declared === "1"
? runtimeConfigV1Schema
: declared === "2.0"
? runtimeConfigV2Schema
: null;
```
`assertEndpointUrl` must reject every protocol outside `http:` and `https:`
before applying the non-local HTTPS rule.
- [ ] **Step 4: Run tests and confirm GREEN**
Run: `corepack pnpm exec vitest run tests/runtime-schema/runtime-config.test.ts`
Expected: exact V1/V2 cases pass and future/non-HTTP cases fail.
### Task 4: Enforce config/manifest protocol pairing
**Files:**
- Modify: `src/bootstrap/load-release-manifest.ts`
- Modify: `tests/runtime-schema/release-manifest.test.ts`
**Interfaces:**
- Adds `MANIFEST_PROTOCOL_PAIR_MISMATCH` to `ReleaseManifestErrorCode`.
- Requires V1 runtime config with V1 manifest and V2 runtime config with V2 manifest.
- [ ] **Step 1: Replace the permissive compatibility test with a pairing matrix**
```ts
it.each([
["V1", 2],
["V2", 1],
] as const)("rejects %s runtime with manifest V%s", async (configSchema, schemaVersion) => {
await expect(loadReleaseManifest(
runtimeFor(configSchema),
{ fetcher: async () => jsonResponse(manifestFor(schemaVersion)) },
)).rejects.toMatchObject({ code: "MANIFEST_PROTOCOL_PAIR_MISMATCH" });
});
```
Also test that a V1 scalar mismatch fails, and that V2 contract-set verification
is mandatory rather than conditional.
- [ ] **Step 2: Run tests and confirm RED**
Run: `corepack pnpm exec vitest run tests/runtime-schema/release-manifest.test.ts`
Expected: V2 runtime plus V1 manifest currently resolves successfully.
- [ ] **Step 3: Implement pair validation before tuple checks**
```ts
const expectedManifestVersion = runtime.configSchema === "V1" ? 1 : 2;
if (manifest.schemaVersion !== expectedManifestVersion) {
throw new ReleaseManifestError("MANIFEST_PROTOCOL_PAIR_MISMATCH", identity);
}
```
For V1, require both legacy scalar values and compare them. For V2, require the
contract set and always call `verifyContractSet`. Do not use presence checks to
choose security validation.
- [ ] **Step 4: Run tests and confirm GREEN**
Run: `corepack pnpm exec vitest run tests/runtime-schema/release-manifest.test.ts tests/runtime-schema/runtime-config.test.ts`
Expected: complete pairing matrix passes and all tampered V2 sets fail.
### Task 5: Close boot cancellation and timing semantics
**Files:**
- Modify: `src/bootstrap/read-bounded-boot-json.ts`
- Modify: `src/bootstrap/load-runtime-config.ts`
- Modify: `tests/runtime-schema/runtime-config.test.ts`
- Create: `tests/unit/read-bounded-boot-json.test.ts`
**Interfaces:**
- Pre-aborted external signals prevent fetch admission.
- `validationDurationMs` excludes network acquisition.
- [ ] **Step 1: Add a failing pre-abort test**
Create an already-aborted controller, call `readBoundedBootJson`, and assert the
fetcher is never called and the outcome is a stable fetch/abort failure.
- [ ] **Step 2: Add a failing network-exclusion timing test**
Use a deferred fetcher and a deterministic `now()` sequence. Assert that elapsed
network time does not contribute to `validationDurationMs`.
- [ ] **Step 3: Run both tests and confirm RED**
Run: `corepack pnpm exec vitest run tests/unit/read-bounded-boot-json.test.ts tests/runtime-schema/runtime-config.test.ts`
- [ ] **Step 4: Implement admission precheck and move the timer start**
Check `options.signal?.aborted` before installing listeners or invoking fetch.
In `loadRuntimeConfig`, set `startedAt` immediately after a successful bounded
read and before safe-name/schema validation.
- [ ] **Step 5: Run both tests and confirm GREEN**
Run the same focused command and expect all tests to pass with no leaked abort
listeners or timers.
### Task 6: Full verification and handoff
**Files:**
- Verify all files touched by Tasks 1-5.
- [ ] **Step 1: Run focused suites**
Run:
```sh
corepack pnpm exec vitest run \
tests/unit/release-artifacts.test.ts \
tests/unit/release-coherence.test.ts \
tests/unit/read-bounded-boot-json.test.ts \
tests/runtime-schema/runtime-config.test.ts \
tests/runtime-schema/release-manifest.test.ts
```
- [ ] **Step 2: Run repository static and non-browser suites**
Run `corepack pnpm check:types`, `corepack pnpm lint`, and
`corepack pnpm test:all`.
- [ ] **Step 3: Run release verification from a clean generated output**
Run the complete local build and `corepack pnpm verify:release`. Record the
actual exit status and mismatch list.
- [ ] **Step 4: Run diff hygiene**
Run `git diff --check` and a NUL-byte scan. Do not attribute pre-existing
unrelated failures to this sub-project.
- [ ] **Step 5: Report exact remaining gates**
List passing commands, failing commands, files changed, and any browser-only
coverage that still requires a Playwright-capable environment.