docs: plan release and boot integrity work
This commit is contained in:
@@ -0,0 +1,344 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user