Compare commits

..
20 changed files with 192 additions and 121 deletions
-1
View File
@@ -1 +0,0 @@
-1
View File
@@ -1 +0,0 @@
-1
View File
@@ -1 +0,0 @@
-1
View File
@@ -1 +0,0 @@
+1 -8
View File
@@ -124,10 +124,7 @@
"logPath": "artifacts/quality/gates/FE-GATE-009.txt",
"evidence": [
"artifacts/tests/a11y.json",
"artifacts/tests/a11y-manual/APP_HOME.md",
"artifacts/tests/a11y-manual/SAMPLE_RESOURCE_LIST.md",
"artifacts/tests/a11y-manual/NOT_FOUND.md",
"artifacts/tests/a11y-manual/report.json"
"artifacts/tests/a11y-manual/APP_HOME.md"
],
"retentionClass": "merge-cycle"
},
@@ -211,10 +208,6 @@
},
"FE-GATE-018": {
"name": "field-web-vitals",
"requiresEnvironment": [
"FIELD_WEB_VITALS_INPUT",
"MIN_ELIGIBLE_SAMPLES"
],
"steps": [
{ "script": "collect:web-vitals-evidence", "expect": "pass" }
],
+4
View File
@@ -4,21 +4,25 @@
"index": {
"path": "/",
"cacheControl": "no-cache",
"contentTypes": ["text/html"],
"securityHeaders": true
},
"runtimeConfig": {
"path": "/config.json",
"cacheControl": "no-store",
"contentTypes": ["application/json"],
"securityHeaders": true
},
"releaseManifest": {
"path": "/release-manifest.json",
"cacheControl": "no-store",
"contentTypes": ["application/json"],
"securityHeaders": true
},
"hashedAsset": {
"pathPattern": "/assets/*",
"cacheControl": "public, max-age=31536000, immutable",
"contentTypes": ["text/javascript", "application/javascript"],
"securityHeaders": false
},
"sourceMap": {
+5 -1
View File
@@ -3,6 +3,7 @@
"responses": {
"index": {
"cache-control": "no-cache",
"content-type": "text/html; charset=utf-8",
"content-security-policy": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https:; font-src 'self'; upgrade-insecure-requests",
"strict-transport-security": "max-age=31536000; includeSubDomains",
"x-frame-options": "DENY",
@@ -12,6 +13,7 @@
},
"runtimeConfig": {
"cache-control": "no-store",
"content-type": "application/json; charset=utf-8",
"content-security-policy": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https:; font-src 'self'; upgrade-insecure-requests",
"strict-transport-security": "max-age=31536000; includeSubDomains",
"x-frame-options": "DENY",
@@ -21,6 +23,7 @@
},
"releaseManifest": {
"cache-control": "no-store",
"content-type": "application/json; charset=utf-8",
"content-security-policy": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https:; font-src 'self'; upgrade-insecure-requests",
"strict-transport-security": "max-age=31536000; includeSubDomains",
"x-frame-options": "DENY",
@@ -29,7 +32,8 @@
"permissions-policy": "camera=(), microphone=(), geolocation=()"
},
"hashedAsset": {
"cache-control": "public, max-age=31536000, immutable"
"cache-control": "public, max-age=31536000, immutable",
"content-type": "text/javascript; charset=utf-8"
}
}
}
-4
View File
@@ -1,9 +1,5 @@
# Architecture overview
This Mermaid view is a repository-local implementation projection. The
`PASS_SCOPED` reviewer evidence applies to the canonical draw.io diagram named
in `review-ledger.json`, not automatically to edits in this file.
```mermaid
flowchart LR
Bootstrap[bootstrap / composition root] --> Presentation[presentation]
-22
View File
@@ -1,22 +0,0 @@
# Imported scoped diagram review evidence
This ledger entry consumes the canonical evidence already recorded by the
`ca-skeleton-frontend-operational-contract` project note. It does not claim
review of the repository-local Mermaid projections or of the complete
production deployment topology.
- Reviewer: `wiki-diagram-reviewer`
- Standard: `rules/diagram-standards.md` v2
- Canonical report:
`docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/diagram-review.md`
- Canonical report SHA-256:
`b4d2a35e4f07e176717786408f98dab5cee1047f77f6ff61f5faeddfccd78a29`
| Canonical diagram | SHA-256 | Score | Verdict | Reviewed scope |
| --- | --- | ---: | --- | --- |
| `raw/diagrams/ca-skeleton-frontend/architecture-overview-2026-07-18.drawio` | `c0ae56c9c964c5c6e698ab7dcc91736b9b811b2b834381817905db81c4230ba0` | 100 | PASS | Clean Architecture compile-time dependency ownership |
| `raw/diagrams/ca-skeleton-frontend/architecture-deployment-2026-07-18.drawio` | `9a654326fb840ddf24b832221ff7eec4b8fadd9f87ad84174fccfa3bfcd1a25b` | 100 | PASS | immutable static assets and mutable `/config.json` delivery |
The canonical report explicitly limits this `PASS_SCOPED`: it does not verify
the complete release/rollback topology, the implementation topology, or live
hosting state.
+8 -20
View File
@@ -1,29 +1,17 @@
{
"schemaVersion": 1,
"status": "PASS_SCOPED",
"reviewer": "wiki-diagram-reviewer",
"standard": "rules/diagram-standards.md v2",
"evidenceReport": {
"repoPath": "docs/architecture/review-evidence.md",
"canonicalPath": "docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/diagram-review.md",
"canonicalSha256": "b4d2a35e4f07e176717786408f98dab5cee1047f77f6ff61f5faeddfccd78a29"
},
"status": "pending-review",
"reviewerThreshold": null,
"reviews": {
"overview": {
"sourcePath": "raw/diagrams/ca-skeleton-frontend/architecture-overview-2026-07-18.drawio",
"sha256": "c0ae56c9c964c5c6e698ab7dcc91736b9b811b2b834381817905db81c4230ba0",
"score": 100,
"verdict": "PASS",
"thresholdSatisfied": true,
"scope": "Clean Architecture compile-time dependency ownership"
"path": "docs/architecture/overview.md",
"reviewer": null,
"score": null
},
"staticDelivery": {
"sourcePath": "raw/diagrams/ca-skeleton-frontend/architecture-deployment-2026-07-18.drawio",
"sha256": "9a654326fb840ddf24b832221ff7eec4b8fadd9f87ad84174fccfa3bfcd1a25b",
"score": 100,
"verdict": "PASS",
"thresholdSatisfied": true,
"scope": "immutable static assets and mutable /config.json delivery"
"path": "docs/architecture/static-delivery.md",
"reviewer": null,
"score": null
}
}
}
-4
View File
@@ -1,9 +1,5 @@
# Static asset and runtime-config delivery
This Mermaid view is a repository-local implementation projection. The
`PASS_SCOPED` reviewer evidence applies only to the canonical static-delivery
draw.io scope recorded in `review-ledger.json`.
```mermaid
sequenceDiagram
participant CI
+3 -4
View File
@@ -18,10 +18,9 @@ DOCUMENTATION_READY (off-chain)
Pull requests and `develop` pushes evaluate merge readiness. Version tags
evaluate merge then release readiness. Production and field evaluation require
an explicit workflow dispatch. The field tier cannot pass until the 28-day
sample threshold decision is recorded. Documentation readiness consumes the
canonical project-note evidence in which both scoped diagrams already received
100/100 `PASS_SCOPED`; the repo ledger preserves the evidence scope and
canonical digests.
sample threshold decision is recorded. Documentation readiness cannot pass
until both scoped diagrams have a recorded reviewer threshold and signed
scores.
All jobs upload the shared `artifacts/` tree even after failure. Numeric
retention remains an organization/provider decision; the workflow intentionally
@@ -20,6 +20,10 @@ The provider-independent cache defaults are:
- public source maps: disabled
- service worker/offline cache: disabled
HTML, JSON config/manifest, and hashed JavaScript MIME types are also compared
to the declared allowlist; a cache-correct response with a mismatched
`Content-Type` still fails the hosting gate.
`corepack pnpm verify:hosting-headers` uses a deterministic fixture locally.
Set `HOSTING_BASE_URL` to probe deployed responses; production promotion
requires the artifact to report `mode: "live"`.
+1 -1
View File
@@ -16,7 +16,7 @@
"lint": "eslint src scripts tests vite.config.js vitest.config.js playwright.config.js --max-warnings=0",
"check:architecture": "node scripts/check-architecture.mjs",
"check:types": "tsc --allowJs --checkJs --noEmit",
"check:types:fixture": "tsc --allowJs --checkJs --noEmit --target ES2022 --module NodeNext --moduleResolution NodeNext tests/fixtures/typecheck/invalid-port-call.js",
"check:types:fixture": "tsc --ignoreConfig --allowJs --checkJs --noEmit --target ES2022 --module NodeNext --moduleResolution NodeNext tests/fixtures/typecheck/invalid-port-call.js",
"test:runtime-schema": "vitest run tests/runtime-schema --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/runtime-schema.xml --passWithNoTests",
"test:unit": "vitest run tests/unit --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/unit.xml",
"test:component": "vitest run tests/component --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/component.xml",
+22 -15
View File
@@ -1,6 +1,7 @@
import { readFile, writeFile } from "node:fs/promises";
import { evaluateBundleBudget } from "../src/application/policies/performance-budgets.js";
import { classifyViteJavascript } from "./lib/classify-vite-bundle.mjs";
const report =
/** @type {{
@@ -10,7 +11,7 @@ const report =
JSON.parse(await readFile("artifacts/performance/bundle.json", "utf8"))
);
const viteManifest =
/** @type {Record<string, { file: string, isEntry?: boolean }>} */ (
/** @type {Record<string, { file: string, isEntry?: boolean, imports?: string[] }>} */ (
JSON.parse(await readFile("dist/.vite/manifest.json", "utf8"))
);
const budgets =
@@ -21,24 +22,19 @@ const budgets =
const outputByPath = new Map(
report.outputs.map((output) => [output.path.replace(/^dist\//, ""), output]),
);
const initialFiles = new Set(
Object.values(viteManifest)
.filter((entry) => entry.isEntry)
.map((entry) => entry.file),
);
const lazyFiles = new Set(
Object.values(viteManifest)
.filter((entry) => !entry.isEntry && entry.file.endsWith(".js"))
.map((entry) => entry.file),
);
const initialJsGzipBytes = [...initialFiles].reduce(
const classification = classifyViteJavascript(viteManifest);
const initialJsGzipBytes = classification.initialFiles.reduce(
(total, file) => total + (outputByPath.get(file)?.gzipBytes ?? 0),
0,
);
const lazyChunks = [...lazyFiles].map((file) => ({
const lazyChunks = classification.lazyFiles.map((file) => ({
path: file,
gzipBytes: outputByPath.get(file)?.gzipBytes ?? 0,
}));
const missingOutputs = [
...classification.initialFiles,
...classification.lazyFiles,
].filter((file) => !outputByPath.has(file));
const measurements = { initialJsGzipBytes, lazyChunks };
const result = evaluateBundleBudget(measurements, budgets);
const fixtures = [
@@ -70,10 +66,16 @@ const fixtures = [
).passed,
},
];
const passed = result.passed && fixtures.every((fixture) => fixture.passed);
const passed =
result.passed &&
fixtures.every((fixture) => fixture.passed) &&
classification.missingImports.length === 0 &&
missingOutputs.length === 0;
const completedReport = {
...report,
measurements,
classification,
missingOutputs,
thresholds: budgets,
results: result,
fixtures,
@@ -85,7 +87,12 @@ await writeFile(
`${JSON.stringify(completedReport, null, 2)}\n`,
);
if (!passed) {
process.stderr.write("Bundle budget exceeded.\n");
process.stderr.write(
`Bundle budget or manifest integrity failed: ${[
...classification.missingImports,
...missingOutputs,
].join(", ")}\n`,
);
process.exit(1);
}
process.stdout.write(
+49
View File
@@ -0,0 +1,49 @@
/**
* @typedef {{
* file: string,
* isEntry?: boolean,
* imports?: string[]
* }} ViteManifestEntry
*/
/**
* Static imports of an entry are part of initial JavaScript. Every remaining
* JavaScript output is governed by the lazy-chunk budget.
*
* @param {Record<string, ViteManifestEntry>} manifest
*/
export function classifyViteJavascript(manifest) {
const initialFiles = new Set();
const visitedKeys = new Set();
const pendingKeys = Object.entries(manifest)
.filter(([, entry]) => entry.isEntry)
.map(([key]) => key);
const missingImports = [];
while (pendingKeys.length > 0) {
const key = /** @type {string} */ (pendingKeys.pop());
if (visitedKeys.has(key)) continue;
visitedKeys.add(key);
const entry = manifest[key];
if (!entry) {
missingImports.push(key);
continue;
}
if (entry.file.endsWith(".js")) initialFiles.add(entry.file);
pendingKeys.push(...(entry.imports ?? []));
}
const allJavaScript = new Set(
Object.values(manifest)
.map((entry) => entry.file)
.filter((file) => file.endsWith(".js")),
);
const lazyFiles = [...allJavaScript].filter(
(file) => !initialFiles.has(file),
);
return Object.freeze({
initialFiles: Object.freeze([...initialFiles].sort()),
lazyFiles: Object.freeze(lazyFiles.sort()),
missingImports: Object.freeze(missingImports.sort()),
});
}
+12 -29
View File
@@ -3,43 +3,29 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
const ledger = JSON.parse(
await readFile("docs/architecture/review-ledger.json", "utf8"),
);
const evidence = await readFile(ledger.evidenceReport.repoPath, "utf8");
const results = [];
for (const [diagram, review] of Object.entries(ledger.reviews)) {
const sourceReferenced = evidence.includes(review.sourcePath);
const digestReferenced =
/^[0-9a-f]{64}$/.test(review.sha256) &&
evidence.includes(review.sha256);
const content = await readFile(review.path, "utf8");
const hasDiagram = /```mermaid[\s\S]+```/.test(content);
const scorePass =
review.thresholdSatisfied === true &&
review.verdict === "PASS" &&
typeof ledger.reviewerThreshold === "number" &&
typeof review.score === "number" &&
evidence.includes(`| ${review.score} | PASS |`);
review.score >= ledger.reviewerThreshold;
results.push({
diagram,
sourcePath: review.sourcePath,
sha256: review.sha256,
sourceReferenced,
digestReferenced,
reviewer: ledger.reviewer,
path: review.path,
hasDiagram,
reviewer: review.reviewer,
score: review.score,
scorePass,
passed:
sourceReferenced &&
digestReferenced &&
ledger.reviewer === "wiki-diagram-reviewer" &&
ledger.standard === "rules/diagram-standards.md v2" &&
hasDiagram &&
Boolean(review.reviewer) &&
scorePass &&
ledger.status === "PASS_SCOPED",
});
}
const reportDigestValid =
/^[0-9a-f]{64}$/.test(ledger.evidenceReport.canonicalSha256) &&
evidence.includes(ledger.evidenceReport.canonicalSha256);
const passed =
reportDigestValid &&
results.length === 2 &&
results.every((result) => result.passed);
const passed = results.every((result) => result.passed);
await mkdir("artifacts/quality", { recursive: true });
await writeFile(
"artifacts/quality/documentation-review.json",
@@ -48,10 +34,7 @@ await writeFile(
schemaVersion: 1,
generatedAt: new Date().toISOString(),
status: ledger.status,
reviewer: ledger.reviewer,
standard: ledger.standard,
evidenceReport: ledger.evidenceReport,
reportDigestValid,
reviewerThreshold: ledger.reviewerThreshold,
results,
passed,
},
@@ -61,7 +44,7 @@ await writeFile(
);
if (!passed) {
process.stderr.write(
"Documentation readiness: FAIL_UNVERIFIED (canonical scoped-review evidence is incomplete)\n",
"Documentation readiness: FAIL_UNVERIFIED (reviewer threshold and signed reviews required)\n",
);
process.exit(1);
}
+31 -8
View File
@@ -7,6 +7,11 @@ const securityPolicy = JSON.parse(
await readFile("config/hosting/security-headers.json", "utf8"),
);
const baseUrl = process.env.HOSTING_BASE_URL;
const distFiles = (await readdir("dist", { recursive: true })).map(String);
const publicSourceMaps = distFiles.filter((file) => file.endsWith(".map"));
const publicServiceWorkers = distFiles.filter((file) =>
/(?:^|\/)(?:service-worker|sw)(?:[.-][^/]*)?\.js$/i.test(file),
);
/** @type {Record<string, Record<string, string>>} */
let responses;
@@ -15,13 +20,13 @@ let mode;
if (baseUrl) {
mode = "live";
const assets = await readdir("dist/assets");
const hashedAsset = assets.find((file) => !file.endsWith(".map"));
if (!hashedAsset) throw new Error("No built hashed asset found.");
const hashedJavaScript = assets.find((file) => file.endsWith(".js"));
if (!hashedJavaScript) throw new Error("No built hashed JavaScript found.");
const paths = {
index: "/",
runtimeConfig: "/config.json",
releaseManifest: "/release-manifest.json",
hashedAsset: `/assets/${hashedAsset}`,
hashedAsset: `/assets/${hashedJavaScript}`,
};
responses = {};
for (const [surface, pathname] of Object.entries(paths)) {
@@ -51,6 +56,18 @@ for (const [surface, policy] of Object.entries(cachePolicy.surfaces)) {
observed,
passed: observed === policy.cacheControl,
});
const observedContentType = responses[surface]?.["content-type"];
const observedMime = observedContentType
?.split(";", 1)[0]
.trim()
.toLowerCase();
results.push({
surface,
header: "content-type",
expected: policy.contentTypes,
observed: observedContentType,
passed: policy.contentTypes.includes(observedMime),
});
if (policy.securityHeaders) {
for (const [header, expected] of Object.entries(securityPolicy.headers)) {
const observedSecurity = responses[surface]?.[header.toLowerCase()];
@@ -69,15 +86,19 @@ results.push({
surface: "sourceMap",
header: "public",
expected: false,
observed: cachePolicy.surfaces.sourceMap.public,
passed: cachePolicy.surfaces.sourceMap.public === false,
observed: publicSourceMaps.length > 0,
passed:
cachePolicy.surfaces.sourceMap.public === false &&
publicSourceMaps.length === 0,
});
results.push({
surface: "serviceWorker",
header: "enabled",
expected: false,
observed: cachePolicy.surfaces.serviceWorker.enabled,
passed: cachePolicy.surfaces.serviceWorker.enabled === false,
observed: publicServiceWorkers.length > 0,
passed:
cachePolicy.surfaces.serviceWorker.enabled === false &&
publicServiceWorkers.length === 0,
});
const passed = results.every((result) => result.passed);
@@ -100,7 +121,9 @@ await writeFile(
);
if (!passed) {
process.stderr.write("Hosting cache/security header verification failed.\n");
process.stderr.write(
"Hosting cache/content-type/security header verification failed.\n",
);
process.exit(1);
}
process.stdout.write(
+12 -1
View File
@@ -2,7 +2,10 @@ import { createHash } from "node:crypto";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { verifyCompatibilityTuple } from "../src/application/policies/compatibility.js";
import { compareReleaseToRuntime } from "../src/contracts/release-tokens.js";
import {
compareReleaseToRuntime,
RELEASE_TOKEN_REGISTRY,
} from "../src/contracts/release-tokens.js";
const fixturesDocument =
/** @type {{
@@ -38,6 +41,14 @@ const actualAssetManifestHash = createHash("sha256")
const artifactComparison = compareReleaseToRuntime(release, runtimeConfig);
const artifactMismatches = [...artifactComparison.mismatches];
for (const token of Object.keys(RELEASE_TOKEN_REGISTRY)) {
if (typeof release[token] !== "string" || release[token].length === 0) {
artifactMismatches.push(`releaseToken:${token}`);
}
}
if (!Number.isFinite(Date.parse(release.builtAt))) {
artifactMismatches.push("releaseToken:builtAtFormat");
}
if (release.assetManifestHash !== actualAssetManifestHash) {
artifactMismatches.push("assetManifestContent");
}
+40
View File
@@ -0,0 +1,40 @@
import { describe, expect, it } from "vitest";
import { classifyViteJavascript } from "../../scripts/lib/classify-vite-bundle.mjs";
describe("Vite bundle classification", () => {
it("counts transitive static imports as initial and keeps dynamic chunks lazy", () => {
expect(
classifyViteJavascript({
"index.html": {
file: "assets/entry.js",
isEntry: true,
imports: ["_shared.js"],
},
"_shared.js": { file: "assets/shared.js", imports: ["_runtime.js"] },
"_runtime.js": { file: "assets/runtime.js" },
"src/lazy.js": { file: "assets/lazy.js" },
}),
).toEqual({
initialFiles: [
"assets/entry.js",
"assets/runtime.js",
"assets/shared.js",
],
lazyFiles: ["assets/lazy.js"],
missingImports: [],
});
});
it("reports a manifest import that cannot be resolved", () => {
expect(
classifyViteJavascript({
"index.html": {
file: "assets/entry.js",
isEntry: true,
imports: ["_missing.js"],
},
}).missingImports,
).toEqual(["_missing.js"]);
});
});