feat: 기능 추가 과정중

This commit is contained in:
donghyeon-ka
2026-07-30 15:58:20 +09:00
parent d3ef801fe6
commit 6c52cdb916
648 changed files with 126325 additions and 6680 deletions
-110
View File
@@ -1,110 +0,0 @@
import { mkdir, readdir, writeFile } from "node:fs/promises";
import { spawnSync } from "node:child_process";
await mkdir("artifacts/quality", { recursive: true });
const pnpmCli = /** @type {string} */ (process.env.npm_execpath);
if (!pnpmCli) {
throw new Error("check:architecture must run through the pnpm script");
}
/** @param {string[]} arguments_ */
function runPnpm(arguments_) {
return spawnSync(process.execPath, [pnpmCli, ...arguments_], {
encoding: "utf8",
});
}
const production = runPnpm(
[
"exec",
"depcruise",
"src",
"--config",
".dependency-cruiser.cjs",
"--output-type",
"json",
],
);
await writeFile(
"artifacts/quality/dependency-report.json",
production.stdout || JSON.stringify({ summary: { errors: 1 } }),
);
if (production.status !== 0) {
process.stderr.write(
production.error?.message ?? production.stderr ?? production.stdout ?? "failed",
);
process.exit(production.status ?? 1);
}
const allowed = runPnpm(
[
"exec",
"eslint",
"tests/fixtures/architecture/allowed",
"--no-ignore",
"--max-warnings=0",
],
);
const forbidden = runPnpm(
[
"exec",
"eslint",
"tests/fixtures/architecture/forbidden",
"--no-ignore",
"--max-warnings=0",
],
);
/** @param {string} directory @returns {Promise<string[]>} */
async function fixtureFiles(directory) {
const entries = await readdir(directory, { withFileTypes: true });
const files = await Promise.all(
entries.map((entry) => {
const target = `${directory}/${entry.name}`;
return entry.isDirectory()
? fixtureFiles(target)
: /\.(?:js|jsx|mjs|ts|tsx|mts)$/.test(entry.name)
? [target]
: [];
}),
);
return files.flat();
}
const forbiddenResults = await Promise.all(
(await fixtureFiles("tests/fixtures/architecture/forbidden")).map((file) => ({
file,
result: runPnpm([
"exec",
"eslint",
file,
"--no-ignore",
"--max-warnings=0",
]),
})),
);
const acceptedForbidden = forbiddenResults.filter(
({ result }) => result.status === 0,
);
if (
allowed.status !== 0 ||
forbidden.status === 0 ||
acceptedForbidden.length > 0
) {
process.stderr.write(allowed.stderr || allowed.stdout);
process.stderr.write(forbidden.stderr || forbidden.stdout);
for (const { file } of acceptedForbidden) {
process.stderr.write(`Forbidden fixture was accepted: ${file}\n`);
}
process.exit(1);
}
process.stdout.write(
`Architecture fixtures: allowed PASS, ${forbiddenResults.length} forbidden rejected\n`,
);
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,207 @@
import { spawnSync } from "node:child_process";
import { readFile, readdir } from "node:fs/promises";
import path from "node:path";
function requiredPnpmCli(): string {
const executable = process.env.npm_execpath;
if (!executable) {
throw new Error(
"check:browser-file-storage-boundaries must run through pnpm",
);
}
return executable;
}
function runEslint(path: string) {
return spawnSync(
process.execPath,
[
requiredPnpmCli(),
"exec",
"eslint",
path,
"--no-ignore",
"--max-warnings=0",
],
{ encoding: "utf8" },
);
}
function runEslintSource(source: string, virtualPath: string) {
return spawnSync(
process.execPath,
[
requiredPnpmCli(),
"exec",
"eslint",
"--stdin",
"--stdin-filename",
virtualPath,
"--no-ignore",
"--max-warnings=0",
],
{ encoding: "utf8", input: source },
);
}
const allowed = runEslint(
"tests/fixtures/browser-file-storage-boundaries/allowed",
);
const forbiddenRoot =
"tests/fixtures/browser-file-storage-boundaries/forbidden";
const forbiddenFiles = (
await readdir(forbiddenRoot, { withFileTypes: true })
)
.filter((entry) => entry.isFile() && /\.tsx?$/u.test(entry.name))
.map((entry) => path.join(forbiddenRoot, entry.name));
const forbiddenResults = forbiddenFiles.map((file) => ({
file,
result: runEslint(file),
}));
const acceptedForbidden = forbiddenResults.filter(
({ result }) => result.status === 0,
);
const indexedDbBypassSource = await readFile(
path.join(forbiddenRoot, "direct-indexeddb.ts"),
"utf8",
);
const objectUrlBypassSource = await readFile(
path.join(forbiddenRoot, "object-url.ts"),
"utf8",
);
const blobBypassSource = await readFile(
path.join(forbiddenRoot, "direct-blob.ts"),
"utf8",
);
const crossContextAdapterSource = await readFile(
"tests/fixtures/browser-file-storage-boundaries/allowed/cross-context-host.ts",
"utf8",
);
const browserStorageAdapterSource = await readFile(
"tests/fixtures/browser-file-storage-boundaries/allowed/browser-storage-host.ts",
"utf8",
);
const protectedSourceFixtureNames = [
"aliased-globalthis.ts",
"class-field-alias.ts",
"constructor-root-escape.ts",
"default-parameter-alias.ts",
"direct-blob.ts",
"dynamic-capability-key.ts",
"global-object-container.ts",
"identity-wrapped-global.ts",
"instance-property-alias.ts",
"property-descriptor-access.ts",
] as const;
const protectedSourceFixtures = await Promise.all(
protectedSourceFixtureNames.map(async (file) => ({
file,
source: await readFile(path.join(forbiddenRoot, file), "utf8"),
})),
);
const approvedAdapterResults = [
{
file: "src/adapters/storage/indexeddb/boundary-fixture.ts",
result: runEslintSource(
indexedDbBypassSource,
"src/adapters/storage/indexeddb/boundary-fixture.ts",
),
},
{
file: "src/adapters/cross-context-invalidation/boundary-fixture.ts",
result: runEslintSource(
crossContextAdapterSource,
"src/adapters/cross-context-invalidation/boundary-fixture.ts",
),
},
{
file: "src/adapters/storage/boundary-fixture.ts",
result: runEslintSource(
browserStorageAdapterSource,
"src/adapters/storage/boundary-fixture.ts",
),
},
{
file: "src/adapters/browser-transfer/boundary-fixture.ts",
result: runEslintSource(
blobBypassSource,
"src/adapters/browser-transfer/boundary-fixture.ts",
),
},
];
const rejectedApprovedAdapters = approvedAdapterResults.filter(
({ result }) => result.status !== 0,
);
const misplacedAdapterResults = [
{
file: "src/adapters/http/boundary-fixture.ts",
result: runEslintSource(
indexedDbBypassSource,
"src/adapters/http/boundary-fixture.ts",
),
},
{
file: "src/features/reference-feature/adapters/boundary-fixture.ts",
result: runEslintSource(
objectUrlBypassSource,
"src/features/reference-feature/adapters/boundary-fixture.ts",
),
},
{
file: "src/adapters/http/cross-context-boundary-fixture.ts",
result: runEslintSource(
crossContextAdapterSource,
"src/adapters/http/cross-context-boundary-fixture.ts",
),
},
{
file: "src/adapters/http/browser-storage-boundary-fixture.ts",
result: runEslintSource(
browserStorageAdapterSource,
"src/adapters/http/browser-storage-boundary-fixture.ts",
),
},
...protectedSourceFixtures.map(({ file, source }) => {
const virtualPath = `src/presentation/${file}`;
return {
file: virtualPath,
result: runEslintSource(source, virtualPath),
};
}),
];
const acceptedMisplacedAdapters = misplacedAdapterResults.filter(
({ result }) => result.status === 0,
);
if (
allowed.status !== 0 ||
rejectedApprovedAdapters.length > 0 ||
forbiddenFiles.length === 0 ||
acceptedForbidden.length > 0 ||
acceptedMisplacedAdapters.length > 0
) {
process.stderr.write(allowed.stderr || allowed.stdout);
for (const { file, result } of rejectedApprovedAdapters) {
process.stderr.write(
`${file}: owned browser capability adapter was rejected\n`,
);
process.stderr.write(result.stderr || result.stdout);
}
for (const { file, result } of acceptedForbidden) {
process.stderr.write(
`${file}: forbidden browser API fixture was accepted\n`,
);
process.stderr.write(result.stderr || result.stdout);
}
for (const { file, result } of acceptedMisplacedAdapters) {
process.stderr.write(
`${file}: native browser storage access outside its owned adapter was accepted\n`,
);
process.stderr.write(result.stderr || result.stdout);
}
process.exit(1);
}
process.stdout.write(
`Browser file/storage boundaries: PASS (owned adapter allowed, ${forbiddenFiles.length + misplacedAdapterResults.length} direct or misplaced native access cases rejected)\n`,
);
@@ -1,10 +1,17 @@
import { readdir } from "node:fs/promises";
import { spawnSync } from "node:child_process";
const pnpmCli = /** @type {string} */ (process.env.npm_execpath);
function requiredPnpmCli(): string {
const executable = process.env.npm_execpath;
if (!executable) {
throw new Error("check:browser-security must run through the pnpm script");
}
return executable;
}
/** @param {string[]} arguments_ */
function runPnpm(arguments_) {
const pnpmCli = requiredPnpmCli();
function runPnpm(arguments_: string[]) {
return spawnSync(process.execPath, [pnpmCli, ...arguments_], {
encoding: "utf8",
});
@@ -1,23 +1,28 @@
import { readFile, writeFile } from "node:fs/promises";
import { evaluateBundleBudget } from "../src/application/policies/performance-budgets.js";
import { classifyViteJavascript } from "./lib/classify-vite-bundle.mjs";
import { evaluateBundleBudget } from "../src/application/policies/performance-budgets.ts";
import { classifyViteJavascript } from "./lib/classify-vite-bundle.ts";
const report =
/** @type {{
* outputs: Array<{ path: string, gzipBytes: number }>,
* [key: string]: unknown
* }} */ (
JSON.parse(await readFile("artifacts/performance/bundle.json", "utf8"))
);
const viteManifest =
/** @type {Record<string, { file: string, isEntry?: boolean, imports?: string[] }>} */ (
JSON.parse(await readFile("dist/.vite/manifest.json", "utf8"))
);
const budgets =
/** @type {{ initialJsGzipBytes: number, lazyChunkGzipBytes: number }} */ (
JSON.parse(await readFile("config/performance/budgets.json", "utf8")).bundle
);
type BundleOutput = { path: string; gzipBytes: number };
type BundleReport = { outputs: BundleOutput[]; [key: string]: unknown };
type ViteManifest = Record<
string,
{ file: string; isEntry?: boolean; imports?: string[] }
>;
type BundleBudgets = {
initialJsGzipBytes: number;
lazyChunkGzipBytes: number;
};
const report = JSON.parse(
await readFile("artifacts/performance/bundle.json", "utf8"),
) as BundleReport;
const viteManifest = JSON.parse(
await readFile("dist/.vite/manifest.json", "utf8"),
) as ViteManifest;
const budgets = JSON.parse(
await readFile("config/performance/budgets.json", "utf8"),
).bundle as BundleBudgets;
const outputByPath = new Map(
report.outputs.map((output) => [output.path.replace(/^dist\//, ""), output]),
-111
View File
@@ -1,111 +0,0 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import {
evaluatePromotionReadiness,
PROMOTION_FORMULA,
} from "../src/application/policies/promotion-readiness.js";
const document = JSON.parse(await readFile("config/ci/gates.json", "utf8"));
const workflow = await readFile(document.providerAdapter, "utf8");
const failures = [];
const stageFormula = {
merge: PROMOTION_FORMULA.MERGE_READY,
release: PROMOTION_FORMULA.RELEASE_READY,
production: PROMOTION_FORMULA.PROD_PROMOTION_READY,
field: PROMOTION_FORMULA.FIELD_SLO_READY,
documentation: PROMOTION_FORMULA.DOCUMENTATION_READY,
};
for (const [stage, expectedGates] of Object.entries(stageFormula)) {
const actual = document.stages[stage]?.gates;
if (JSON.stringify(actual) !== JSON.stringify(expectedGates)) {
failures.push(`${stage} gate formula drift`);
}
}
const configuredGateIds = Object.keys(document.gates).sort();
const expectedGateIds = Array.from(
{ length: 26 },
(_, index) => `FE-GATE-${String(index + 1).padStart(3, "0")}`,
);
if (JSON.stringify(configuredGateIds) !== JSON.stringify(expectedGateIds)) {
failures.push("gate registry must contain FE-GATE-001..026 exactly once");
}
for (const [gateId, gate] of Object.entries(document.gates)) {
if (!gate.steps?.length || !gate.evidence?.length || !gate.retentionClass) {
failures.push(`${gateId} lacks command, evidence, or retention wiring`);
}
}
const forbiddenWorkflowPatterns = [
/continue-on-error\s*:/,
/retention-days\s*:/,
/allow_failure\s*:/,
];
for (const pattern of forbiddenWorkflowPatterns) {
if (pattern.test(workflow)) {
failures.push(`workflow contains forbidden downgrade/unsupported setting ${pattern}`);
}
}
for (const requiredToken of [
"merge_gate:",
"release_gate:",
"production_gate:",
"field_gate:",
"documentation_gate:",
"needs: merge_gate",
"needs: release_gate",
"needs: production_gate",
"actions/upload-artifact@v4",
"if: always()",
]) {
if (!workflow.includes(requiredToken)) {
failures.push(`workflow missing ${requiredToken}`);
}
}
const passingResults = Object.fromEntries(
expectedGateIds.map((gateId) => [gateId, /** @type {const} */ ("PASS")]),
);
const allPass = evaluatePromotionReadiness(passingResults);
const negativeFixtures = [];
for (const [readiness, gateIds] of Object.entries(PROMOTION_FORMULA)) {
const failedGate = gateIds[0];
const result = evaluatePromotionReadiness({
...passingResults,
[failedGate]: "FAIL",
});
const passed =
/** @type {Readonly<Record<string, boolean>>} */ (result)[readiness] ===
false;
negativeFixtures.push({ readiness, failedGate, passed });
if (!passed) failures.push(`${readiness} did not fail closed`);
}
if (!Object.values(allPass).every(Boolean)) {
failures.push("all-PASS formula did not produce every readiness state");
}
const report = {
schemaVersion: 1,
generatedAt: new Date().toISOString(),
providerAdapter: document.providerAdapter,
gateCount: configuredGateIds.length,
noDowngrade: failures.every(
(failure) => !failure.includes("downgrade"),
),
durationStatus: document.retention.durationStatus,
negativeFixtures,
failures,
passed: failures.length === 0,
};
await mkdir("artifacts/quality", { recursive: true });
await writeFile(
"artifacts/quality/ci-contract.json",
`${JSON.stringify(report, null, 2)}\n`,
);
if (failures.length > 0) {
process.stderr.write(`CI contract failed:\n${failures.join("\n")}\n`);
process.exit(1);
}
process.stdout.write("CI contract: 26 blocking gates and 4-tier graph PASS\n");
+266
View File
@@ -0,0 +1,266 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import {
evaluatePromotionReadiness,
PROMOTION_FORMULA,
type GateResult,
} from "../src/application/policies/promotion-readiness.ts";
type GateDefinition = Readonly<{
steps?: readonly unknown[];
evidence?: readonly string[];
retentionClass?: string;
}>;
type CiContractDocument = Readonly<{
providerAdapter: string;
stages: Readonly<Record<string, Readonly<{ gates?: readonly string[] }>>>;
gates: Readonly<Record<string, GateDefinition>>;
retention: Readonly<{ durationStatus: unknown }>;
}>;
const document = parseCiContractDocument(
JSON.parse(await readFile("config/ci/gates.json", "utf8")),
);
const workflow = await readFile(document.providerAdapter, "utf8");
const nodeVersion = (await readFile(".nvmrc", "utf8")).trim();
const gateRunner = await readFile("scripts/run-ci-gate.ts", "utf8");
const drillRunner = await readFile("scripts/drill-runbook.ts", "utf8");
const buildManifestGenerator = await readFile(
"scripts/generate-build-manifest.ts",
"utf8",
);
const failures: string[] = [];
if (!/^\d+\.\d+\.\d+$/.test(nodeVersion)) {
failures.push(".nvmrc must contain one exact Node.js semantic version");
}
const setupNodeCount =
workflow.match(/uses:\s*actions\/setup-node@v4/g)?.length ?? 0;
const nodeVersionFileCount =
workflow.match(/node-version-file:\s*\.nvmrc/g)?.length ?? 0;
if (setupNodeCount === 0 || nodeVersionFileCount !== setupNodeCount) {
failures.push("every setup-node step must use node-version-file: .nvmrc");
}
if (/node-version\s*:/.test(workflow) || /NODE_VERSION\s*:/.test(workflow)) {
failures.push("workflow must not override the exact .nvmrc Node.js pin");
}
const stageFormula: Readonly<Record<string, readonly string[]>> = {
merge: PROMOTION_FORMULA.MERGE_READY,
release: PROMOTION_FORMULA.RELEASE_READY,
production: PROMOTION_FORMULA.PROD_PROMOTION_READY,
field: PROMOTION_FORMULA.FIELD_SLO_READY,
documentation: PROMOTION_FORMULA.DOCUMENTATION_READY,
};
for (const [stage, expectedGates] of Object.entries(stageFormula)) {
const actual = document.stages[stage]?.gates;
if (JSON.stringify(actual) !== JSON.stringify(expectedGates)) {
failures.push(`${stage} gate formula drift`);
}
}
const configuredGateIds = Object.keys(document.gates).sort();
const expectedGateIds = Array.from(
{ length: 26 },
(_, index) => `FE-GATE-${String(index + 1).padStart(3, "0")}`,
);
if (JSON.stringify(configuredGateIds) !== JSON.stringify(expectedGateIds)) {
failures.push("gate registry must contain FE-GATE-001..026 exactly once");
}
for (const [gateId, gate] of Object.entries(document.gates)) {
if (!gate.steps?.length || !gate.evidence?.length || !gate.retentionClass) {
failures.push(`${gateId} lacks command, evidence, or retention wiring`);
}
}
const runbookGateEvidence = Object.freeze({
"FE-GATE-016": "artifacts/runbooks/FE-RB-005/record.json",
"FE-GATE-021": "artifacts/runbooks/FE-RB-001/record.json",
"FE-GATE-022": "artifacts/runbooks/FE-RB-002/record.json",
"FE-GATE-023": "artifacts/runbooks/FE-RB-003/record.json",
"FE-GATE-024": "artifacts/runbooks/FE-RB-004/record.json",
"FE-GATE-025": "artifacts/runbooks/FE-RB-005/record.json",
});
for (const [gateId, evidencePath] of Object.entries(runbookGateEvidence)) {
const evidence = document.gates[gateId]?.evidence;
if (
!Array.isArray(evidence) ||
evidence.length !== 1 ||
evidence[0] !== evidencePath
) {
failures.push(`${gateId} runbook evidence path drift`);
}
}
if (
!drillRunner.includes(
"const artifactDirectory = `artifacts/runbooks/${runbookId}`",
) ||
drillRunner.includes(
"artifacts/runbooks/${runbookId}/${release.releaseId}",
)
) {
failures.push(
"runbook evidence path must be stable while releaseId stays in the record",
);
}
const forbiddenWorkflowPatterns = [
/continue-on-error\s*:/,
/retention-days\s*:/,
/timeout-minutes\s*:/,
/allow_failure\s*:/,
];
for (const pattern of forbiddenWorkflowPatterns) {
if (pattern.test(workflow)) {
failures.push(`workflow contains forbidden downgrade/unsupported setting ${pattern}`);
}
}
for (const requiredToken of [
"merge_gate:",
"release_gate:",
"production_gate:",
"field_gate:",
"documentation_gate:",
"needs: merge_gate",
"needs: release_gate",
"needs: production_gate",
"actions/upload-artifact@v4",
"if: always()",
"permissions:",
"contents: read",
'CI: "true"',
'VITE_BUILD_ID: "gitea-${{ gitea.run_id }}-${{ gitea.run_attempt }}"',
'VITE_COMMIT_SHA: "${{ gitea.sha }}"',
'RELEASE_ID: "${{ gitea.ref }}-${{ gitea.run_id }}-${{ gitea.run_attempt }}"',
'CI_RUNNER_IMAGE: "${{ vars.RUNNER_IMAGE_DIGEST }}"',
]) {
if (!workflow.includes(requiredToken)) {
failures.push(`workflow missing ${requiredToken}`);
}
}
for (const requiredToken of [
"ciCheckoutIdentityFailures",
"ciBuildEnvironmentFailures",
"SOURCE_DATE_EPOCH",
'"--format=%H%n%ct"',
"env: gateEnvironment",
]) {
if (!gateRunner.includes(requiredToken)) {
failures.push(`CI gate runner missing ${requiredToken}`);
}
}
for (const requiredToken of [
"assertCiBuildEnvironment(process.env)",
"releaseId",
"sourceDateEpoch",
]) {
if (!buildManifestGenerator.includes(requiredToken)) {
failures.push(`build manifest generator missing ${requiredToken}`);
}
}
const passingResults: Record<string, GateResult> = {};
for (const gateId of expectedGateIds) passingResults[gateId] = "PASS";
const allPass = evaluatePromotionReadiness(passingResults);
const negativeFixtures: Array<{
readiness: keyof typeof PROMOTION_FORMULA;
failedGate: string;
passed: boolean;
}> = [];
for (const readiness of Object.keys(PROMOTION_FORMULA) as Array<
keyof typeof PROMOTION_FORMULA
>) {
const gateIds = PROMOTION_FORMULA[readiness];
const failedGate = gateIds[0];
if (!failedGate) throw new Error(`${readiness} has no configured gates`);
const result = evaluatePromotionReadiness({
...passingResults,
[failedGate]: "FAIL",
});
const passed = result[readiness] === false;
negativeFixtures.push({ readiness, failedGate, passed });
if (!passed) failures.push(`${readiness} did not fail closed`);
}
if (!Object.values(allPass).every(Boolean)) {
failures.push("all-PASS formula did not produce every readiness state");
}
const report = {
schemaVersion: 1,
generatedAt: new Date().toISOString(),
providerAdapter: document.providerAdapter,
nodeVersion,
gateCount: configuredGateIds.length,
noDowngrade: failures.every(
(failure) => !failure.includes("downgrade"),
),
durationStatus: document.retention.durationStatus,
negativeFixtures,
failures,
passed: failures.length === 0,
};
await mkdir("artifacts/quality", { recursive: true });
await writeFile(
"artifacts/quality/ci-contract.json",
`${JSON.stringify(report, null, 2)}\n`,
);
if (failures.length > 0) {
process.stderr.write(`CI contract failed:\n${failures.join("\n")}\n`);
process.exit(1);
}
process.stdout.write("CI contract: 26 blocking gates and 4-tier graph PASS\n");
function parseCiContractDocument(value: unknown): CiContractDocument {
if (!isRecord(value)) throw new TypeError("CI gate config must be an object");
if (
typeof value.providerAdapter !== "string" ||
!isRecord(value.stages) ||
!isRecord(value.gates) ||
!isRecord(value.retention)
) {
throw new TypeError("CI gate config is missing required registries");
}
const stages: Record<string, { gates?: readonly string[] }> = {};
for (const [stage, candidate] of Object.entries(value.stages)) {
if (!isRecord(candidate)) throw new TypeError(`Invalid CI stage: ${stage}`);
if (
candidate.gates !== undefined &&
(!Array.isArray(candidate.gates) ||
!candidate.gates.every((gate) => typeof gate === "string"))
) {
throw new TypeError(`Invalid gate list for CI stage: ${stage}`);
}
stages[stage] = {
gates: candidate.gates as readonly string[] | undefined,
};
}
const gates: Record<string, GateDefinition> = {};
for (const [gateId, candidate] of Object.entries(value.gates)) {
if (!isRecord(candidate)) throw new TypeError(`Invalid CI gate: ${gateId}`);
if (
candidate.evidence !== undefined &&
(!Array.isArray(candidate.evidence) ||
!candidate.evidence.every((path) => typeof path === "string"))
) {
throw new TypeError(`Invalid evidence list for CI gate: ${gateId}`);
}
gates[gateId] = {
steps: Array.isArray(candidate.steps) ? candidate.steps : undefined,
evidence: candidate.evidence as readonly string[] | undefined,
retentionClass:
typeof candidate.retentionClass === "string"
? candidate.retentionClass
: undefined,
};
}
return {
providerAdapter: value.providerAdapter,
stages,
gates,
retention: { durationStatus: value.retention.durationStatus },
};
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
@@ -1,14 +1,30 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { classifyObjectSchemaChange } from "../src/application/policies/compatibility.js";
import { classifyObjectSchemaChange } from "../src/application/policies/compatibility.ts";
type CompatibilitySchema = Readonly<{
required?: readonly string[];
properties?: Readonly<Record<string, unknown>>;
}>;
type CompatibilityFixture = Readonly<{
before: CompatibilitySchema;
after: CompatibilitySchema;
}>;
type CompatibilityFixtures = Readonly<{
families: Readonly<
Record<string, Readonly<Record<"additive" | "breaking", CompatibilityFixture>>>
>;
}>;
const fixtures = JSON.parse(
await readFile("config/compatibility/fixtures.json", "utf8"),
);
) as CompatibilityFixtures;
const results = [];
for (const [family, cases] of Object.entries(fixtures.families)) {
for (const expected of ["additive", "breaking"]) {
for (const expected of ["additive", "breaking"] as const) {
const fixture = cases[expected];
const actual = classifyObjectSchemaChange(fixture.before, fixture.after);
results.push({ family, expected, actual, passed: actual === expected });
@@ -1,18 +1,16 @@
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
import path from "node:path";
// @ts-expect-error Node 24 executes erasable TypeScript for this build-time gate.
import { REQUIRED_COMPONENT_TOKENS, REQUIRED_PRIMITIVE_TOKENS, REQUIRED_SEMANTIC_TOKENS } from "../src/presentation/design-system/tokens/token-contract.ts";
const fixtureMode = process.argv.includes("--fixture");
const failures = [];
const failures: string[] = [];
const tokenFiles = {
primitive: "src/presentation/design-system/tokens/primitive.css",
semantic: "src/presentation/design-system/tokens/semantic.css",
component: "src/presentation/design-system/tokens/component.css",
};
/** @type {Array<readonly [string, string, readonly string[]]>} */
const tokenLayers = [
const tokenLayers: Array<readonly [string, string, readonly string[]]> = [
["primitive", tokenFiles.primitive, REQUIRED_PRIMITIVE_TOKENS],
["semantic", tokenFiles.semantic, REQUIRED_SEMANTIC_TOKENS],
["component", tokenFiles.component, REQUIRED_COMPONENT_TOKENS],
@@ -75,13 +73,12 @@ if (!componentSource.includes("@media (forced-colors: active)")) {
failures.push("forced-colors token fallback is missing");
}
/** @param {string} directory @returns {Promise<string[]>} */
async function listSourceFiles(directory) {
const result = [];
async function listSourceFiles(directory: string): Promise<string[]> {
const result: string[] = [];
for (const entry of await readdir(directory, { withFileTypes: true })) {
const target = path.join(directory, entry.name);
if (entry.isDirectory()) result.push(...(await listSourceFiles(target)));
else if (/\.(js|jsx|mjs|ts|tsx|mts)$/.test(entry.name)) result.push(target);
else if (/\.(?:ts|tsx|mts|cts)$/.test(entry.name)) result.push(target);
}
return result;
}
@@ -103,7 +100,7 @@ for (const file of sources) {
}
if (
!file.includes("src/presentation/design-system/") &&
/presentation\/design-system\/(?!index(?:\.js)?["'])/.test(source)
/presentation\/design-system\/(?!index(?:\.tsx?)?["'])/.test(source)
) {
failures.push(`design-system deep import in ${file}`);
}
@@ -1,17 +1,15 @@
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
import path from "node:path";
// @ts-expect-error Node 24 executes erasable TypeScript for this build-time gate.
import { DIAGNOSTIC_EVENT_REGISTRY } from "../src/contracts/diagnostics.ts";
import { TELEMETRY_REGISTRY } from "../src/contracts/telemetry.js";
import { TELEMETRY_REGISTRY } from "../src/contracts/telemetry.ts";
const fixtureMode = process.argv.includes("--fixture");
const failures = [];
const extensions = /\.(?:js|jsx|mjs|ts|tsx|mts)$/;
const failures: string[] = [];
const extensions = /\.(?:ts|tsx|mts|cts)$/;
/** @param {string} directory @returns {Promise<string[]>} */
async function filesBelow(directory) {
const result = [];
async function filesBelow(directory: string): Promise<string[]> {
const result: string[] = [];
for (const entry of await readdir(directory, { withFileTypes: true })) {
const target = path.join(directory, entry.name);
if (entry.isDirectory()) result.push(...(await filesBelow(target)));
@@ -20,28 +18,26 @@ async function filesBelow(directory) {
return result;
}
const telemetryProducerFiles =
/** @type {Readonly<Record<string, string>>} */ ({
const telemetryProducerFiles: Readonly<Record<string, string>> = {
"app.boot.failed": "src/adapters/diagnostics/bounded-diagnostics.ts",
"api.request.failed": "src/adapters/http/client.js",
"api.request.failed": "src/adapters/http/client.ts",
"ui.render.failed": "src/application/create-application.ts",
"release.mismatch.detected": "src/application/create-application.ts",
"telemetry.delivery.dropped":
"src/adapters/telemetry/best-effort-telemetry.js",
});
const diagnosticProducerFiles =
/** @type {Readonly<Record<string, string>>} */ ({
"src/adapters/telemetry/best-effort-telemetry.ts",
};
const diagnosticProducerFiles: Readonly<Record<string, string>> = {
"app.boot.failed": "src/adapters/diagnostics/bounded-diagnostics.ts",
"http.request.completed": "src/adapters/http/client.js",
"http.request.completed": "src/adapters/http/client.ts",
"cache.operation.failed":
"src/adapters/query-cache/tanstack-query-cache.js",
"src/adapters/query-cache/tanstack-query-cache.ts",
"storage.operation.failed":
"src/adapters/storage/browser-storage-adapter.js",
"src/adapters/storage/browser-storage-adapter.ts",
"route.changed": "src/application/create-application.ts",
"ui.render.failed": "src/application/create-application.ts",
"release.mismatch.detected": "src/application/create-application.ts",
"telemetry.delivery.dropped": "src/bootstrap/runtime-adapters.js",
});
"telemetry.delivery.dropped": "src/bootstrap/runtime-adapters.ts",
};
if (!fixtureMode) {
for (const eventName of Object.keys(TELEMETRY_REGISTRY)) {
@@ -75,7 +71,7 @@ const sensitiveContext =
/\b(?:authorization|cookie|access_token|refresh_token|request_body|response_body|raw_url|query_string|email|user_name)\b/i;
for (const file of sources) {
const source = await readFile(file, "utf8");
if (file.includes("contracts/telemetry.js")) continue;
if (file.includes("contracts/telemetry.ts")) continue;
if (source.includes("console.")) {
failures.push(`direct console diagnostics bypass in ${file}`);
}
@@ -1,19 +1,17 @@
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
import path from "node:path";
// @ts-expect-error Node 24 executes erasable TypeScript for this build-time gate.
import { EN_MESSAGES, KO_MESSAGES, MESSAGE_CATALOGS } from "../src/presentation/i18n/catalog.ts";
const fixtureMode = process.argv.includes("--fixture");
const failures = [];
const sourceExtensions = /\.(?:js|jsx|mjs|ts|tsx|mts)$/;
const failures: string[] = [];
const sourceExtensions = /\.(?:ts|tsx|mts|cts)$/;
const koreanLiteral = /[가-힣]/;
const rawFailureRender =
/(?<!\$)\{\s*(?:failure|error|response|backend)(?:\?\.|\.)[\w?.]*message\s*\}/;
/** @param {string} directory @returns {Promise<string[]>} */
async function listSourceFiles(directory) {
const result = [];
async function listSourceFiles(directory: string): Promise<string[]> {
const result: string[] = [];
for (const entry of await readdir(directory, { withFileTypes: true })) {
const target = path.join(directory, entry.name);
if (entry.isDirectory()) result.push(...(await listSourceFiles(target)));
@@ -22,8 +20,7 @@ async function listSourceFiles(directory) {
return result;
}
/** @param {string} template */
function placeholders(template) {
function placeholders(template: string): string[] {
return [...template.matchAll(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g)]
.map((match) => match[1])
.sort();
@@ -32,10 +29,8 @@ function placeholders(template) {
if (!fixtureMode) {
const canonicalKeys = Object.keys(KO_MESSAGES).sort();
for (const [locale, catalog] of Object.entries(MESSAGE_CATALOGS)) {
const readableCatalog =
/** @type {Readonly<Record<string, string>>} */ (catalog);
const canonicalCatalog =
/** @type {Readonly<Record<string, string>>} */ (KO_MESSAGES);
const readableCatalog = catalog as Readonly<Record<string, string>>;
const canonicalCatalog = KO_MESSAGES as Readonly<Record<string, string>>;
const keys = Object.keys(catalog).sort();
if (JSON.stringify(keys) !== JSON.stringify(canonicalKeys)) {
failures.push(`${locale} catalog keys do not match ko-KR`);
@@ -1,93 +0,0 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import {
scanOptionalRecipeSources,
validateRecipeCatalog,
} from "./lib/optional-recipes.mjs";
const catalog = JSON.parse(
await readFile("config/recipes/frontend-capability-recipes.json", "utf8"),
);
const packageDocument = JSON.parse(await readFile("package.json", "utf8"));
const cleanupCatalog = structuredClone(catalog);
cleanupCatalog.recipes.find(
/** @param {{id: string}} recipe */ (recipe) => recipe.id === "realtime",
).lifecycleMethods = [];
const dependencyCatalog = structuredClone(catalog);
dependencyCatalog.productionRuntimeDependencies = ["zustand"];
const workflowCatalog = structuredClone(catalog);
workflowCatalog.recipes.find(
/** @param {{id: string}} recipe */ (recipe) => recipe.id === "client-workflow",
).serverStatePolicy = "copied-server-state";
const sourceViolations = await scanOptionalRecipeSources(
"tests/fixtures/optional-recipes/forbidden",
{ scanProductionBoundary: false },
);
const productionViolations = await scanOptionalRecipeSources(
"tests/fixtures/optional-recipes/forbidden/production-import",
{ scanProductionBoundary: true },
);
sourceViolations.push(...productionViolations);
const ruleIds = new Set(sourceViolations.map(({ ruleId }) => ruleId));
const results = [
{
id: "cleanup-omission",
passed: validateRecipeCatalog(cleanupCatalog, packageDocument).some(
(violation) => violation === "realtime:CLEANUP_CONTRACT_MISSING",
),
},
{
id: "unselected-runtime-dependency",
passed: validateRecipeCatalog(dependencyCatalog, packageDocument).includes(
"UNSELECTED_RUNTIME_DEPENDENCY",
),
},
{
id: "server-state-policy",
passed: validateRecipeCatalog(workflowCatalog, packageDocument).includes(
"client-workflow:SERVER_STATE_DUPLICATION_POLICY",
),
},
{
id: "vendor-direct-import",
passed: ruleIds.has("VENDOR_IMPORT_OUTSIDE_ADAPTER"),
},
{
id: "credential-leak",
passed: ruleIds.has("CREDENTIAL_LEAK_PATH"),
},
{
id: "server-state-source-duplication",
passed: ruleIds.has("CLIENT_STORE_DUPLICATES_SERVER_STATE"),
},
{
id: "production-imports-recipe",
passed: ruleIds.has("PRODUCTION_IMPORTS_RECIPE"),
},
];
const report = {
schemaVersion: 1,
results,
passed: results.every(({ passed }) => passed),
};
await mkdir("artifacts/quality", { recursive: true });
await writeFile(
"artifacts/quality/optional-recipe-fixtures.json",
`${JSON.stringify(report, null, 2)}\n`,
);
if (!report.passed) {
process.stderr.write(
`Optional recipe negative fixtures failed: ${results
.filter(({ passed }) => !passed)
.map(({ id }) => id)
.join(", ")}\n`,
);
process.exit(1);
}
process.stdout.write(
`Optional recipe negative fixtures: PASS (${results.length} forbidden cases rejected)\n`,
);
+201
View File
@@ -0,0 +1,201 @@
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { measureOptionalRecipeBundle } from "./lib/optional-recipe-bundle.ts";
import {
scanOptionalRecipeSources,
scanProductionBundle,
validateRecipeCatalog,
} from "./lib/optional-recipes.ts";
type ReferenceRuntimeRecipe = Readonly<{
id: string;
referenceRuntime: Readonly<{
sourceRoots: readonly string[];
}>;
}>;
const catalog = JSON.parse(
await readFile("config/recipes/frontend-capability-recipes.json", "utf8"),
);
const packageDocument = JSON.parse(await readFile("package.json", "utf8"));
const catalogRecipes: unknown[] = Array.isArray(catalog.recipes)
? (catalog.recipes as unknown[])
: [];
const referenceRuntimeRecipes = catalogRecipes.filter(
(recipe: unknown): recipe is ReferenceRuntimeRecipe =>
Boolean(
recipe &&
typeof recipe === "object" &&
typeof (recipe as Record<string, unknown>).id === "string" &&
(recipe as Record<string, unknown>).referenceRuntime &&
typeof (recipe as Record<string, unknown>).referenceRuntime ===
"object" &&
Array.isArray(
(
(recipe as Record<string, unknown>)
.referenceRuntime as Record<string, unknown>
).sourceRoots,
),
),
);
const bundleBudgetFixtures = await Promise.all(
referenceRuntimeRecipes.map(async (recipe) => {
const measurement = await measureOptionalRecipeBundle({
recipeId: recipe.id,
sourceRoots: recipe.referenceRuntime.sourceRoots,
bundleBudgetGzipBytes: 1,
});
return Object.freeze({
recipeId: recipe.id,
gzipBytes: measurement.gzipBytes,
fixtureBudgetGzipBytes: measurement.bundleBudgetGzipBytes,
rejected: !measurement.passed,
});
}),
);
const cleanupCatalog = structuredClone(catalog);
cleanupCatalog.recipes.find(
(recipe: { id: string }) => recipe.id === "realtime",
).lifecycleMethods = [];
const dependencyCatalog = structuredClone(catalog);
dependencyCatalog.productionRuntimeDependencies = ["zustand"];
const workflowCatalog = structuredClone(catalog);
workflowCatalog.recipes.find(
(recipe: { id: string }) => recipe.id === "client-workflow",
).serverStatePolicy = "copied-server-state";
const sourceViolations = await scanOptionalRecipeSources(
"tests/fixtures/optional-recipes/forbidden",
{ scanProductionBoundary: false },
);
const productionViolations = await scanOptionalRecipeSources(
"tests/fixtures/optional-recipes/forbidden/production-import",
{ scanProductionBoundary: true },
);
const runtimeCompositionViolations = await scanOptionalRecipeSources(
"tests/fixtures/optional-recipes/forbidden/runtime-composition",
{ scanProductionBoundary: true },
);
const bundleFixtureRoot = ".tmp/optional-recipe-runtime-bundle";
await rm(bundleFixtureRoot, { recursive: true, force: true });
await mkdir(`${bundleFixtureRoot}/assets`, { recursive: true });
await writeFile(
`${bundleFixtureRoot}/assets/runtime.js`,
'throw new TypeError("OPFS runtime policy is invalid.");\n',
);
const bundleViolations = await scanProductionBundle(bundleFixtureRoot);
await rm(bundleFixtureRoot, { recursive: true, force: true });
const inventoryFixtureRoot =
".tmp/optional-recipe-runtime-module-inventory";
await rm(inventoryFixtureRoot, { recursive: true, force: true });
await mkdir(`${inventoryFixtureRoot}/.vite`, { recursive: true });
await writeFile(`${inventoryFixtureRoot}/.vite/manifest.json`, "{}\n");
await writeFile(
`${inventoryFixtureRoot}/.vite/module-inventory.json`,
`${JSON.stringify({
schemaVersion: 1,
chunks: [
{
fileName: "assets/application.js",
modules: [
"src/adapters/browser-files/browser-file-vault.ts",
],
},
],
})}\n`,
);
const inventoryViolations =
await scanProductionBundle(inventoryFixtureRoot);
await rm(inventoryFixtureRoot, { recursive: true, force: true });
sourceViolations.push(...productionViolations);
sourceViolations.push(...runtimeCompositionViolations);
const ruleIds = new Set(sourceViolations.map(({ ruleId }) => ruleId));
const results = [
{
id: "cleanup-omission",
passed: validateRecipeCatalog(cleanupCatalog, packageDocument).some(
(violation) => violation === "realtime:CLEANUP_CONTRACT_MISSING",
),
},
{
id: "unselected-runtime-dependency",
passed: validateRecipeCatalog(dependencyCatalog, packageDocument).includes(
"UNSELECTED_RUNTIME_DEPENDENCY",
),
},
{
id: "server-state-policy",
passed: validateRecipeCatalog(workflowCatalog, packageDocument).includes(
"client-workflow:SERVER_STATE_DUPLICATION_POLICY",
),
},
{
id: "vendor-direct-import",
passed: ruleIds.has("VENDOR_IMPORT_OUTSIDE_ADAPTER"),
},
{
id: "credential-leak",
passed: ruleIds.has("CREDENTIAL_LEAK_PATH"),
},
{
id: "server-state-source-duplication",
passed: ruleIds.has("CLIENT_STORE_DUPLICATES_SERVER_STATE"),
},
{
id: "production-imports-recipe",
passed: ruleIds.has("PRODUCTION_IMPORTS_RECIPE"),
},
{
id: "reference-runtime-not-composed",
passed: ruleIds.has("REFERENCE_RUNTIME_COMPOSED_WITHOUT_SELECTION"),
},
{
id: "reference-runtime-not-bundled",
passed: bundleViolations.some((file) => file.endsWith("runtime.js")),
},
{
id: "reference-runtime-module-not-bundled",
passed: inventoryViolations.some((violation) =>
violation.includes(
"src/adapters/browser-files/browser-file-vault.ts",
),
),
},
{
id: "reference-runtime-bundle-over-budget",
passed:
bundleBudgetFixtures.length === referenceRuntimeRecipes.length &&
bundleBudgetFixtures.length > 0 &&
bundleBudgetFixtures.every(
(fixture) =>
fixture.gzipBytes > fixture.fixtureBudgetGzipBytes &&
fixture.rejected,
),
},
];
const report = {
schemaVersion: 1,
results,
bundleBudgetFixtures,
passed: results.every(({ passed }) => passed),
};
await mkdir("artifacts/quality", { recursive: true });
await writeFile(
"artifacts/quality/optional-recipe-fixtures.json",
`${JSON.stringify(report, null, 2)}\n`,
);
if (!report.passed) {
process.stderr.write(
`Optional recipe negative fixtures failed: ${results
.filter(({ passed }) => !passed)
.map(({ id }) => id)
.join(", ")}\n`,
);
process.exit(1);
}
process.stdout.write(
`Optional recipe negative fixtures: PASS (${results.length} forbidden cases rejected)\n`,
);
+62
View File
@@ -0,0 +1,62 @@
import { spawnSync } from "node:child_process";
import { readdir } from "node:fs/promises";
import { extname, resolve } from "node:path";
const recipeRoot = resolve("recipes");
const pnpmCli = requireEnvironment("npm_execpath");
if (!(await containsTypeScriptSource(recipeRoot))) {
process.stdout.write(
"Optional recipe typecheck: SKIP (no recipe TypeScript sources installed)\n",
);
} else {
const result = spawnSync(
process.execPath,
[pnpmCli, "exec", "tsc", "--project", "tsconfig.recipes.json"],
{ stdio: "inherit" },
);
if (result.error) {
throw result.error;
}
process.exitCode = result.status ?? 1;
}
function requireEnvironment(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`${name} is required to typecheck optional recipes`);
}
return value;
}
async function containsTypeScriptSource(directory: string): Promise<boolean> {
let entries;
try {
entries = await readdir(directory, { withFileTypes: true });
} catch (error) {
if (hasErrorCode(error, "ENOENT")) {
return false;
}
throw error;
}
for (const entry of entries) {
const target = resolve(directory, entry.name);
if (entry.isDirectory() && (await containsTypeScriptSource(target))) {
return true;
}
if (entry.isFile() && [".ts", ".tsx", ".mts", ".cts"].includes(extname(entry.name))) {
return true;
}
}
return false;
}
function hasErrorCode(error: unknown, code: string): boolean {
return (
typeof error === "object" &&
error !== null &&
"code" in error &&
error.code === code
);
}
-74
View File
@@ -1,74 +0,0 @@
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
import {
scanOptionalRecipeSources,
scanProductionBundle,
validateRecipeCatalog,
} from "./lib/optional-recipes.mjs";
/** @param {string} name @param {string} fallback */
const argument = (name, fallback) => {
const index = process.argv.indexOf(name);
return index === -1 ? fallback : process.argv[index + 1];
};
const catalogPath = argument(
"--catalog",
"config/recipes/frontend-capability-recipes.json",
);
const sourceRoot = argument("--source-root", "src");
const distRoot = argument("--dist-root", "dist");
const artifactPath = argument(
"--artifact",
"artifacts/quality/optional-recipes.json",
);
const requireDist = process.argv.includes("--require-dist");
const catalog = JSON.parse(await readFile(catalogPath, "utf8"));
const packageDocument = JSON.parse(await readFile("package.json", "utf8"));
const catalogViolations = validateRecipeCatalog(catalog, packageDocument);
const sourceViolations = await scanOptionalRecipeSources(sourceRoot);
const bundlePresent = await stat(`${distRoot}/.vite/manifest.json`)
.then(() => true)
.catch(() => false);
const bundleViolations = await scanProductionBundle(distRoot);
const violations = [
...catalogViolations.map((ruleId) => ({ ruleId, path: catalogPath })),
...sourceViolations,
...bundleViolations.map((path) => ({
ruleId: "UNSELECTED_RECIPE_IN_PRODUCTION_BUNDLE",
path,
})),
...(requireDist && !bundlePresent
? [{ ruleId: "PRODUCTION_BUNDLE_MISSING", path: distRoot }]
: []),
];
const report = {
schemaVersion: 1,
decisionId: "VD-10",
selectedCapabilities: [],
recipeCount: Array.isArray(catalog.recipes) ? catalog.recipes.length : 0,
productionRuntimeDependencies:
catalog.productionRuntimeDependencies ?? null,
bundleStatus: bundlePresent
? bundleViolations.length === 0
? "PASS"
: "FAIL"
: "NOT_BUILT",
violations,
passed: violations.length === 0,
};
await mkdir("artifacts/quality", { recursive: true });
await writeFile(artifactPath, `${JSON.stringify(report, null, 2)}\n`);
if (violations.length > 0) {
process.stderr.write(
`Optional recipe contract failed:\n${violations
.map((violation) => `${violation.ruleId}: ${violation.path}`)
.join("\n")}\n`,
);
process.exit(1);
}
process.stdout.write(
`Optional recipes: PASS (${report.recipeCount} recipe-only capabilities, bundle=${report.bundleStatus})\n`,
);
+246
View File
@@ -0,0 +1,246 @@
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
import {
measureOptionalRecipeBundle,
type OptionalRecipeBundleMeasurement,
} from "./lib/optional-recipe-bundle.ts";
import {
scanOptionalRecipeSources,
scanProductionBundle,
validateRecipeCatalog,
} from "./lib/optional-recipes.ts";
type GateViolation = Readonly<{
ruleId: string;
path: string;
detail?: string;
}>;
type ReferenceRuntimeBundleInput = Readonly<{
recipeId: string;
sourceRoots: readonly string[];
bundleBudgetGzipBytes: number;
}>;
const argument = (name: string, fallback: string): string => {
const index = process.argv.indexOf(name);
return index === -1 ? fallback : (process.argv[index + 1] ?? fallback);
};
const catalogPath = argument(
"--catalog",
"config/recipes/frontend-capability-recipes.json",
);
const sourceRoot = argument("--source-root", "src");
const distRoot = argument("--dist-root", "dist");
const artifactPath = argument(
"--artifact",
"artifacts/quality/optional-recipes.json",
);
const requireDist = process.argv.includes("--require-dist");
const catalog = JSON.parse(
await readFile(catalogPath, "utf8"),
) as Record<string, unknown>;
const recipes = Array.isArray(catalog.recipes) ? catalog.recipes : [];
const packageDocument = JSON.parse(
await readFile("package.json", "utf8"),
) as Record<string, unknown>;
const catalogViolations = validateRecipeCatalog(catalog, packageDocument);
const runtimeSourceViolations = (
await Promise.all(
recipes.flatMap((recipe: unknown) => {
if (!recipe || typeof recipe !== "object") return [];
const row = recipe as Record<string, unknown>;
const runtime = row.referenceRuntime;
if (!runtime || typeof runtime !== "object") return [];
const sourceRoots = (runtime as Record<string, unknown>).sourceRoots;
if (!Array.isArray(sourceRoots)) return [];
return sourceRoots
.filter(
(sourceRoot): sourceRoot is string =>
typeof sourceRoot === "string",
)
.map(async (sourceRoot) => ({
sourceRoot,
exists: await stat(sourceRoot)
.then(() => true)
.catch(() => false),
}));
}),
)
).filter(({ exists }) => !exists);
const {
inputs: referenceRuntimeBundleInputs,
violations: referenceRuntimeBundleConfigurationViolations,
} = referenceRuntimeBundleInputsFrom(recipes);
const referenceRuntimeBundles: OptionalRecipeBundleMeasurement[] = [];
const referenceRuntimeBundleMeasurementViolations: GateViolation[] = [];
for (const bundleInput of referenceRuntimeBundleInputs) {
try {
const measurement = await measureOptionalRecipeBundle(bundleInput);
referenceRuntimeBundles.push(measurement);
if (!measurement.passed) {
referenceRuntimeBundleMeasurementViolations.push({
ruleId: "REFERENCE_RUNTIME_BUNDLE_BUDGET_EXCEEDED",
path: measurement.recipeId,
detail:
`${measurement.gzipBytes} > ` +
`${measurement.bundleBudgetGzipBytes} gzip bytes`,
});
}
} catch (error) {
referenceRuntimeBundleMeasurementViolations.push({
ruleId: "REFERENCE_RUNTIME_BUNDLE_MEASUREMENT_FAILED",
path: bundleInput.recipeId,
detail: safeErrorSummary(error),
});
}
}
referenceRuntimeBundles.sort((left, right) =>
compareText(left.recipeId, right.recipeId),
);
const sourceViolations = await scanOptionalRecipeSources(sourceRoot);
const bundlePresent = await stat(`${distRoot}/.vite/manifest.json`)
.then(() => true)
.catch(() => false);
const bundleViolations = await scanProductionBundle(distRoot);
const violations: GateViolation[] = [
...catalogViolations.map((ruleId) => ({ ruleId, path: catalogPath })),
...runtimeSourceViolations.map(({ sourceRoot }) => ({
ruleId: "REFERENCE_RUNTIME_SOURCE_MISSING",
path: sourceRoot,
})),
...sourceViolations,
...bundleViolations.map((path) => ({
ruleId: "UNSELECTED_RECIPE_IN_PRODUCTION_BUNDLE",
path,
})),
...(requireDist && !bundlePresent
? [{ ruleId: "PRODUCTION_BUNDLE_MISSING", path: distRoot }]
: []),
...referenceRuntimeBundleConfigurationViolations,
...referenceRuntimeBundleMeasurementViolations,
];
const report = {
schemaVersion: 1,
decisionId: "VD-10",
selectedCapabilities: [],
referenceRuntimes: recipes
.filter(
(recipe: unknown): recipe is Record<string, unknown> =>
Boolean(
recipe &&
typeof recipe === "object" &&
(recipe as Record<string, unknown>).referenceRuntime,
),
)
.map((recipe: Record<string, unknown>) => ({
id: recipe.id,
referenceRuntime: recipe.referenceRuntime,
})),
recipeCount: recipes.length,
productionRuntimeDependencies:
catalog.productionRuntimeDependencies ?? null,
referenceRuntimeBundleBudgets: referenceRuntimeBundles,
bundleStatus: bundlePresent
? bundleViolations.length === 0
? "PASS"
: "FAIL"
: "NOT_BUILT",
violations,
passed: violations.length === 0,
};
await mkdir("artifacts/quality", { recursive: true });
await writeFile(artifactPath, `${JSON.stringify(report, null, 2)}\n`);
if (violations.length > 0) {
process.stderr.write(
`Optional recipe contract failed:\n${violations
.map(
(violation) =>
`${violation.ruleId}: ${violation.path}` +
(violation.detail ? ` (${violation.detail})` : ""),
)
.join("\n")}\n`,
);
process.exit(1);
}
const referenceRuntimeBudgetStatus =
referenceRuntimeBundles.length > 0
? `, budgets=${referenceRuntimeBundles
.map(
(measurement) =>
`${measurement.recipeId}:${measurement.gzipBytes}/` +
measurement.bundleBudgetGzipBytes,
)
.join(",")} gzip bytes`
: "";
process.stdout.write(
`Optional recipes: PASS (${report.recipeCount} optional capabilities, ${report.referenceRuntimes.length} uncomposed reference runtimes, bundle=${report.bundleStatus}${referenceRuntimeBudgetStatus})\n`,
);
function referenceRuntimeBundleInputsFrom(
recipeRows: readonly unknown[],
): Readonly<{
inputs: readonly ReferenceRuntimeBundleInput[];
violations: readonly GateViolation[];
}> {
const inputs: ReferenceRuntimeBundleInput[] = [];
const violations: GateViolation[] = [];
for (const recipe of recipeRows) {
if (!recipe || typeof recipe !== "object") continue;
const row = recipe as Record<string, unknown>;
if (row.referenceRuntime === undefined) continue;
const recipeId =
typeof row.id === "string" ? row.id : "unknown-reference-runtime";
const runtime = row.referenceRuntime;
const sourceRoots =
runtime && typeof runtime === "object"
? (runtime as Record<string, unknown>).sourceRoots
: null;
if (
!Array.isArray(sourceRoots) ||
!sourceRoots.every(
(sourceRoot): sourceRoot is string =>
typeof sourceRoot === "string",
) ||
!Number.isSafeInteger(row.bundleBudgetGzipBytes) ||
(row.bundleBudgetGzipBytes as number) < 1
) {
violations.push({
ruleId: "REFERENCE_RUNTIME_BUNDLE_CONFIGURATION_INVALID",
path: recipeId,
});
continue;
}
inputs.push(
Object.freeze({
recipeId,
sourceRoots: Object.freeze([...sourceRoots]),
bundleBudgetGzipBytes: row.bundleBudgetGzipBytes as number,
}),
);
}
return Object.freeze({
inputs: Object.freeze(
inputs.sort((left, right) =>
compareText(left.recipeId, right.recipeId),
),
),
violations: Object.freeze(violations),
});
}
function safeErrorSummary(error: unknown): string {
const message =
error instanceof Error ? error.message : "unknown measurement failure";
return message
.split(/\r?\n/, 1)[0]
?.replaceAll(process.cwd(), ".")
.slice(0, 512) ?? "unknown measurement failure";
}
function compareText(left: string, right: string): number {
return left < right ? -1 : left > right ? 1 : 0;
}
+40
View File
@@ -0,0 +1,40 @@
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { scanRealtimeBoundaries } from "./lib/realtime-boundaries.ts";
const sourceRoot = argument("--source-root") ?? "src";
const artifact =
argument("--artifact") ??
"artifacts/quality/realtime-boundaries.json";
const violations = await scanRealtimeBoundaries(sourceRoot);
const report = Object.freeze({
schemaVersion: 1,
sourceRoot,
violations,
passed: violations.length === 0,
});
await mkdir(path.dirname(artifact), { recursive: true });
await writeFile(
artifact,
`${JSON.stringify(report, null, 2)}\n`,
);
if (violations.length > 0) {
for (const violation of violations) {
process.stderr.write(
`${violation.file}:${violation.line} ${violation.ruleId}\n`,
);
}
process.exit(1);
}
process.stdout.write(
`Realtime boundaries: PASS (${sourceRoot})\n`,
);
function argument(name: string): string | undefined {
const index = process.argv.indexOf(name);
return index >= 0 ? process.argv[index + 1] : undefined;
}
@@ -0,0 +1,47 @@
import { spawnSync } from "node:child_process";
const checker = "scripts/check-realtime-boundaries.ts";
const allowed = run(
"tests/fixtures/realtime-boundaries/allowed",
".tmp/realtime-boundaries-allowed.json",
);
const forbidden = run(
"tests/fixtures/realtime-boundaries/forbidden",
".tmp/realtime-boundaries-forbidden.json",
);
const forbiddenOutput = `${forbidden.stdout}${forbidden.stderr}`;
const expectedRules = [
"NATIVE_REALTIME_API_OUTSIDE_ADAPTER",
"PRESENTATION_INTERVAL_OWNER",
"UNSELECTED_REALTIME_RUNTIME_COMPOSED",
] as const;
if (
allowed.status !== 0 ||
forbidden.status === 0 ||
!expectedRules.every((rule) => forbiddenOutput.includes(rule))
) {
process.stderr.write(allowed.stdout);
process.stderr.write(allowed.stderr);
process.stderr.write(forbidden.stdout);
process.stderr.write(forbidden.stderr);
process.exit(1);
}
process.stdout.write(
`Realtime boundary fixtures: PASS (${expectedRules.length} forbidden rules rejected)\n`,
);
function run(sourceRoot: string, artifact: string) {
return spawnSync(
process.execPath,
[
checker,
"--source-root",
sourceRoot,
"--artifact",
artifact,
],
{ encoding: "utf8" },
);
}
@@ -14,10 +14,48 @@ import {
registrySnapshotDigest,
validateBreakingEvidence,
verifyRegistryBaselineApproval,
} from "./lib/registry-compatibility.mjs";
} from "./lib/registry-compatibility.ts";
/** @param {string} name @param {string | undefined} fallback */
function argumentValue(name, fallback) {
type RegistryRow = Record<string, unknown>;
type RegistryRows = Record<string, RegistryRow>;
type RegistryReference = Readonly<{
registryId: string;
field: string;
targetField: string;
}>;
type RegistryConsumer = Readonly<{ path: string; token: string }>;
type RegistrySpecification = Readonly<{
registryId: string;
owner: string;
path: string;
exportName: string;
declaredRows?: unknown;
requiredFields: readonly string[];
fieldTypes?: Readonly<Record<string, string>>;
keyField?: string;
uniqueFields?: readonly string[];
allowedValues?: Readonly<Record<string, readonly unknown[]>>;
references?: readonly RegistryReference[];
breakingFields?: readonly string[];
consumers?: readonly RegistryConsumer[];
consumerIdentityField?: string;
consumerDirectories?: readonly string[];
orphanExemptRows?: readonly string[];
}>;
type RegistryGovernance = Readonly<{
registries: readonly RegistrySpecification[];
sourceDirectories?: readonly string[];
}>;
type RegistrySnapshot = Record<string, unknown>;
type CompatibilitySummary = Readonly<{
impact: string;
changes: readonly unknown[];
}>;
function argumentValue(
name: string,
fallback: string | undefined,
): string | undefined {
const index = process.argv.indexOf(name);
return index >= 0 && process.argv[index + 1]
? process.argv[index + 1]
@@ -26,13 +64,11 @@ function argumentValue(name, fallback) {
const defaultGovernancePath = "config/contracts/registry-governance.json";
const governancePath =
/** @type {string} */ (
argumentValue("--governance", defaultGovernancePath)
);
argumentValue("--governance", defaultGovernancePath) ??
defaultGovernancePath;
const artifactPath =
/** @type {string} */ (
argumentValue("--artifact", "artifacts/quality/registries.json")
);
argumentValue("--artifact", "artifacts/quality/registries.json") ??
"artifacts/quality/registries.json";
const usesRepositoryBaseline =
governancePath === defaultGovernancePath &&
!process.argv.includes("--no-baseline");
@@ -54,28 +90,31 @@ const evidencePath = argumentValue(
? "config/contracts/registry-change-evidence.json"
: undefined,
);
const governance = JSON.parse(await readFile(governancePath, "utf8"));
const failures = [];
const owners = new Map();
const snapshots = [];
const rowsByRegistry = new Map();
const sourcesByRegistry = new Map();
const registryExtensions = [".js", ".jsx", ".mjs", ".ts", ".tsx", ".mts"];
const governance = JSON.parse(
await readFile(governancePath, "utf8"),
) as RegistryGovernance;
const failures: string[] = [];
const owners = new Map<string, string>();
const snapshots: RegistrySnapshot[] = [];
const rowsByRegistry = new Map<string, RegistryRows>();
const sourcesByRegistry = new Map<string, string>();
const registryExtensions = [".ts", ".tsx", ".mts", ".cts"];
/** @param {string} declaredPath */
async function resolveRegistrySource(declaredPath) {
async function resolveRegistrySource(
declaredPath: string,
): Promise<string | null> {
const extension = path.extname(declaredPath);
const basePath = extension
? declaredPath.slice(0, -extension.length)
: declaredPath;
const candidates = [];
const candidates: string[] = [];
for (const candidateExtension of registryExtensions) {
const candidate = `${basePath}${candidateExtension}`;
try {
await access(candidate);
candidates.push(candidate);
} catch {
// A TypeScript migration may replace the declared extension.
// Continue through the supported TypeScript source extensions.
}
}
if (candidates.length > 1) {
@@ -87,16 +126,14 @@ async function resolveRegistrySource(declaredPath) {
return candidates[0] ?? null;
}
/** @param {unknown} value */
function runtimeType(value) {
function runtimeType(value: unknown): string {
if (value === null) return "null";
if (Array.isArray(value)) return "array";
if (Number.isInteger(value)) return "integer";
return typeof value;
}
/** @param {unknown} value @param {string} declaration */
function matchesDeclaredType(value, declaration) {
function matchesDeclaredType(value: unknown, declaration: string): boolean {
const actual = runtimeType(value);
return declaration
.split("|")
@@ -107,8 +144,7 @@ function matchesDeclaredType(value, declaration) {
);
}
/** @param {string} directory @returns {Promise<string[]>} */
async function filesBelow(directory) {
async function filesBelow(directory: string): Promise<string[]> {
try {
const entries = await readdir(directory, { withFileTypes: true });
const groups = await Promise.all(
@@ -118,7 +154,7 @@ async function filesBelow(directory) {
}),
);
return groups.flat().filter((file) =>
/\.(?:js|jsx|mjs|ts|tsx|mts)$/.test(file),
/\.(?:ts|tsx|mts|cts)$/.test(file),
);
} catch {
return [];
@@ -135,10 +171,10 @@ for (const specification of governance.registries) {
const sourcePath = await resolveRegistrySource(specification.path);
try {
if (!sourcePath) throw new Error("missing registry source");
const module = await import(
const registryModule = (await import(
`${pathToFileURL(path.resolve(sourcePath)).href}?registry-check=${Date.now()}`
);
rows = module[specification.exportName];
)) as Record<string, unknown>;
rows = registryModule[specification.exportName];
} catch {
if (!rows) failures.push(`missing registry source ${specification.path}`);
}
@@ -148,13 +184,14 @@ for (const specification of governance.registries) {
continue;
}
rowsByRegistry.set(specification.registryId, rows);
const registryRows = rows as RegistryRows;
rowsByRegistry.set(specification.registryId, registryRows);
sourcesByRegistry.set(
specification.registryId,
sourcePath ?? specification.path,
);
for (const [rowName, row] of Object.entries(rows)) {
for (const [rowName, row] of Object.entries(registryRows)) {
if (!row || typeof row !== "object" || Array.isArray(row)) {
failures.push(`${specification.registryId}.${rowName} is not an object`);
continue;
@@ -187,8 +224,8 @@ for (const specification of governance.registries) {
}
for (const field of specification.uniqueFields ?? []) {
const values = new Map();
for (const [rowName, row] of Object.entries(rows)) {
const values = new Map<string, string>();
for (const [rowName, row] of Object.entries(registryRows)) {
if (!row || typeof row !== "object" || Array.isArray(row)) continue;
const value = row[field];
if (value === undefined) continue;
@@ -206,13 +243,10 @@ for (const specification of governance.registries) {
for (const [field, allowed] of Object.entries(
specification.allowedValues ?? {},
)) {
for (const [rowName, row] of Object.entries(rows)) {
for (const [rowName, row] of Object.entries(registryRows)) {
if (!row || typeof row !== "object" || Array.isArray(row)) continue;
if (
!allowed.some(
/** @param {unknown} value */
(value) => Object.is(value, row[field]),
)
!allowed.some((value) => Object.is(value, row[field]))
) {
failures.push(
`${specification.registryId}.${rowName}.${field} has unknown value ${String(row[field])}`,
@@ -234,9 +268,9 @@ for (const specification of governance.registries) {
registryId: specification.registryId,
owner: specification.owner,
source: sourcePath ?? specification.path,
rowCount: Object.keys(rows).length,
rowCount: Object.keys(registryRows).length,
contract,
rows: canonicalizeRegistryValue(rows),
rows: canonicalizeRegistryValue(registryRows),
});
}
@@ -351,25 +385,25 @@ for (const sourceDirectory of sourceFiles) {
}
}
const currentSnapshot =
/** @type {Readonly<Record<string, unknown>>} */ (
canonicalizeRegistryValue({
schemaVersion: 2,
registries: snapshots,
})
);
let baselineDigest = null;
let currentDigest = registrySnapshotDigest(currentSnapshot);
let compatibility =
/** @type {{impact: string, changes: readonly Record<string, unknown>[]}} */ ({
impact: "not-evaluated",
changes: [],
});
const currentSnapshot = canonicalizeRegistryValue({
schemaVersion: 2,
registries: snapshots,
}) as Readonly<Record<string, unknown>>;
let baselineDigest: string | null = null;
const currentDigest = registrySnapshotDigest(currentSnapshot);
let compatibility: CompatibilitySummary = {
impact: "not-evaluated",
changes: [],
};
if (baselinePath && approvalPath && evidencePath) {
try {
const baseline = JSON.parse(await readFile(baselinePath, "utf8"));
const approval = JSON.parse(await readFile(approvalPath, "utf8"));
const baseline = JSON.parse(
await readFile(baselinePath, "utf8"),
) as Record<string, unknown>;
const approval = JSON.parse(
await readFile(approvalPath, "utf8"),
) as Record<string, unknown>;
const approvalResult = verifyRegistryBaselineApproval(baseline, approval);
baselineDigest = approvalResult.actualDigest;
if (!approvalResult.passed) {
@@ -377,9 +411,12 @@ if (baselinePath && approvalPath && evidencePath) {
`registry baseline approval digest mismatch: approved=${approvalResult.approvedDigest} actual=${approvalResult.actualDigest}`,
);
}
compatibility = diffRegistrySnapshots(baseline, currentSnapshot);
const evidence = JSON.parse(await readFile(evidencePath, "utf8"));
const evidenceResult = validateBreakingEvidence(compatibility, evidence);
const registryDiff = diffRegistrySnapshots(baseline, currentSnapshot);
compatibility = registryDiff;
const evidence = JSON.parse(
await readFile(evidencePath, "utf8"),
) as Record<string, unknown>;
const evidenceResult = validateBreakingEvidence(registryDiff, evidence);
failures.push(...evidenceResult.failures);
} catch (error) {
failures.push(
@@ -4,7 +4,7 @@ import {
diffRegistrySnapshots,
validateBreakingEvidence,
verifyRegistryBaselineApproval,
} from "./lib/registry-compatibility.mjs";
} from "./lib/registry-compatibility.ts";
const fixtures = JSON.parse(
await readFile(
@@ -1,8 +1,24 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
/** @param {string} name @param {string} fallback */
function argumentValue(name, fallback) {
type CoverageMetrics = Record<string, { pct: number }>;
type CoveragePolicy = Readonly<{
summary: Record<string, number>;
criticalModules: readonly Readonly<{
path: string;
minimum: Record<string, number>;
}>[];
}>;
type CoverageSummary = Record<string, CoverageMetrics>;
type CoverageResult = Readonly<{
scope: string;
metric: string;
threshold: number;
received: number | undefined;
passed: boolean;
}>;
function argumentValue(name: string, fallback: string): string {
const index = process.argv.indexOf(name);
return index >= 0 && process.argv[index + 1]
? process.argv[index + 1]
@@ -21,24 +37,20 @@ const artifactPath = argumentValue(
"--artifact",
"artifacts/quality/risk-coverage.json",
);
const policy = JSON.parse(await readFile(policyPath, "utf8"));
const summary = JSON.parse(await readFile(summaryPath, "utf8"));
const failures = [];
/** @type {Array<{
* scope: string,
* metric: string,
* threshold: number,
* received: number | undefined,
* passed: boolean
* }>} */
const results = [];
const policy = JSON.parse(
await readFile(policyPath, "utf8"),
) as CoveragePolicy;
const summary = JSON.parse(
await readFile(summaryPath, "utf8"),
) as CoverageSummary;
const failures: string[] = [];
const results: CoverageResult[] = [];
/**
* @param {string} scope
* @param {Record<string, {pct: number}>} actual
* @param {Record<string, number>} minimum
*/
function evaluate(scope, actual, minimum) {
function evaluate(
scope: string,
actual: CoverageMetrics,
minimum: Record<string, number>,
): void {
for (const [metric, threshold] of Object.entries(minimum)) {
const received = actual?.[metric]?.pct;
const passed =
@@ -8,7 +8,7 @@ import {
validateLicensePolicy,
validateVulnerabilityReport,
verifySupplyChainCoherence,
} from "./lib/supply-chain.mjs";
} from "./lib/supply-chain.ts";
const integrity = `sha512-${Buffer.alloc(64, 1).toString("base64")}`;
const baseDependency = {
@@ -2,17 +2,26 @@ import { spawnSync } from "node:child_process";
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import path from "node:path";
type Document = Record<string, unknown>;
function isRecord(value: unknown): value is Document {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
async function readDocument(file: string): Promise<Document> {
const parsed: unknown = JSON.parse(await readFile(file, "utf8"));
if (!isRecord(parsed)) throw new Error(`${file} must be a JSON object`);
return parsed;
}
const fixtureDirectory = path.resolve(".tmp/supply-chain-provider-fixture");
await rm(fixtureDirectory, { recursive: true, force: true });
await mkdir(fixtureDirectory, { recursive: true });
const inventory = JSON.parse(
await readFile("artifacts/release/dependency-inventory.json", "utf8"),
const inventory = await readDocument(
"artifacts/release/dependency-inventory.json",
);
const verification = JSON.parse(
await readFile(
"artifacts/security/supply-chain-verification.json",
"utf8",
),
const verification = await readDocument(
"artifacts/security/supply-chain-verification.json",
);
const vulnerabilityPath = path.join(
fixtureDirectory,
@@ -51,7 +60,7 @@ await writeFile(
);
const providerRun = spawnSync(
"node",
["scripts/generate-supply-chain.mjs"],
["scripts/generate-supply-chain.ts"],
{
env: {
...process.env,
@@ -63,16 +72,17 @@ const providerRun = spawnSync(
);
let promotionStatus = "MISSING";
if (providerRun.status === 0) {
promotionStatus = JSON.parse(
await readFile(
"artifacts/security/supply-chain-verification.json",
"utf8",
),
).promotionStatus;
const providerVerification = await readDocument(
"artifacts/security/supply-chain-verification.json",
);
promotionStatus =
typeof providerVerification.promotionStatus === "string"
? providerVerification.promotionStatus
: "MISSING";
}
const restore = spawnSync(
"node",
["scripts/generate-supply-chain.mjs"],
["scripts/generate-supply-chain.ts"],
{ encoding: "utf8" },
);
await rm(fixtureDirectory, { recursive: true, force: true });
@@ -95,8 +105,14 @@ await writeFile(
)}\n`,
);
if (!passed) {
const detail =
providerRun.stderr ||
restore.stderr ||
providerRun.stdout ||
restore.stdout ||
`providerStatus=${String(providerRun.status)}, promotionStatus=${promotionStatus}, restoreStatus=${String(restore.status)}`;
process.stderr.write(
`Supply-chain provider fixture failed: ${providerRun.stderr || restore.stderr}\n`,
`Supply-chain provider fixture failed: ${detail}\n`,
);
process.exit(1);
}
-167
View File
@@ -1,167 +0,0 @@
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
import path from "node:path";
/** @param {string} name @param {string} fallback */
function argumentValue(name, fallback) {
const index = process.argv.indexOf(name);
return index >= 0 && process.argv[index + 1]
? process.argv[index + 1]
: fallback;
}
const sourceRoot = argumentValue("--source-root", "tests");
const artifactPath = argumentValue(
"--artifact",
"artifacts/quality/test-evidence.json",
);
const fixtureMode = sourceRoot !== "tests";
const failures = [];
const facts = {
scannedFiles: 0,
visualBaselines: 0,
sharedScenarios: 0,
};
/** @param {string} target @returns {Promise<string[]>} */
async function filesBelow(target) {
try {
const metadata = await stat(target);
if (metadata.isFile()) return [target];
const entries = await readdir(target, { withFileTypes: true });
const groups = await Promise.all(
entries.map((entry) => filesBelow(path.join(target, entry.name))),
);
return groups.flat();
} catch {
return [];
}
}
const sourceFiles = (await filesBelow(sourceRoot)).filter(
(file) => fixtureMode || !file.split(path.sep).includes("fixtures"),
);
for (const file of sourceFiles) {
if (!/\.(?:js|jsx|mjs|ts|tsx|fixture|txt)$/.test(file)) continue;
const source = await readFile(file, "utf8");
facts.scannedFiles += 1;
const skipPattern =
/\b(?:test|it|describe)(?:\.describe)?\.(?:skip|fixme)\s*\(/g;
if (skipPattern.test(source)) {
const quarantine =
/quarantine\(owner=[^)]+,\s*defect=[^)]+,\s*expires=\d{4}-\d{2}-\d{2}\)/;
if (!quarantine.test(source)) {
failures.push(`${file}: skip/fixme lacks owned expiring quarantine`);
}
}
const wholeUiMask =
/\bmask\s*:\s*\[[\s\S]{0,240}(?:locator|getByRole)\s*\(\s*["'](?:html|body|main|application|document)["']/i;
if (wholeUiMask.test(source)) {
failures.push(`${file}: screenshot mask may not cover the whole UI`);
}
}
if (!fixtureMode) {
const e2eConfig = await readFile("playwright.config.js", "utf8");
for (const token of [
"pnpm build",
"pnpm preview",
"reuseExistingServer: false",
'"junit"',
'trace: "retain-on-failure"',
'"chromium-compact"',
'"firefox"',
'"webkit"',
]) {
if (!e2eConfig.includes(token)) {
failures.push(`playwright.config.js missing release evidence token ${token}`);
}
}
const e2eFiles = (await filesBelow("tests/e2e")).filter((file) =>
/\.spec\.(?:js|ts)$/.test(file),
);
for (const file of e2eFiles) {
const source = await readFile(file, "utf8");
if (!source.includes("support/browser/strict-browser-test")) {
failures.push(`${file}: bypasses strict browser fixture`);
}
}
const scenarioCatalog = await readFile(
"tests/mocks/scenarios/catalog.ts",
"utf8",
);
const scenarioIdBlock =
scenarioCatalog.match(
/HTTP_SCENARIO_IDS\s*=\s*Object\.freeze\(\[([\s\S]*?)\]\s*as const\)/,
)?.[1] ?? "";
facts.sharedScenarios = (scenarioIdBlock.match(/"[^"]+"/g) ?? []).length;
if (facts.sharedScenarios < 19) {
failures.push("shared MSW catalog must retain all 19 failure scenarios");
}
const handler = await readFile(
"tests/mocks/handlers/reference-resources.ts",
"utf8",
);
if (
!handler.includes("assertOperationScenario") ||
!handler.includes("../scenarios/catalog.js")
) {
failures.push("MSW handler bypasses shared scenario catalog");
}
const baselineFiles = (await filesBelow("tests/visual/__snapshots__")).filter(
(file) => file.endsWith(".png"),
);
facts.visualBaselines = baselineFiles.length;
if (facts.visualBaselines < 4) {
failures.push("visual baseline requires at least four risk surfaces");
}
for (const required of [
"playwright.storybook.config.js",
"playwright.visual.config.js",
"tests/storybook/workshop.spec.ts",
"artifacts/tests/storybook/results.xml",
"artifacts/tests/visual/results.xml",
]) {
if ((await filesBelow(required)).length === 0) {
failures.push(`test evidence missing ${required}`);
}
}
const requiredBuiltFiles = [
"dist/index.html",
"dist/config.json",
"dist/release-manifest.json",
"dist/runtime-config.schema.json",
"dist/.vite/manifest.json",
];
for (const required of requiredBuiltFiles) {
if ((await filesBelow(required)).length === 0) {
failures.push(`built-dist contract missing ${required}`);
}
}
const sourceMaps = (await filesBelow("dist")).filter((file) =>
file.endsWith(".map"),
);
if (sourceMaps.length > 0) {
failures.push(`production dist contains source maps: ${sourceMaps.join(", ")}`);
}
}
const report = {
schemaVersion: 1,
sourceRoot,
status: failures.length === 0 ? "PASS" : "FAIL",
facts,
failures,
};
await mkdir(path.dirname(artifactPath), { recursive: true });
await writeFile(artifactPath, `${JSON.stringify(report, null, 2)}\n`);
if (failures.length > 0) {
process.stderr.write(`Test evidence failed:\n- ${failures.join("\n- ")}\n`);
process.exit(1);
}
process.stdout.write(
`Test evidence: PASS (${facts.scannedFiles} files, ${facts.visualBaselines} baselines, ${facts.sharedScenarios} scenarios)\n`,
);
+262
View File
@@ -0,0 +1,262 @@
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
import path from "node:path";
type ScenarioCatalogContribution = Readonly<{
owner: string;
path: string;
arrayExport: string;
minimumEntries: number;
}>;
type SourceContractContribution = Readonly<{
owner: string;
path: string;
requiredTokens: readonly string[];
}>;
type TestEvidencePolicy = Readonly<{
schemaVersion: number;
scenarioCatalogs: readonly unknown[];
sourceContracts: readonly unknown[];
}>;
function argumentValue(name: string, fallback: string): string {
const index = process.argv.indexOf(name);
return index >= 0 && process.argv[index + 1]
? process.argv[index + 1]
: fallback;
}
const sourceRoot = argumentValue("--source-root", "tests");
const artifactPath = argumentValue(
"--artifact",
"artifacts/quality/test-evidence.json",
);
const policyPath = argumentValue(
"--policy",
"config/testing/test-evidence.json",
);
const fixtureMode = sourceRoot !== "tests";
const sourceOnly = process.argv.includes("--source-only");
const failures: string[] = [];
const facts = {
scannedFiles: 0,
visualBaselines: 0,
sharedScenarios: 0,
};
async function filesBelow(target: string): Promise<string[]> {
try {
const metadata = await stat(target);
if (metadata.isFile()) return [target];
const entries = await readdir(target, { withFileTypes: true });
const groups = await Promise.all(
entries.map((entry) => filesBelow(path.join(target, entry.name))),
);
return groups.flat();
} catch {
return [];
}
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
const sourceFiles = (await filesBelow(sourceRoot)).filter(
(file) => fixtureMode || !file.split(path.sep).includes("fixtures"),
);
for (const file of sourceFiles) {
if (!/\.(?:ts|tsx|fixture|txt)$/.test(file)) continue;
const source = await readFile(file, "utf8");
facts.scannedFiles += 1;
const skipPattern =
/\b(?:test|it|describe)(?:\.describe)?\.(?:skip|fixme)\s*\(/g;
if (skipPattern.test(source)) {
const quarantine =
/quarantine\(owner=[^)]+,\s*defect=[^)]+,\s*expires=\d{4}-\d{2}-\d{2}\)/;
if (!quarantine.test(source)) {
failures.push(`${file}: skip/fixme lacks owned expiring quarantine`);
}
}
const wholeUiMask =
/\bmask\s*:\s*\[[\s\S]{0,240}(?:locator|getByRole)\s*\(\s*["'](?:html|body|main|application|document)["']/i;
if (wholeUiMask.test(source)) {
failures.push(`${file}: screenshot mask may not cover the whole UI`);
}
}
if (!fixtureMode) {
const e2eConfig = await readFile("playwright.config.ts", "utf8");
for (const token of [
"pnpm build",
"pnpm preview",
"reuseExistingServer: false",
'"junit"',
'trace: "retain-on-failure"',
'"chromium-compact"',
'"firefox"',
'"webkit"',
]) {
if (!e2eConfig.includes(token)) {
failures.push(`playwright.config.ts missing release evidence token ${token}`);
}
}
const e2eFiles = (await filesBelow("tests/e2e")).filter((file) =>
/\.spec\.ts$/.test(file),
);
for (const file of e2eFiles) {
const source = await readFile(file, "utf8");
if (!source.includes("support/browser/strict-browser-test")) {
failures.push(`${file}: bypasses strict browser fixture`);
}
}
let evidencePolicy: unknown;
try {
evidencePolicy = JSON.parse(await readFile(policyPath, "utf8"));
} catch (error) {
failures.push(
`${policyPath}: cannot read test evidence policy (${error instanceof Error ? error.message : String(error)})`,
);
}
const policy = evidencePolicy as Partial<TestEvidencePolicy> | undefined;
if (
policy?.schemaVersion !== 1 ||
!Array.isArray(policy.scenarioCatalogs) ||
!Array.isArray(policy.sourceContracts)
) {
failures.push(`${policyPath}: invalid test evidence policy`);
} else {
for (const candidate of policy.scenarioCatalogs) {
const contribution = candidate as Partial<ScenarioCatalogContribution>;
const minimumEntries = contribution.minimumEntries;
if (
typeof contribution?.owner !== "string" ||
typeof contribution?.path !== "string" ||
typeof contribution?.arrayExport !== "string" ||
typeof minimumEntries !== "number" ||
!Number.isInteger(minimumEntries) ||
minimumEntries < 1
) {
failures.push(`${policyPath}: invalid scenario catalog contribution`);
continue;
}
let source: string;
try {
source = await readFile(contribution.path, "utf8");
} catch (error) {
failures.push(
`${contribution.path}: cannot read scenario catalog (${error instanceof Error ? error.message : String(error)})`,
);
continue;
}
const arrayPattern = new RegExp(
`${escapeRegExp(contribution.arrayExport)}\\s*=\\s*Object\\.freeze\\(\\[([\\s\\S]*?)\\]\\s*as const\\)`,
);
const entryCount = (
arrayPattern.exec(source)?.[1]?.match(/"[^"]+"/g) ?? []
).length;
facts.sharedScenarios += entryCount;
if (entryCount < minimumEntries) {
failures.push(
`${contribution.path}: ${contribution.owner} requires at least ${minimumEntries} shared scenarios`,
);
}
}
for (const candidate of policy.sourceContracts) {
const contract = candidate as Partial<SourceContractContribution>;
const requiredTokens = contract.requiredTokens;
if (
typeof contract?.owner !== "string" ||
typeof contract?.path !== "string" ||
!Array.isArray(requiredTokens) ||
requiredTokens.some((token: unknown) => typeof token !== "string")
) {
failures.push(`${policyPath}: invalid source contract contribution`);
continue;
}
let source: string;
try {
source = await readFile(contract.path, "utf8");
} catch (error) {
failures.push(
`${contract.path}: cannot read source contract (${error instanceof Error ? error.message : String(error)})`,
);
continue;
}
for (const token of requiredTokens as readonly string[]) {
if (!source.includes(token)) {
failures.push(
`${contract.path}: ${contract.owner} evidence contract is missing ${token}`,
);
}
}
}
}
const baselineFiles = (await filesBelow("tests/visual/__snapshots__")).filter(
(file) => file.endsWith(".png"),
);
facts.visualBaselines = baselineFiles.length;
if (facts.visualBaselines < 4) {
failures.push("visual baseline requires at least four risk surfaces");
}
for (const required of [
"playwright.storybook.config.ts",
"playwright.visual.config.ts",
"tests/storybook/workshop.spec.ts",
]) {
if ((await filesBelow(required)).length === 0) {
failures.push(`test evidence missing ${required}`);
}
}
if (!sourceOnly) {
for (const required of [
"artifacts/tests/storybook/results.xml",
"artifacts/tests/visual/results.xml",
]) {
if ((await filesBelow(required)).length === 0) {
failures.push(`test evidence missing ${required}`);
}
}
const requiredBuiltFiles = [
"dist/index.html",
"dist/config.json",
"dist/release-manifest.json",
"dist/runtime-config.schema.json",
"dist/.vite/manifest.json",
];
for (const required of requiredBuiltFiles) {
if ((await filesBelow(required)).length === 0) {
failures.push(`built-dist contract missing ${required}`);
}
}
const sourceMaps = (await filesBelow("dist")).filter((file) =>
file.endsWith(".map"),
);
if (sourceMaps.length > 0) {
failures.push(`production dist contains source maps: ${sourceMaps.join(", ")}`);
}
}
}
const report = {
schemaVersion: 1,
sourceRoot,
status: failures.length === 0 ? "PASS" : "FAIL",
facts,
failures,
};
await mkdir(path.dirname(artifactPath), { recursive: true });
await writeFile(artifactPath, `${JSON.stringify(report, null, 2)}\n`);
if (failures.length > 0) {
process.stderr.write(`Test evidence failed:\n- ${failures.join("\n- ")}\n`);
process.exit(1);
}
process.stdout.write(
`Test evidence: PASS (${facts.scannedFiles} files, ${facts.visualBaselines} baselines, ${facts.sharedScenarios} scenarios)\n`,
);
@@ -3,8 +3,8 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
import {
evaluateFieldBudget,
percentile75,
} from "../src/application/policies/performance-budgets.js";
import { validateFieldEvidenceInput } from "./lib/field-vitals-evidence.mjs";
} from "../src/application/policies/performance-budgets.ts";
import { validateFieldEvidenceInput } from "./lib/field-vitals-evidence.ts";
const inputPath =
process.env.FIELD_WEB_VITALS_INPUT ||
@@ -17,15 +17,16 @@ const validation = validateFieldEvidenceInput(
now,
);
const input = validation.data;
const configured =
/** @type {{
* p75LcpMs: number,
* p75Cls: number,
* p75InpMs: number,
* minimumEligibleSamples: number | null
* }} */ (
JSON.parse(await readFile("config/performance/budgets.json", "utf8")).field
);
type FieldBudgets = {
p75LcpMs: number;
p75Cls: number;
p75InpMs: number;
minimumEligibleSamples: number | null;
};
const configured = JSON.parse(
await readFile("config/performance/budgets.json", "utf8"),
).field as FieldBudgets;
const minimumEligibleSamples = validation.minimumEligibleSamples;
const fallbackEnd = now;
const fallbackStart = new Date(fallbackEnd);
@@ -60,7 +61,7 @@ const routeSamples = Object.fromEntries(
counts[sample.routeId] = (counts[sample.routeId] ?? 0) + 1;
return counts;
},
/** @type {Record<string, number>} */ ({}),
{} as Record<string, number>,
),
).sort(([left], [right]) => left.localeCompare(right)),
);
@@ -1,58 +1,72 @@
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
import { shouldRetry } from "../src/adapters/http/retry-policy.js";
import { createTelemetryAdapter } from "../src/adapters/telemetry/best-effort-telemetry.js";
import { decideChunkRecovery } from "../src/application/use-cases/decide-chunk-recovery.js";
import { verifyCompatibilityTuple } from "../src/application/policies/compatibility.js";
import { validateRuntimeConfig } from "../src/bootstrap/runtime-config-schema.js";
import { projectTelemetryEvent } from "../src/contracts/telemetry.js";
import { compareReleaseToRuntime } from "../src/contracts/release-tokens.js";
import { shouldRetry } from "../src/adapters/http/retry-policy.ts";
import { createTelemetryAdapter } from "../src/adapters/telemetry/best-effort-telemetry.ts";
import { verifyCompatibilityTuple } from "../src/application/policies/compatibility.ts";
import type { StoragePort } from "../src/application/ports/storage-port.ts";
import { decideChunkRecovery } from "../src/application/use-cases/decide-chunk-recovery.ts";
import { validateRuntimeConfig } from "../src/bootstrap/runtime-config-schema.ts";
import { compareReleaseToRuntime } from "../src/contracts/release-tokens.ts";
import { projectTelemetryEvent } from "../src/contracts/telemetry.ts";
/**
* @typedef {{
* triggerAsserted: boolean,
* containmentAsserted: boolean,
* recoveryAssertions: Array<{
* assertion: string,
* evidence: string,
* passed: boolean
* }>,
* negativeFixtureFailedAsExpected: boolean,
* providerVerificationRequired: boolean
* }} DrillResult
*/
type RecoveryAssertion = Readonly<{
assertion: string;
evidence: string;
passed: boolean;
}>;
type DrillResult = Readonly<{
triggerAsserted: boolean;
containmentAsserted: boolean;
recoveryAssertions: RecoveryAssertion[];
negativeFixtureFailedAsExpected: boolean;
providerVerificationRequired: boolean;
}>;
type RunbookSpecification = Readonly<{
title: string;
gateId: string;
triggerKinds: string[];
containment: string;
window: string;
escalation: string[];
recoveryEvidence: string[];
negativeFixture: string;
}>;
type RunbookDocument = Readonly<{
runbooks: Record<string, RunbookSpecification>;
}>;
type ReleaseManifest = Record<string, unknown> & {
buildId: string;
configSchemaVersion: string;
apiContractVersion: string;
assetManifestHash: string;
releaseId: string;
};
const runbookId = process.argv
.slice(2)
.find((argument) => /^FE-RB-00[1-5]$/.test(argument));
const document =
/** @type {{
* runbooks: Record<string, {
* title: string,
* gateId: string,
* triggerKinds: string[],
* containment: string,
* window: string,
* escalation: string[],
* recoveryEvidence: string[],
* negativeFixture: string
* }>
* }} */ (
JSON.parse(await readFile("config/runbooks/runbooks.json", "utf8"))
);
const document = JSON.parse(
await readFile("config/runbooks/runbooks.json", "utf8"),
) as RunbookDocument;
const specification = runbookId ? document.runbooks[runbookId] : undefined;
if (!runbookId || !specification) {
process.stderr.write("Usage: drill:runbook -- FE-RB-001..FE-RB-005\n");
process.exit(2);
}
async function releaseManifest() {
async function releaseManifest(): Promise<ReleaseManifest> {
for (const candidate of [
"dist/release-manifest.json",
"public/release-manifest.json",
]) {
try {
return JSON.parse(await readFile(candidate, "utf8"));
return JSON.parse(
await readFile(candidate, "utf8"),
) as ReleaseManifest;
} catch {
// Continue to the source fallback.
}
@@ -74,12 +88,15 @@ const validConfig = {
RELEASE_ID: "local-release",
};
/** @param {string} assertion @param {string} evidence @param {boolean} passed */
function assertion(assertion, evidence, passed) {
function assertion(
assertion: string,
evidence: string,
passed: boolean,
): RecoveryAssertion {
return { assertion, evidence, passed };
}
async function drillBoot() {
async function drillBoot(): Promise<DrillResult> {
const invalid = validateRuntimeConfig({
...validConfig,
APP_ENV: "production",
@@ -103,23 +120,21 @@ async function drillBoot() {
};
}
function memoryStorage() {
/** @type {unknown} */
let value;
function memoryStorage(): StoragePort {
let value: unknown;
return {
read: () => ({ ok: /** @type {const} */ (true), value }),
/** @param {string} _key @param {unknown} next */
write: (_key, next) => {
read: () => ({ ok: true, value }),
write: (_key: string, next: unknown) => {
value = next;
return { ok: /** @type {const} */ (true) };
return { ok: true };
},
remove: () => ({ ok: /** @type {const} */ (true) }),
remove: () => ({ ok: true }),
};
}
async function drillChunkMismatch() {
async function drillChunkMismatch(): Promise<DrillResult> {
const storage = memoryStorage();
const input = {
const input: Parameters<typeof decideChunkRecovery>[0] = {
failureKind: "DEPLOY_MISMATCH",
manifestLoaded: true,
currentBuildId: "build-a",
@@ -157,7 +172,7 @@ async function drillChunkMismatch() {
};
}
async function drillApiDegradation() {
async function drillApiDegradation(): Promise<DrillResult> {
const unkeyedRetry = shouldRetry(
{ idempotency: "none" },
{ kind: "SERVER_FAILURE", httpStatus: 503 },
@@ -182,7 +197,7 @@ async function drillApiDegradation() {
};
}
async function drillTelemetry() {
async function drillTelemetry(): Promise<DrillResult> {
const adapter = createTelemetryAdapter({
enabled: true,
endpoint: "https://telemetry.invalid/events",
@@ -221,7 +236,7 @@ async function drillTelemetry() {
};
}
async function drillRollback() {
async function drillRollback(): Promise<DrillResult> {
const release = await releaseManifest();
const runtime = JSON.parse(
await readFile(
@@ -255,21 +270,20 @@ async function drillRollback() {
assertion("compatibility gate", "typed version comparison", coherent.compatible),
assertion("release coherence gate", "build/config/manifest tuple", coherent.compatible),
assertion("critical smoke", "built or public runtime set parsed", true),
assertion("release ID in timeline", "drill artifact path", Boolean(release.releaseId)),
assertion("release ID in timeline", "drill record releaseId", Boolean(release.releaseId)),
],
negativeFixtureFailedAsExpected: !mixed.compatible,
providerVerificationRequired: true,
};
}
const drillById =
/** @type {Record<string, () => Promise<DrillResult>>} */ ({
"FE-RB-001": drillBoot,
"FE-RB-002": drillChunkMismatch,
"FE-RB-003": drillApiDegradation,
"FE-RB-004": drillTelemetry,
"FE-RB-005": drillRollback,
});
const drillById: Record<string, () => Promise<DrillResult>> = {
"FE-RB-001": drillBoot,
"FE-RB-002": drillChunkMismatch,
"FE-RB-003": drillApiDegradation,
"FE-RB-004": drillTelemetry,
"FE-RB-005": drillRollback,
};
const drill = await drillById[runbookId]();
const escalationPathAsserted = specification.escalation.length >= 2;
const passed =
@@ -294,7 +308,7 @@ const record = {
providerVerificationRequired: drill.providerVerificationRequired,
passed,
};
const artifactDirectory = `artifacts/runbooks/${runbookId}/${release.releaseId}`;
const artifactDirectory = `artifacts/runbooks/${runbookId}`;
await mkdir(artifactDirectory, { recursive: true });
await writeFile(
`${artifactDirectory}/record.json`,
-102
View File
@@ -1,102 +0,0 @@
import { createHash } from "node:crypto";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import process from "node:process";
import { z } from "zod";
import {
ROUTE_REGISTRY,
ROUTE_RUNTIME_CONTRACT,
} from "../src/features/installed-feature-contracts.js";
import { runtimeConfigSchema } from "../src/bootstrap/runtime-config-schema.js";
const packageJson = JSON.parse(await readFile("package.json", "utf8"));
const packageManagerVersion = packageJson.packageManager.split("@").at(-1);
const buildId = process.env.VITE_BUILD_ID ?? "local-build";
const commitSha = process.env.VITE_COMMIT_SHA ?? "local";
const releaseId = process.env.RELEASE_ID ?? "local-release";
const runnerImage = process.env.CI_RUNNER_IMAGE ?? `${process.platform}-${process.arch}`;
const buildTime = process.env.SOURCE_DATE_EPOCH
? new Date(Number(process.env.SOURCE_DATE_EPOCH) * 1_000)
: new Date();
if (!Number.isFinite(buildTime.getTime())) {
throw new Error("SOURCE_DATE_EPOCH must be epoch seconds");
}
const builtAt = buildTime.toISOString();
const viteManifest = await readFile("dist/.vite/manifest.json", "utf8");
const viteManifestObject =
/** @type {Record<string, {file: string, name?: string, isDynamicEntry?: boolean}>} */ (
JSON.parse(viteManifest)
);
const assetManifestHash = createHash("sha256")
.update(viteManifest)
.digest("hex");
const runtimeConfig = JSON.parse(await readFile("dist/config.json", "utf8"));
/** @type {Record<string, string>} */
const routeChunks = {};
for (const definition of Object.values(ROUTE_REGISTRY)) {
const runtime =
/** @type {Record<string, {moduleId: string}>} */ (
ROUTE_RUNTIME_CONTRACT
)[definition.routeId];
const asset = Object.values(viteManifestObject).find(
(entry) => entry.name === runtime?.moduleId && entry.isDynamicEntry,
);
if (!runtime || !asset?.file) {
throw new Error(`Missing built route chunk: ${definition.routeId}`);
}
routeChunks[definition.chunkId] = asset.file;
}
const runtimeConfigJsonSchema = z.toJSONSchema(runtimeConfigSchema);
runtimeConfig.BUILD_ID = buildId;
runtimeConfig.RELEASE_ID = releaseId;
const manifest = {
schemaVersion: 1,
buildId,
commitSha,
generatedAt: builtAt,
buildContext: {
nodeVersion: process.version,
packageManagerVersion,
runnerImage,
},
outputs: {
directory: "dist",
viteManifest: "dist/.vite/manifest.json",
routeChunks,
runtimeConfigSchema: "dist/runtime-config.schema.json",
},
};
const releaseManifest = {
schemaVersion: 1,
appVersion: packageJson.version,
buildId,
commitSha,
configSchemaVersion: runtimeConfig.CONFIG_SCHEMA_VERSION,
apiContractVersion: runtimeConfig.API_CONTRACT_VERSION,
assetManifestHash,
releaseId,
builtAt,
routeChunks,
};
await mkdir("artifacts/release", { recursive: true });
await writeFile("dist/config.json", `${JSON.stringify(runtimeConfig, null, 2)}\n`);
await writeFile(
"dist/release-manifest.json",
`${JSON.stringify(releaseManifest, null, 2)}\n`,
);
await writeFile(
"dist/runtime-config.schema.json",
`${JSON.stringify(runtimeConfigJsonSchema, null, 2)}\n`,
);
await writeFile(
"artifacts/release/runtime-config.schema.json",
`${JSON.stringify(runtimeConfigJsonSchema, null, 2)}\n`,
);
await writeFile(
"artifacts/release/build-manifest.json",
`${JSON.stringify(manifest, null, 2)}\n`,
);
+180
View File
@@ -0,0 +1,180 @@
import { createHash } from "node:crypto";
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import process from "node:process";
import { z } from "zod";
import {
ROUTE_REGISTRY,
ROUTE_RUNTIME_CONTRACT,
} from "../src/features/installed-feature-contracts.ts";
import { runtimeConfigSchema } from "../src/bootstrap/runtime-config-schema.ts";
import {
assertCiBuildEnvironment,
buildDate,
} from "./lib/build-environment.ts";
assertCiBuildEnvironment(process.env);
type ViteManifestEntry = Readonly<{
file: string;
name?: string;
isDynamicEntry?: boolean;
}>;
const packageJson = parsePackageMetadata(
JSON.parse(await readFile("package.json", "utf8")),
);
const packageManagerVersion = packageJson.packageManager.split("@").at(-1);
const buildId = process.env.VITE_BUILD_ID ?? "local-build";
const commitSha = process.env.VITE_COMMIT_SHA ?? "local";
const releaseId = process.env.RELEASE_ID ?? "local-release";
const runnerImage = process.env.CI_RUNNER_IMAGE ?? `${process.platform}-${process.arch}`;
const buildTime = buildDate(process.env);
const builtAt = buildTime.toISOString();
const viteManifest = await readFile("dist/.vite/manifest.json", "utf8");
const viteManifestObject = parseViteManifest(JSON.parse(viteManifest));
const moduleInventory = await readFile(
"dist/.vite/module-inventory.json",
"utf8",
);
parseModuleInventory(JSON.parse(moduleInventory));
const assetManifestHash = createHash("sha256")
.update(viteManifest)
.digest("hex");
const moduleInventoryHash = createHash("sha256")
.update(moduleInventory)
.digest("hex");
const runtimeConfig = runtimeConfigSchema.parse(
JSON.parse(await readFile("dist/config.json", "utf8")),
);
const routeChunks: Record<string, string> = {};
const runtimeContracts: Readonly<Record<string, { moduleId: string }>> =
ROUTE_RUNTIME_CONTRACT;
for (const definition of Object.values(ROUTE_REGISTRY)) {
const runtime = runtimeContracts[definition.routeId];
const asset = Object.values(viteManifestObject).find(
(entry) => entry.name === runtime?.moduleId && entry.isDynamicEntry,
);
if (!runtime || !asset?.file) {
throw new Error(`Missing built route chunk: ${definition.routeId}`);
}
routeChunks[definition.chunkId] = asset.file;
}
const runtimeConfigJsonSchema = z.toJSONSchema(runtimeConfigSchema);
runtimeConfig.BUILD_ID = buildId;
runtimeConfig.RELEASE_ID = releaseId;
const manifest = {
schemaVersion: 1,
buildId,
commitSha,
releaseId,
moduleInventoryHash,
generatedAt: builtAt,
buildContext: {
nodeVersion: process.version,
packageManagerVersion,
runnerImage,
sourceDateEpoch: process.env.SOURCE_DATE_EPOCH ?? null,
},
outputs: {
directory: "dist",
viteManifest: "dist/.vite/manifest.json",
moduleInventory: "artifacts/quality/vite-module-inventory.json",
routeChunks,
runtimeConfigSchema: "dist/runtime-config.schema.json",
},
};
const releaseManifest = {
schemaVersion: 1,
appVersion: packageJson.version,
buildId,
commitSha,
configSchemaVersion: runtimeConfig.CONFIG_SCHEMA_VERSION,
apiContractVersion: runtimeConfig.API_CONTRACT_VERSION,
assetManifestHash,
releaseId,
builtAt,
routeChunks,
};
await mkdir("artifacts/release", { recursive: true });
await mkdir("artifacts/quality", { recursive: true });
await writeFile(
"artifacts/quality/vite-module-inventory.json",
moduleInventory,
);
await rm("dist/.vite/module-inventory.json");
await writeFile("dist/config.json", `${JSON.stringify(runtimeConfig, null, 2)}\n`);
await writeFile(
"dist/release-manifest.json",
`${JSON.stringify(releaseManifest, null, 2)}\n`,
);
await writeFile(
"dist/runtime-config.schema.json",
`${JSON.stringify(runtimeConfigJsonSchema, null, 2)}\n`,
);
await writeFile(
"artifacts/release/runtime-config.schema.json",
`${JSON.stringify(runtimeConfigJsonSchema, null, 2)}\n`,
);
await writeFile(
"artifacts/release/build-manifest.json",
`${JSON.stringify(manifest, null, 2)}\n`,
);
function parsePackageMetadata(value: unknown): Readonly<{
version: string;
packageManager: string;
}> {
if (
!isRecord(value) ||
typeof value.version !== "string" ||
typeof value.packageManager !== "string"
) {
throw new TypeError("package.json release metadata is invalid");
}
return { version: value.version, packageManager: value.packageManager };
}
function parseViteManifest(
value: unknown,
): Readonly<Record<string, ViteManifestEntry>> {
if (!isRecord(value)) throw new TypeError("Vite manifest must be an object");
const entries: Record<string, ViteManifestEntry> = {};
for (const [key, candidate] of Object.entries(value)) {
if (!isRecord(candidate) || typeof candidate.file !== "string") {
throw new TypeError(`Invalid Vite manifest entry: ${key}`);
}
entries[key] = {
file: candidate.file,
...(typeof candidate.name === "string" ? { name: candidate.name } : {}),
...(typeof candidate.isDynamicEntry === "boolean"
? { isDynamicEntry: candidate.isDynamicEntry }
: {}),
};
}
return entries;
}
function parseModuleInventory(value: unknown): void {
if (
!isRecord(value) ||
value.schemaVersion !== 1 ||
!Array.isArray(value.chunks) ||
value.chunks.some(
(chunk) =>
!isRecord(chunk) ||
typeof chunk.fileName !== "string" ||
!Array.isArray(chunk.modules) ||
chunk.modules.some((moduleId) => typeof moduleId !== "string"),
)
) {
throw new TypeError("Vite module inventory is invalid");
}
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
@@ -20,31 +20,54 @@ import {
validateLicensePolicy,
validateVulnerabilityReport,
verifySupplyChainCoherence,
} from "./lib/supply-chain.mjs";
type DependencyInventoryDiff,
} from "./lib/supply-chain.ts";
/** @param {string} directory @returns {Promise<string[]>} */
async function filesWithin(directory) {
type Document = Record<string, unknown>;
function isRecord(value: unknown): value is Document {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function documentValue(value: unknown, label: string): Document {
if (!isRecord(value)) throw new Error(`${label} must be a JSON object`);
return value;
}
function stringMap(value: unknown): Record<string, string> {
if (!isRecord(value)) return {};
return Object.fromEntries(
Object.entries(value).filter(
(entry): entry is [string, string] => typeof entry[1] === "string",
),
);
}
async function jsonDocument(file: string): Promise<Document> {
const parsed: unknown = JSON.parse(await readFile(file, "utf8"));
return documentValue(parsed, file);
}
async function filesWithin(directory: string): Promise<string[]> {
try {
const entries = await readdir(directory, { withFileTypes: true });
const nested = /** @type {string[][]} */ (await Promise.all(
const nested: string[][] = await Promise.all(
entries.map((entry) => {
const target = path.join(directory, entry.name);
return entry.isDirectory() ? filesWithin(target) : [target];
}),
));
);
return nested.flat().sort();
} catch {
return [];
}
}
/** @param {string} file */
async function sha256File(file) {
async function sha256File(file: string): Promise<string> {
return createHash("sha256").update(await readFile(file)).digest("hex");
}
/** @param {string[]} files */
async function digestFileSet(files) {
async function digestFileSet(files: string[]): Promise<string> {
const rows = await Promise.all(
files.sort().map(async (file) => ({
path: file.replaceAll("\\", "/"),
@@ -54,17 +77,17 @@ async function digestFileSet(files) {
return supplyChainDigest(rows);
}
/** @param {string} file @returns {Promise<Record<string, unknown> | null>} */
async function optionalJson(file) {
async function optionalJson(file: string): Promise<Document | null> {
try {
return JSON.parse(await readFile(file, "utf8"));
const parsed: unknown = JSON.parse(await readFile(file, "utf8"));
return isRecord(parsed) ? parsed : null;
} catch {
return null;
}
}
export async function buildDependencyInventory() {
const packageJson = JSON.parse(await readFile("package.json", "utf8"));
const packageJson = await jsonDocument("package.json");
const lockfileText = await readFile("pnpm-lock.yaml", "utf8");
const lockfileSha256 = createHash("sha256")
.update(lockfileText)
@@ -80,18 +103,19 @@ export async function buildDependencyInventory() {
if (listed.status !== 0) {
throw new Error(`pnpm dependency graph failed: ${listed.stderr}`);
}
const roots = JSON.parse(listed.stdout);
const root = roots[0];
const roots: unknown = JSON.parse(listed.stdout);
const root = Array.isArray(roots) && isRecord(roots[0]) ? roots[0] : null;
if (!root) throw new Error("pnpm dependency graph root is invalid");
const flattened = await flattenPnpmDependencyTree(
root,
packageJson.dependencies ?? {},
packageJson.devDependencies ?? {},
stringMap(packageJson.dependencies),
stringMap(packageJson.devDependencies),
);
const lockRows = parsePnpmLockfilePackages(lockfileText);
const lockByIdentity = new Map(
lockRows.map((row) => [`${row.name}@${row.version}`, row]),
);
const failures = [];
const failures: string[] = [];
const dependencies = flattened.map((dependency) => {
const identity = `${dependency.name}@${dependency.version}`;
const lockRow = lockByIdentity.get(identity);
@@ -118,7 +142,7 @@ export async function buildDependencyInventory() {
}
return {
schemaVersion: 2,
packageManager: packageJson.packageManager,
packageManager: String(packageJson.packageManager ?? ""),
lockfileSha256,
dependencyCount: dependencies.length,
directDependencyCount: dependencies.filter((entry) => entry.direct).length,
@@ -126,7 +150,7 @@ export async function buildDependencyInventory() {
};
}
const packageJson = JSON.parse(await readFile("package.json", "utf8"));
const packageJson = await jsonDocument("package.json");
const outputFiles = await filesWithin("dist");
if (outputFiles.length === 0) {
throw new Error("dist is missing; run the production build first");
@@ -169,19 +193,19 @@ const dependencyEvidence = JSON.parse(
),
);
const skipsBaseline = process.argv.includes("--no-baseline");
const baselineFailures = [];
let dependencyDiff =
/** @type {ReturnType<typeof diffDependencyInventories>} */ ({
added: [],
removed: [],
changed: [],
upgrades: [],
const baselineFailures: string[] = [];
let dependencyDiff: DependencyInventoryDiff = Object.freeze({
added: Object.freeze([]),
removed: Object.freeze([]),
changed: Object.freeze([]),
upgrades: Object.freeze([]),
});
let reviewResult =
/** @type {ReturnType<typeof validateDependencyReview>} */ ({
let reviewResult: ReturnType<typeof validateDependencyReview> = Object.freeze({
passed: skipsBaseline,
highRisk: [],
failures: skipsBaseline ? [] : ["dependency baseline unavailable"],
highRisk: Object.freeze([]),
failures: Object.freeze(
skipsBaseline ? [] : ["dependency baseline unavailable"],
),
});
if (baseline && baselineApproval) {
const actualBaselineDigest = supplyChainDigest(baseline);
@@ -253,7 +277,7 @@ const sourceFiles = (
"schemas",
"package.json",
"pnpm-lock.yaml",
"vite.config.js",
"vite.config.ts",
].map(async (target) => {
try {
const metadata = await stat(target);
@@ -299,8 +323,8 @@ const sbom = {
metadata: {
component: {
type: "application",
name: packageJson.name,
version: packageJson.version,
name: String(packageJson.name ?? ""),
version: String(packageJson.version ?? ""),
},
properties: [
{
@@ -328,7 +352,7 @@ const provenance = {
buildType: "https://vite.dev/build/v1",
externalParameters: {
nodeVersion: process.version,
packageManager: packageJson.packageManager,
packageManager: String(packageJson.packageManager ?? ""),
},
internalParameters: {
sourceSetSha256,
@@ -361,12 +385,12 @@ const coherence = verifySupplyChainCoherence(
const attestationInput = process.env.PROVENANCE_ATTESTATION_PATH
? await optionalJson(process.env.PROVENANCE_ATTESTATION_PATH)
: null;
const attestationSubject =
/** @type {Record<string, unknown>} */ (
/** @type {Record<string, unknown>} */ (
attestationInput?.subject ?? {}
).digest ?? {}
);
const attestation = isRecord(attestationInput?.subject)
? attestationInput.subject
: {};
const attestationSubject = isRecord(attestation.digest)
? attestation.digest
: {};
const attestationPassed =
attestationSubject.sha256 === distDigest &&
typeof attestationInput?.provider === "string" &&
@@ -416,7 +440,7 @@ await writeFile(
generatedAt: new Date().toISOString(),
context: {
nodeVersion: process.version,
packageManager: packageJson.packageManager,
packageManager: String(packageJson.packageManager ?? ""),
runnerImage:
process.env.CI_RUNNER_IMAGE ?? `${process.platform}-${process.arch}`,
},
+96
View File
@@ -0,0 +1,96 @@
export const CI_BUILD_ENVIRONMENT_VARIABLES = Object.freeze([
"VITE_BUILD_ID",
"VITE_COMMIT_SHA",
"RELEASE_ID",
"CI_RUNNER_IMAGE",
"SOURCE_DATE_EPOCH",
]);
export function ciBuildEnvironmentFailures(
environment: Readonly<Record<string, string | undefined>>,
) {
if (environment.CI !== "true") return [];
const failures = CI_BUILD_ENVIRONMENT_VARIABLES.filter(
(name) => !environment[name]?.trim(),
).map((name) => `missing required CI build environment: ${name}`);
const commitSha = environment.VITE_COMMIT_SHA?.trim();
if (commitSha && !isValidCommitSha(commitSha)) {
failures.push(
"VITE_COMMIT_SHA must be a full 40- or 64-character hexadecimal commit ID",
);
}
const sourceDateEpoch = environment.SOURCE_DATE_EPOCH?.trim();
if (sourceDateEpoch && !isValidSourceDateEpoch(sourceDateEpoch)) {
failures.push("SOURCE_DATE_EPOCH must be non-negative epoch seconds");
}
const runnerImage = environment.CI_RUNNER_IMAGE?.trim();
if (
runnerImage &&
!/@sha256:[0-9a-f]{64}$/i.test(runnerImage)
) {
failures.push(
"CI_RUNNER_IMAGE must end with an immutable @sha256 image digest",
);
}
return failures;
}
export function assertCiBuildEnvironment(
environment: Readonly<Record<string, string | undefined>>,
) {
const failures = ciBuildEnvironmentFailures(environment);
if (failures.length > 0) {
throw new Error(failures.join("; "));
}
}
export function isValidCommitSha(value: string) {
return /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(value);
}
export function isValidSourceDateEpoch(value: string) {
if (!/^\d+$/.test(value)) return false;
const milliseconds = Number(value) * 1_000;
return Number.isSafeInteger(milliseconds) && Number.isFinite(
new Date(milliseconds).getTime(),
);
}
export function ciCheckoutIdentityFailures(
environment: Readonly<Record<string, string | undefined>>,
checkout: { commitSha: string; sourceDateEpoch: string },
) {
if (environment.CI !== "true") return [];
const failures = [];
const configuredCommitSha = environment.VITE_COMMIT_SHA?.trim();
if (
configuredCommitSha &&
configuredCommitSha.toLowerCase() !== checkout.commitSha.toLowerCase()
) {
failures.push("VITE_COMMIT_SHA does not identify the checked-out commit");
}
const configuredEpoch = environment.SOURCE_DATE_EPOCH?.trim();
if (configuredEpoch && configuredEpoch !== checkout.sourceDateEpoch) {
failures.push(
"SOURCE_DATE_EPOCH does not match the checked-out commit timestamp",
);
}
return failures;
}
export function buildDate(
environment: Readonly<Record<string, string | undefined>>,
) {
const sourceDateEpoch = environment.SOURCE_DATE_EPOCH?.trim();
if (!sourceDateEpoch) return new Date();
if (!isValidSourceDateEpoch(sourceDateEpoch)) {
throw new Error("SOURCE_DATE_EPOCH must be non-negative epoch seconds");
}
return new Date(Number(sourceDateEpoch) * 1_000);
}
@@ -1,27 +1,27 @@
/**
* @typedef {{
* file: string,
* isEntry?: boolean,
* imports?: string[]
* }} ViteManifestEntry
*/
type ViteManifestEntry = Readonly<{
file: string;
isEntry?: boolean;
imports?: readonly string[];
}>;
/**
* 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();
export function classifyViteJavascript(
manifest: Readonly<Record<string, ViteManifestEntry>>,
) {
const initialFiles = new Set<string>();
const visitedKeys = new Set<string>();
const pendingKeys = Object.entries(manifest)
.filter(([, entry]) => entry.isEntry)
.map(([key]) => key);
const missingImports = [];
const missingImports: string[] = [];
while (pendingKeys.length > 0) {
const key = /** @type {string} */ (pendingKeys.pop());
const key = pendingKeys.pop();
if (key === undefined) break;
if (visitedKeys.has(key)) continue;
visitedKeys.add(key);
const entry = manifest[key];
@@ -68,15 +68,10 @@ const fieldEvidenceInputSchema = z
}
});
/**
* @param {unknown} input
* @param {string | undefined} configuredMinimum
* @param {Date} [now]
*/
export function validateFieldEvidenceInput(
input,
configuredMinimum,
now = new Date(),
input: unknown,
configuredMinimum: string | undefined,
now: Date = new Date(),
) {
const parsed = fieldEvidenceInputSchema.safeParse(input);
const failures = parsed.success
@@ -4,15 +4,21 @@ const LOOPBACK_IPV4 = /^127(?:\.\d{1,3}){3}$/;
* A release gate must not promote a local preview server as live hosting
* evidence.
*
* @param {string} value
* @returns {
* | { passed: true; reason: null; url: URL; observedOrigin: string }
* | { passed: false; reason: string; url: URL | null; observedOrigin: string | null }
* }
*/
export function classifyLiveHostingBaseUrl(value) {
/** @type {URL} */
let url;
export function classifyLiveHostingBaseUrl(value: string):
| {
passed: true;
reason: null;
url: URL;
observedOrigin: string;
}
| {
passed: false;
reason: string;
url: URL | null;
observedOrigin: string | null;
} {
let url: URL;
try {
url = new URL(value);
} catch {
@@ -1,4 +1,4 @@
import { ROUTE_REGISTRY } from "../../src/features/installed-feature-contracts.js";
import { ROUTE_REGISTRY } from "../../src/features/installed-feature-contracts.ts";
export const MANUAL_A11Y_ROUTE_IDS = Object.freeze(
Object.values(ROUTE_REGISTRY).map((route) => route.routeId),
@@ -15,19 +15,18 @@ const REVIEW_FIELDS = Object.freeze([
"Screen reader",
]);
/** @param {string} content */
export function validateManualA11yEvidence(content) {
export function validateManualA11yEvidence(content: string) {
const fields = Object.fromEntries(
content
.split(/\r?\n/)
.map((line) => /^([^:]+):\s*(.*)$/.exec(line))
.filter(Boolean)
.filter((match): match is RegExpExecArray => match !== null)
.map((match) => [
/** @type {RegExpExecArray} */ (match)[1].trim(),
/** @type {RegExpExecArray} */ (match)[2].trim(),
match[1].trim(),
match[2].trim(),
]),
);
const failures = [];
const failures: string[] = [];
if (fields.Status !== "reviewed") failures.push("Status");
if (!fields["Route ID"]) failures.push("Route ID");
if (!fields["Release ID"]) failures.push("Release ID");
+373
View File
@@ -0,0 +1,373 @@
import { createHash } from "node:crypto";
import { lstat, readdir, realpath } from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { gzipSync } from "node:zlib";
import {
build,
normalizePath,
type Plugin,
version as viteVersion,
} from "vite";
const RECIPE_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
const SOURCE_EXTENSION = /\.(?:[cm]?[jt]s|[jt]sx)$/;
const DECLARATION_FILE = /\.d\.[cm]?ts$/;
type EmittedOutput =
| Readonly<{
type: "chunk";
fileName: string;
code: string;
}>
| Readonly<{
type: "asset";
fileName: string;
source: string | Uint8Array;
}>;
export type OptionalRecipeBundleOutput = Readonly<{
fileName: string;
bytes: number;
gzipBytes: number;
sha256: string;
}>;
export type OptionalRecipeBundleMeasurement = Readonly<{
recipeId: string;
sourceRoots: readonly string[];
sourceFileCount: number;
toolchain: Readonly<{
bundler: "vite";
viteVersion: string;
mode: "production";
target: "es2022";
format: "es";
minifier: "esbuild";
treeshake: false;
compression: "node-zlib-gzip";
}>;
outputs: readonly OptionalRecipeBundleOutput[];
bytes: number;
gzipBytes: number;
bundleBudgetGzipBytes: number;
remainingGzipBytes: number;
sha256: string;
passed: boolean;
}>;
/**
* Builds an uncomposed reference runtime as a synthetic production consumer.
* Every catalog-owned source module is exposed as an entry namespace and
* tree-shaking is disabled so internal fail-closed paths remain in the budget.
*/
export async function measureOptionalRecipeBundle(input: Readonly<{
recipeId: string;
sourceRoots: readonly string[];
bundleBudgetGzipBytes: number;
workspaceRoot?: string;
}>): Promise<OptionalRecipeBundleMeasurement> {
const recipeId = validateRecipeId(input.recipeId);
const bundleBudgetGzipBytes = positiveSafeInteger(
input.bundleBudgetGzipBytes,
"Optional recipe bundle budget",
);
const workspaceRoot = await realpath(
path.resolve(input.workspaceRoot ?? process.cwd()),
);
const sourceRoots = validateSourceRoots(input.sourceRoots);
const sourceFiles = await resolveSourceFiles(
workspaceRoot,
sourceRoots,
);
const virtualEntry =
`virtual:optional-reference-runtime-entry/${recipeId}`;
const resolvedVirtualEntry = `\0${virtualEntry}`;
const entrySource = sourceFiles
.map(
(sourceFile, index) =>
`export * as source${index} from ${JSON.stringify(
viteSourceSpecifier(sourceFile),
)};`,
)
.join("\n")
.concat("\n");
const preservePublicEntryPlugin = {
name: "optional-reference-runtime-entry",
enforce: "pre",
resolveId(id) {
return id === virtualEntry ? resolvedVirtualEntry : null;
},
load(id) {
return id === resolvedVirtualEntry ? entrySource : null;
},
options(options) {
return {
...options,
preserveEntrySignatures: "strict",
};
},
} satisfies Plugin;
const buildResult = await build({
root: workspaceRoot,
configFile: false,
envFile: false,
mode: "production",
publicDir: false,
clearScreen: false,
logLevel: "silent",
plugins: [preservePublicEntryPlugin],
build: {
target: "es2022",
minify: "esbuild",
sourcemap: false,
write: false,
emptyOutDir: false,
copyPublicDir: false,
cssCodeSplit: false,
reportCompressedSize: false,
rollupOptions: {
input: virtualEntry,
// Budget the complete selected runtime, including internal fail-closed
// guards that a synthetic consumer cannot predict it will exercise.
treeshake: false,
output: {
format: "es",
entryFileNames: `${recipeId}.js`,
chunkFileNames: `${recipeId}-chunk-[hash].js`,
assetFileNames: `${recipeId}-asset-[name]-[hash][extname]`,
},
},
},
});
const emitted = emittedOutputs(buildResult);
if (emitted.length === 0) {
throw new TypeError("Optional recipe bundle emitted no output.");
}
const outputs = emitted
.map((output) => {
const bytes = outputBytes(output);
return Object.freeze({
fileName: output.fileName,
bytes: bytes.byteLength,
gzipBytes: gzipSync(bytes).byteLength,
sha256: createHash("sha256").update(bytes).digest("hex"),
});
})
.sort((left, right) => compareText(left.fileName, right.fileName));
const aggregateHash = createHash("sha256");
for (const output of outputs) {
aggregateHash.update(output.fileName);
aggregateHash.update("\0");
aggregateHash.update(output.sha256);
aggregateHash.update("\0");
}
const bytes = outputs.reduce(
(total, output) => total + output.bytes,
0,
);
const gzipBytes = outputs.reduce(
(total, output) => total + output.gzipBytes,
0,
);
return Object.freeze({
recipeId,
sourceRoots: Object.freeze([...sourceRoots]),
sourceFileCount: sourceFiles.length,
toolchain: Object.freeze({
bundler: "vite" as const,
viteVersion,
mode: "production" as const,
target: "es2022" as const,
format: "es" as const,
minifier: "esbuild" as const,
treeshake: false as const,
compression: "node-zlib-gzip" as const,
}),
outputs: Object.freeze(outputs),
bytes,
gzipBytes,
bundleBudgetGzipBytes,
remainingGzipBytes: bundleBudgetGzipBytes - gzipBytes,
sha256: aggregateHash.digest("hex"),
passed: gzipBytes <= bundleBudgetGzipBytes,
});
}
async function resolveSourceFiles(
workspaceRoot: string,
sourceRoots: readonly string[],
): Promise<readonly string[]> {
const sourceBoundary = await realpath(path.join(workspaceRoot, "src"));
const discovered: string[] = [];
for (const sourceRoot of sourceRoots) {
const target = path.resolve(workspaceRoot, sourceRoot);
assertInsideSourceBoundary(target, sourceBoundary);
const rootMetadata = await lstat(target);
if (rootMetadata.isSymbolicLink()) {
throw new TypeError("Optional recipe source root cannot be a symlink.");
}
assertInsideSourceBoundary(await realpath(target), sourceBoundary);
if (
rootMetadata.isFile() &&
(!SOURCE_EXTENSION.test(target) || DECLARATION_FILE.test(target))
) {
throw new TypeError("Optional recipe source root is not executable source.");
}
discovered.push(
...(await collectExecutableSources(target, sourceBoundary)),
);
}
const unique = [...new Set(discovered)].sort((left, right) =>
compareText(
normalizePath(path.relative(workspaceRoot, left)),
normalizePath(path.relative(workspaceRoot, right)),
),
);
if (unique.length === 0) {
throw new TypeError("Optional recipe source roots contain no executable source.");
}
return Object.freeze(unique);
}
async function collectExecutableSources(
target: string,
sourceBoundary: string,
): Promise<string[]> {
const metadata = await lstat(target);
if (metadata.isSymbolicLink()) {
throw new TypeError("Optional recipe source cannot be a symlink.");
}
assertInsideSourceBoundary(target, sourceBoundary);
if (metadata.isFile()) {
return SOURCE_EXTENSION.test(target) && !DECLARATION_FILE.test(target)
? [target]
: [];
}
if (!metadata.isDirectory()) return [];
const entries = (await readdir(target, { withFileTypes: true })).sort(
(left, right) => compareText(left.name, right.name),
);
const groups = await Promise.all(
entries.map((entry) =>
collectExecutableSources(
path.join(target, entry.name),
sourceBoundary,
),
),
);
return groups.flat();
}
function emittedOutputs(value: unknown): readonly EmittedOutput[] {
const buildOutputs = Array.isArray(value) ? value : [value];
const emitted: EmittedOutput[] = [];
for (const buildOutput of buildOutputs) {
if (!isRecord(buildOutput) || !Array.isArray(buildOutput.output)) {
throw new TypeError("Optional recipe bundle output is invalid.");
}
for (const output of buildOutput.output) {
if (!isRecord(output)) {
throw new TypeError("Optional recipe emitted output is invalid.");
}
if (
output.type === "chunk" &&
typeof output.fileName === "string" &&
typeof output.code === "string"
) {
emitted.push({
type: "chunk",
fileName: output.fileName,
code: output.code,
});
} else if (
output.type === "asset" &&
typeof output.fileName === "string" &&
(typeof output.source === "string" ||
output.source instanceof Uint8Array)
) {
emitted.push({
type: "asset",
fileName: output.fileName,
source: output.source,
});
} else {
throw new TypeError("Optional recipe emitted output shape is invalid.");
}
}
}
return emitted;
}
function outputBytes(output: EmittedOutput): Buffer {
if (output.type === "chunk") {
return Buffer.from(output.code, "utf8");
}
return Buffer.from(output.source);
}
function validateRecipeId(value: unknown): string {
if (typeof value !== "string" || !RECIPE_ID.test(value)) {
throw new TypeError("Optional recipe ID is invalid.");
}
return value;
}
function validateSourceRoots(value: unknown): readonly string[] {
if (
!Array.isArray(value) ||
value.length === 0 ||
value.length > 32 ||
value.some(
(sourceRoot) =>
typeof sourceRoot !== "string" ||
!sourceRoot.startsWith("src/") ||
sourceRoot.includes("\\") ||
sourceRoot
.split("/")
.some(
(segment) =>
segment.length === 0 || segment === "." || segment === "..",
),
) ||
new Set(value).size !== value.length
) {
throw new TypeError("Optional recipe source roots are invalid.");
}
return Object.freeze([...value].sort(compareText));
}
function assertInsideSourceBoundary(
target: string,
sourceBoundary: string,
): void {
const relative = path.relative(sourceBoundary, target);
if (
relative === ".." ||
relative.startsWith(`..${path.sep}`) ||
path.isAbsolute(relative)
) {
throw new TypeError("Optional recipe source escaped the source boundary.");
}
}
function viteSourceSpecifier(sourceFile: string): string {
return pathToFileURL(sourceFile).href;
}
function positiveSafeInteger(value: unknown, name: string): number {
if (!Number.isSafeInteger(value) || (value as number) < 1) {
throw new TypeError(`${name} is invalid.`);
}
return value as number;
}
function compareText(left: string, right: string): number {
return left < right ? -1 : left > right ? 1 : 0;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
-265
View File
@@ -1,265 +0,0 @@
import { readFile, readdir } from "node:fs/promises";
import path from "node:path";
export const REQUIRED_RECIPE_IDS = Object.freeze([
"analytics-error-sink",
"browser-permission",
"client-workflow",
"feature-flag",
"file-transfer",
"generated-api",
"large-data-ui",
"multi-tab",
"offline-indexeddb",
"realtime",
"service-worker-pwa",
"web-worker",
]);
const lifecycleRecipes = new Set([
"analytics-error-sink",
"browser-permission",
"client-workflow",
"file-transfer",
"generated-api",
"multi-tab",
"offline-indexeddb",
"realtime",
"service-worker-pwa",
"web-worker",
]);
/** @param {unknown} value */
function nonEmptyStrings(value) {
return (
Array.isArray(value) &&
value.length > 0 &&
value.every((entry) => typeof entry === "string" && entry.trim().length > 0)
);
}
/**
* @param {unknown} input
* @param {Readonly<Record<string, unknown>>} packageDocument
* @returns {string[]}
*/
export function validateRecipeCatalog(input, packageDocument) {
const document =
/** @type {Record<string, any>} */ (
input && typeof input === "object" ? input : {}
);
/** @type {string[]} */
const violations = [];
if (document.schemaVersion !== 1) violations.push("CATALOG_SCHEMA_VERSION");
if (document.decisionId !== "VD-10") violations.push("CATALOG_DECISION");
if (document.defaultStatus !== "NOT_INSTALLED") {
violations.push("CATALOG_DEFAULT_MUST_BE_NOT_INSTALLED");
}
if (
!Array.isArray(document.productionRuntimeDependencies) ||
document.productionRuntimeDependencies.length > 0
) {
violations.push("UNSELECTED_RUNTIME_DEPENDENCY");
}
if (!nonEmptyStrings(document.vendorPackagePatterns)) {
violations.push("VENDOR_PATTERN_CATALOG");
}
if (!Array.isArray(document.recipes)) {
return [...violations, "RECIPE_CATALOG_MISSING"];
}
const actualIds = document.recipes
.map(/** @param {Record<string, unknown>} recipe */ (recipe) => recipe.id)
.sort();
if (JSON.stringify(actualIds) !== JSON.stringify(REQUIRED_RECIPE_IDS)) {
violations.push("RECIPE_ID_SET");
}
if (new Set(actualIds).size !== actualIds.length) {
violations.push("RECIPE_ID_DUPLICATE");
}
for (const recipe of document.recipes) {
const id = typeof recipe.id === "string" ? recipe.id : "unknown";
if (recipe.status !== "RECIPE_AVAILABLE") {
violations.push(`${id}:STATUS_MUST_NOT_CLAIM_INSTALLED`);
}
for (const field of [
"trigger",
"boundary",
"port",
"fake",
"owner",
"fallback",
"serverStatePolicy",
]) {
if (typeof recipe[field] !== "string" || recipe[field].trim().length === 0) {
violations.push(`${id}:MISSING_${field.toUpperCase()}`);
}
}
for (const field of [
"forbiddenWhen",
"failureKinds",
"securityPrivacy",
"removal",
]) {
if (!nonEmptyStrings(recipe[field])) {
violations.push(`${id}:MISSING_${field.toUpperCase()}`);
}
}
if (
!Number.isInteger(recipe.bundleBudgetGzipBytes) ||
recipe.bundleBudgetGzipBytes < 1
) {
violations.push(`${id}:INVALID_BUNDLE_BUDGET`);
}
if (recipe.owner === "frontend-platform") {
violations.push(`${id}:PROJECT_OWNER_NOT_ASSIGNED`);
}
if (
lifecycleRecipes.has(id) &&
!nonEmptyStrings(recipe.lifecycleMethods)
) {
violations.push(`${id}:CLEANUP_CONTRACT_MISSING`);
}
if (
id === "client-workflow" &&
recipe.serverStatePolicy !== "reference-only"
) {
violations.push(`${id}:SERVER_STATE_DUPLICATION_POLICY`);
}
}
const dependencies = {
.../** @type {Record<string, string>} */ (packageDocument.dependencies ?? {}),
.../** @type {Record<string, string>} */ (
packageDocument.devDependencies ?? {}
),
};
for (const pattern of document.vendorPackagePatterns ?? []) {
const wildcard = String(pattern).endsWith("*");
const prefix = String(pattern).replace(/\/?\*$/, "");
if (
Object.keys(dependencies).some(
(dependency) =>
dependency === prefix ||
dependency.startsWith(`${prefix}/`) ||
(wildcard && dependency.startsWith(prefix)),
)
) {
violations.push(`UNSELECTED_VENDOR_INSTALLED:${prefix}`);
}
}
return violations;
}
/** @param {string} directory @returns {Promise<string[]>} */
export async function sourceFiles(directory) {
let entries;
try {
entries = await readdir(directory, { withFileTypes: true });
} catch (error) {
if (
error &&
typeof error === "object" &&
"code" in error &&
error.code === "ENOENT"
) {
return [];
}
throw error;
}
const groups = await Promise.all(
entries.map((entry) => {
const target = path.join(directory, entry.name);
return entry.isDirectory()
? sourceFiles(target)
: /\.(?:js|jsx|mjs|ts|tsx|mts)$/.test(entry.name)
? [target]
: [];
}),
);
return groups.flat();
}
/**
* @param {string} root
* @param {{scanProductionBoundary?: boolean}} [options]
*/
export async function scanOptionalRecipeSources(
root,
{ scanProductionBoundary = true } = {},
) {
/** @type {Array<{ruleId: string; path: string}>} */
const violations = [];
for (const file of await sourceFiles(root)) {
const relative = path.relative(process.cwd(), file).replaceAll("\\", "/");
const relativeToRoot = path.relative(root, file).replaceAll("\\", "/");
const content = await readFile(file, "utf8");
const imports = [
...content.matchAll(
/(?:from\s*|import\s*\(\s*)["']([^"']+)["']/g,
),
].map((match) => match[1]);
if (
scanProductionBoundary &&
(relativeToRoot.startsWith("src/") ||
(path.basename(path.resolve(root)) === "src" &&
!relativeToRoot.startsWith(".."))) &&
imports.some((specifier) =>
/(?:^|\/)recipes\/frontend-capabilities(?:\/|$)/.test(specifier),
)
) {
violations.push({ ruleId: "PRODUCTION_IMPORTS_RECIPE", path: relative });
}
const localVendorAdapter =
relative.includes("recipes/") && relative.includes("/adapters/");
if (
!localVendorAdapter &&
imports.some((specifier) =>
/^(?:@launchdarkly\/|@sentry\/|@opentelemetry\/|@openapitools\/openapi-generator-cli$|@reduxjs\/toolkit$|@tanstack\/react-virtual$|@uppy\/|firebase(?:\/|$)|idb$|react-window$|redux(?:\/|$)|socket\.io-client$|tus-js-client$|workbox-window$|xstate$|zustand$)/.test(
specifier,
),
)
) {
violations.push({ ruleId: "VENDOR_IMPORT_OUTSIDE_ADAPTER", path: relative });
}
if (
/localStorage\s*\.\s*(?:setItem|getItem)\s*\([^)]*(?:credential|password|secret|token)/is.test(
content,
) ||
/searchParams\s*\.\s*set\s*\(\s*["'](?:credential|password|secret|token)/is.test(
content,
) ||
/(?:record|track|emit)\s*\(\s*\{[\s\S]{0,400}(?:credential|password|secret|token)\s*:/i.test(
content,
)
) {
violations.push({ ruleId: "CREDENTIAL_LEAK_PATH", path: relative });
}
if (
/(?:createStore|configureStore|create\s*\()\s*\([\s\S]{0,600}(?:apiResponse|queryData|serverState)\s*:/i.test(
content,
)
) {
violations.push({ ruleId: "CLIENT_STORE_DUPLICATES_SERVER_STATE", path: relative });
}
}
return violations;
}
/** @param {string} distRoot */
export async function scanProductionBundle(distRoot) {
/** @type {string[]} */
const violations = [];
for (const file of await sourceFiles(distRoot)) {
const content = await readFile(file, "utf8");
if (content.includes("frontend-optional-recipe-must-not-reach-production")) {
violations.push(path.relative(process.cwd(), file));
}
}
return violations;
}
+441
View File
@@ -0,0 +1,441 @@
import { readFile, readdir } from "node:fs/promises";
import path from "node:path";
export const REQUIRED_RECIPE_IDS = Object.freeze([
"analytics-error-sink",
"browser-permission",
"client-workflow",
"feature-flag",
"file-transfer",
"generated-api",
"large-data-ui",
"multi-tab",
"offline-indexeddb",
"realtime",
"service-worker-pwa",
"web-worker",
] as const);
const lifecycleRecipes: ReadonlySet<string> = new Set([
"analytics-error-sink",
"browser-permission",
"client-workflow",
"file-transfer",
"generated-api",
"multi-tab",
"offline-indexeddb",
"realtime",
"service-worker-pwa",
"web-worker",
]);
type Document = Readonly<Record<string, unknown>>;
export type OptionalRecipeSourceViolation = Readonly<{
ruleId: string;
path: string;
}>;
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function recordValue(value: unknown): Record<string, unknown> {
return isRecord(value) ? value : {};
}
function recordRows(value: unknown): Record<string, unknown>[] {
return Array.isArray(value) ? value.filter(isRecord) : [];
}
function nonEmptyStrings(value: unknown): value is string[] {
return (
Array.isArray(value) &&
value.length > 0 &&
value.every(
(entry): entry is string =>
typeof entry === "string" && entry.trim().length > 0,
)
);
}
function packageVersions(value: unknown): Record<string, string> {
return Object.fromEntries(
Object.entries(recordValue(value)).filter(
(entry): entry is [string, string] => typeof entry[1] === "string",
),
);
}
export function validateRecipeCatalog(
input: unknown,
packageDocument: Document,
): string[] {
const document = recordValue(input);
const violations: string[] = [];
if (document.schemaVersion !== 1) violations.push("CATALOG_SCHEMA_VERSION");
if (document.decisionId !== "VD-10") violations.push("CATALOG_DECISION");
if (document.defaultStatus !== "NOT_INSTALLED") {
violations.push("CATALOG_DEFAULT_MUST_BE_NOT_INSTALLED");
}
if (
!Array.isArray(document.productionRuntimeDependencies) ||
document.productionRuntimeDependencies.length > 0
) {
violations.push("UNSELECTED_RUNTIME_DEPENDENCY");
}
if (!nonEmptyStrings(document.vendorPackagePatterns)) {
violations.push("VENDOR_PATTERN_CATALOG");
}
if (!Array.isArray(document.recipes)) {
return [...violations, "RECIPE_CATALOG_MISSING"];
}
const recipes = recordRows(document.recipes);
const actualIds = recipes.map((recipe) => String(recipe.id ?? "")).sort();
if (JSON.stringify(actualIds) !== JSON.stringify(REQUIRED_RECIPE_IDS)) {
violations.push("RECIPE_ID_SET");
}
if (new Set(actualIds).size !== actualIds.length) {
violations.push("RECIPE_ID_DUPLICATE");
}
for (const recipe of recipes) {
const id = typeof recipe.id === "string" ? recipe.id : "unknown";
if (recipe.status !== "RECIPE_AVAILABLE") {
violations.push(`${id}:STATUS_MUST_NOT_CLAIM_INSTALLED`);
}
for (const field of [
"trigger",
"boundary",
"port",
"fake",
"owner",
"fallback",
"serverStatePolicy",
] as const) {
const value = recipe[field];
if (typeof value !== "string" || value.trim().length === 0) {
violations.push(`${id}:MISSING_${field.toUpperCase()}`);
}
}
for (const field of [
"forbiddenWhen",
"failureKinds",
"securityPrivacy",
"removal",
] as const) {
if (!nonEmptyStrings(recipe[field])) {
violations.push(`${id}:MISSING_${field.toUpperCase()}`);
}
}
if (
typeof recipe.bundleBudgetGzipBytes !== "number" ||
!Number.isInteger(recipe.bundleBudgetGzipBytes) ||
recipe.bundleBudgetGzipBytes < 1
) {
violations.push(`${id}:INVALID_BUNDLE_BUDGET`);
}
if (recipe.owner === "frontend-platform") {
violations.push(`${id}:PROJECT_OWNER_NOT_ASSIGNED`);
}
if (lifecycleRecipes.has(id) && !nonEmptyStrings(recipe.lifecycleMethods)) {
violations.push(`${id}:CLEANUP_CONTRACT_MISSING`);
}
if (
id === "client-workflow" &&
recipe.serverStatePolicy !== "reference-only"
) {
violations.push(`${id}:SERVER_STATE_DUPLICATION_POLICY`);
}
}
const dependencies = {
...packageVersions(packageDocument.dependencies),
...packageVersions(packageDocument.devDependencies),
};
const packageScripts = packageVersions(packageDocument.scripts);
for (const recipe of recipes) {
if (recipe.referenceRuntime === undefined) continue;
const runtime = recordValue(recipe.referenceRuntime);
const id = typeof recipe.id === "string" ? recipe.id : "unknown";
if (
runtime.status !== "AVAILABLE_NOT_COMPOSED" ||
runtime.productionComposition !== false
) {
violations.push(`${id}:REFERENCE_RUNTIME_COMPOSITION`);
}
if (!nonEmptyStrings(runtime.sourceRoots)) {
violations.push(`${id}:REFERENCE_RUNTIME_SOURCE_ROOTS`);
} else if (
runtime.sourceRoots.some(
(sourceRoot) =>
!sourceRoot.startsWith("src/") ||
sourceRoot.includes("\\") ||
sourceRoot.split("/").includes(".."),
)
) {
violations.push(`${id}:REFERENCE_RUNTIME_SOURCE_BOUNDARY`);
}
if (!nonEmptyStrings(runtime.coveredCapabilities)) {
violations.push(`${id}:REFERENCE_RUNTIME_CAPABILITIES`);
}
if (!nonEmptyStrings(runtime.conformanceScripts)) {
violations.push(`${id}:REFERENCE_RUNTIME_CONFORMANCE`);
} else {
for (const script of runtime.conformanceScripts) {
if (!(script in packageScripts)) {
violations.push(`${id}:UNKNOWN_CONFORMANCE_SCRIPT:${script}`);
}
}
}
}
const vendorPatterns = Array.isArray(document.vendorPackagePatterns)
? document.vendorPackagePatterns.filter(
(entry): entry is string => typeof entry === "string",
)
: [];
for (const pattern of vendorPatterns) {
const wildcard = pattern.endsWith("*");
const prefix = pattern.replace(/\/?\*$/, "");
if (
Object.keys(dependencies).some(
(dependency) =>
dependency === prefix ||
dependency.startsWith(`${prefix}/`) ||
(wildcard && dependency.startsWith(prefix)),
)
) {
violations.push(`UNSELECTED_VENDOR_INSTALLED:${prefix}`);
}
}
return violations;
}
export async function sourceFiles(directory: string): Promise<string[]> {
let entries;
try {
entries = await readdir(directory, { withFileTypes: true });
} catch (error: unknown) {
if (
isRecord(error) &&
"code" in error &&
error.code === "ENOENT"
) {
return [];
}
throw error;
}
const groups: string[][] = await Promise.all(
entries.map((entry) => {
const target = path.join(directory, entry.name);
return entry.isDirectory()
? sourceFiles(target)
: /\.(?:[cm]?[jt]s|[jt]sx)$/.test(entry.name)
? [target]
: [];
}),
);
return groups.flat();
}
export async function scanOptionalRecipeSources(
root: string,
{ scanProductionBoundary = true }: Readonly<{
scanProductionBoundary?: boolean;
}> = {},
): Promise<OptionalRecipeSourceViolation[]> {
const violations: OptionalRecipeSourceViolation[] = [];
for (const file of await sourceFiles(root)) {
const relative = path.relative(process.cwd(), file).replaceAll("\\", "/");
const relativeToRoot = path.relative(root, file).replaceAll("\\", "/");
const content = await readFile(file, "utf8");
const imports = [
...content.matchAll(/(?:from\s*|import\s*\(\s*)["']([^"']+)["']/g),
]
.map((match) => match[1])
.filter((specifier): specifier is string => specifier !== undefined);
if (
scanProductionBoundary &&
(relativeToRoot.startsWith("src/") ||
(path.basename(path.resolve(root)) === "src" &&
!relativeToRoot.startsWith(".."))) &&
imports.some((specifier) =>
/(?:^|\/)recipes\/frontend-capabilities(?:\/|$)/.test(specifier),
)
) {
violations.push({ ruleId: "PRODUCTION_IMPORTS_RECIPE", path: relative });
}
const productionRelative =
path.basename(path.resolve(root)) === "src"
? `src/${relativeToRoot}`
: relativeToRoot;
const isCompositionSource =
/(?:^|\/)src\/bootstrap\//.test(productionRelative) ||
/(?:^|\/)src\/features\/installed-feature-(?:adapters|runtimes)\./.test(
productionRelative,
);
if (
scanProductionBoundary &&
isCompositionSource &&
(imports.some((specifier) =>
/(?:^|\/)adapters\/(?:browser-file-storage|browser-files|browser-transfer|cache-storage|storage\/(?:indexeddb|opfs))(?:\/|$)/.test(
specifier,
),
) ||
/["'][^"'\r\n]*(?:^|\/)adapters\/(?:browser-file-storage|browser-files|browser-transfer|cache-storage|storage\/(?:indexeddb|opfs))(?:\/|["'])/u.test(
content,
))
) {
violations.push({
ruleId: "REFERENCE_RUNTIME_COMPOSED_WITHOUT_SELECTION",
path: relative,
});
}
const localVendorAdapter =
relative.includes("recipes/") && relative.includes("/adapters/");
if (
!localVendorAdapter &&
imports.some((specifier) =>
/^(?:@launchdarkly\/|@sentry\/|@opentelemetry\/|@openapitools\/openapi-generator-cli$|@reduxjs\/toolkit$|@tanstack\/react-virtual$|@uppy\/|firebase(?:\/|$)|idb$|react-window$|redux(?:\/|$)|socket\.io-client$|tus-js-client$|workbox-window$|xstate$|zustand$)/.test(
specifier,
),
)
) {
violations.push({ ruleId: "VENDOR_IMPORT_OUTSIDE_ADAPTER", path: relative });
}
if (
/localStorage\s*\.\s*(?:setItem|getItem)\s*\([^)]*(?:credential|password|secret|token)/is.test(
content,
) ||
/searchParams\s*\.\s*set\s*\(\s*["'](?:credential|password|secret|token)/is.test(
content,
) ||
/(?:record|track|emit)\s*\(\s*\{[\s\S]{0,400}(?:credential|password|secret|token)\s*:/i.test(
content,
)
) {
violations.push({ ruleId: "CREDENTIAL_LEAK_PATH", path: relative });
}
if (
/(?:createStore|configureStore|create\s*\()\s*\([\s\S]{0,600}(?:apiResponse|queryData|serverState)\s*:/i.test(
content,
)
) {
violations.push({
ruleId: "CLIENT_STORE_DUPLICATES_SERVER_STATE",
path: relative,
});
}
}
return violations;
}
export async function scanProductionBundle(
distRoot: string,
): Promise<string[]> {
const violations: string[] = [];
const forbiddenRuntimeMarkers = [
"frontend-optional-recipe-must-not-reach-production",
"Browser file runtime hard limits are invalid.",
"Object URL allocation failed",
"Storage pressure policy is invalid.",
"IndexedDB runtime configuration is invalid.",
"Invalid IndexedDB schema migration.",
"OPFS runtime policy is invalid.",
"OPFS operation failed.",
"Public Cache Storage policy is invalid.",
"Public cache validation failed.",
"Presigned capability vault limit is invalid.",
"Resumable upload policy is invalid.",
"Image CDN policy registry is invalid.",
] as const;
for (const file of await sourceFiles(distRoot)) {
const content = await readFile(file, "utf8");
if (forbiddenRuntimeMarkers.some((marker) => content.includes(marker))) {
violations.push(path.relative(process.cwd(), file));
}
}
const viteManifestPath = path.join(distRoot, ".vite/manifest.json");
const emittedModuleInventoryPath = path.join(
distRoot,
".vite/module-inventory.json",
);
const moduleInventoryCandidates = [
emittedModuleInventoryPath,
...(path.resolve(distRoot) === path.resolve("dist")
? ["artifacts/quality/vite-module-inventory.json"]
: []),
];
const viteManifestExists = await readFile(viteManifestPath, "utf8")
.then(() => true)
.catch((error: unknown) => {
if (isRecord(error) && error.code === "ENOENT") return false;
throw error;
});
if (!viteManifestExists) return [...new Set(violations)];
let inventory: unknown;
let moduleInventoryPath = emittedModuleInventoryPath;
for (const candidate of moduleInventoryCandidates) {
try {
inventory = JSON.parse(await readFile(candidate, "utf8"));
moduleInventoryPath = candidate;
break;
} catch {
// A generated build may move the inventory out of the deploy directory.
}
}
if (inventory === undefined) {
violations.push(
path.relative(process.cwd(), moduleInventoryPath),
);
return [...new Set(violations)];
}
const inventoryDocument = recordValue(inventory);
const chunks = recordRows(inventoryDocument.chunks);
if (
inventoryDocument.schemaVersion !== 1 ||
!Array.isArray(inventoryDocument.chunks) ||
chunks.length !== inventoryDocument.chunks.length
) {
violations.push(path.relative(process.cwd(), moduleInventoryPath));
return [...new Set(violations)];
}
const forbiddenSourcePrefixes = [
"src/application/ports/browser-file-storage/",
"src/application/ports/browser-transfer/",
"src/adapters/browser-file-storage/",
"src/adapters/browser-files/",
"src/adapters/browser-transfer/",
"src/adapters/cache-storage/",
"src/adapters/storage/indexeddb/",
"src/adapters/storage/opfs/",
] as const;
for (const chunk of chunks) {
if (
typeof chunk.fileName !== "string" ||
!Array.isArray(chunk.modules) ||
chunk.modules.some((moduleId) => typeof moduleId !== "string")
) {
violations.push(path.relative(process.cwd(), moduleInventoryPath));
continue;
}
for (const moduleId of chunk.modules as string[]) {
if (
forbiddenSourcePrefixes.some((prefix) =>
moduleId.startsWith(prefix),
)
) {
violations.push(`${chunk.fileName}:${moduleId}`);
}
}
}
return [...new Set(violations)];
}
+138
View File
@@ -0,0 +1,138 @@
import { readFile, readdir } from "node:fs/promises";
import path from "node:path";
export type RealtimeBoundaryRuleId =
| "NATIVE_REALTIME_API_OUTSIDE_ADAPTER"
| "PRESENTATION_INTERVAL_OWNER"
| "UNSELECTED_REALTIME_RUNTIME_COMPOSED";
export type RealtimeBoundaryViolation = Readonly<{
ruleId: RealtimeBoundaryRuleId;
file: string;
line: number;
}>;
const SOURCE_EXTENSION = /\.(?:[cm]?ts|tsx)$/u;
const OWNED_NATIVE_ROOTS = [
"src/adapters/realtime/",
"src/adapters/web-push/",
] as const;
const REALTIME_ADAPTER_IMPORT =
/(?:from\s*|import\s*\()\s*["'][^"']*\/adapters\/(?:realtime|web-push)(?:\/[^"']*)?["']/gu;
const NATIVE_REALTIME_PATTERNS = [
/\bnew\s+(?:WebSocket|EventSource|Notification)\s*\(/gu,
/\bNotification\s*\.\s*requestPermission\s*\(/gu,
/\.\s*showNotification\s*\(/gu,
/\.\s*pushManager\s*\.\s*(?:subscribe|getSubscription)\s*\(/gu,
/\bReflect\s*\.\s*get\s*\([^,]+,\s*["'](?:WebSocket|EventSource|Notification|pushManager)["']/gu,
] as const;
const PRESENTATION_INTERVAL = /\bsetInterval\s*\(/gu;
export async function scanRealtimeBoundaries(
sourceRoot: string,
): Promise<readonly RealtimeBoundaryViolation[]> {
const absoluteRoot = path.resolve(sourceRoot);
const files = await collectSourceFiles(absoluteRoot);
const violations: RealtimeBoundaryViolation[] = [];
for (const file of files) {
const source = await readFile(file, "utf8");
const logicalFile = logicalSourcePath(absoluteRoot, file);
inspectFile(source, logicalFile, violations);
}
return Object.freeze(
violations
.sort(
(left, right) =>
left.file.localeCompare(right.file) ||
left.line - right.line ||
left.ruleId.localeCompare(right.ruleId),
)
.map((violation) => Object.freeze(violation)),
);
}
function inspectFile(
source: string,
logicalFile: string,
violations: RealtimeBoundaryViolation[],
): void {
const nativeOwned = OWNED_NATIVE_ROOTS.some((root) =>
logicalFile.startsWith(root),
);
const presentationOwned =
logicalFile.startsWith("src/presentation/") ||
/^src\/features\/[^/]+\/presentation\//u.test(logicalFile);
const compositionBoundary =
logicalFile.startsWith("src/bootstrap/") ||
/^src\/features\/installed-feature-/u.test(logicalFile);
const report = (
ruleId: RealtimeBoundaryRuleId,
index: number,
): void => {
violations.push({
ruleId,
file: logicalFile,
line: lineAt(source, index),
});
};
if (!nativeOwned) {
for (const pattern of NATIVE_REALTIME_PATTERNS) {
for (const match of source.matchAll(pattern)) {
report(
"NATIVE_REALTIME_API_OUTSIDE_ADAPTER",
match.index,
);
}
}
}
if (presentationOwned) {
for (const match of source.matchAll(PRESENTATION_INTERVAL)) {
report("PRESENTATION_INTERVAL_OWNER", match.index);
}
}
if (compositionBoundary) {
for (const match of source.matchAll(REALTIME_ADAPTER_IMPORT)) {
report("UNSELECTED_REALTIME_RUNTIME_COMPOSED", match.index);
}
}
}
async function collectSourceFiles(
directory: string,
): Promise<readonly string[]> {
const output: string[] = [];
const entries = await readdir(directory, { withFileTypes: true });
for (const entry of entries) {
const resolved = path.join(directory, entry.name);
if (
entry.isDirectory() &&
!["node_modules", "dist", "artifacts", ".tmp"].includes(
entry.name,
)
) {
output.push(...(await collectSourceFiles(resolved)));
} else if (entry.isFile() && SOURCE_EXTENSION.test(entry.name)) {
output.push(resolved);
}
}
return output;
}
function logicalSourcePath(root: string, file: string): string {
const workspaceRelative = path
.relative(process.cwd(), file)
.split(path.sep)
.join("/");
if (root === path.resolve("src")) return workspaceRelative;
return path.relative(root, file).split(path.sep).join("/");
}
function lineAt(source: string, index: number): number {
let line = 1;
for (let offset = 0; offset < index; offset += 1) {
if (source.charCodeAt(offset) === 10) line += 1;
}
return line;
}
@@ -5,17 +5,36 @@ export const COMPATIBILITY_IMPACTS = Object.freeze([
"additive",
"behavior-change",
"breaking",
]);
] as const);
type CompatibilityImpact = (typeof COMPATIBILITY_IMPACTS)[number];
type RegistryRecord = Record<string, unknown> & {
registryId?: unknown;
contract?: unknown;
rows?: unknown;
};
type RegistryChange = {
changeId: string;
registryId: string;
rowName: string;
field: string;
kind: string;
impact: CompatibilityImpact;
before?: unknown;
after?: unknown;
};
type RegistryDiff = Readonly<{
impact: CompatibilityImpact;
changes: readonly RegistryChange[];
}>;
const impactRank = new Map(
COMPATIBILITY_IMPACTS.map((impact, index) => [impact, index]),
);
/** @param {unknown} value @returns {unknown} */
export function canonicalizeRegistryValue(value) {
export function canonicalizeRegistryValue(value: unknown): unknown {
if (Array.isArray(value)) {
const projected =
/** @type {unknown[]} */ (value.map(canonicalizeRegistryValue));
const projected: unknown[] = value.map(canonicalizeRegistryValue);
return projected.every(
(item) =>
item === null ||
@@ -36,64 +55,64 @@ export function canonicalizeRegistryValue(value) {
return value;
}
/** @param {unknown} value @returns {string} */
export function canonicalRegistryJson(value) {
export function canonicalRegistryJson(value: unknown): string {
return JSON.stringify(canonicalizeRegistryValue(value)) ?? "undefined";
}
/** @param {unknown} snapshot */
export function registrySnapshotDigest(snapshot) {
export function registrySnapshotDigest(snapshot: unknown): string {
return createHash("sha256")
.update(canonicalRegistryJson(snapshot))
.digest("hex");
}
/** @param {string} current @param {string} candidate */
function strongestImpact(current, candidate) {
function strongestImpact(
current: CompatibilityImpact,
candidate: CompatibilityImpact,
): CompatibilityImpact {
return (impactRank.get(candidate) ?? 0) > (impactRank.get(current) ?? 0)
? candidate
: current;
}
/** @param {unknown} value */
function valueType(value) {
function valueType(value: unknown): string {
if (value === null) return "null";
if (Array.isArray(value)) return "array";
return typeof value;
}
/**
* @param {string} registryId
* @param {string} rowName
* @param {string} field
* @param {string} kind
*/
function changeId(registryId, rowName, field, kind) {
function changeId(
registryId: string,
rowName: string,
field: string,
kind: string,
): string {
return `${registryId}:${rowName}:${field}:${kind}`;
}
/**
* Calculates a semantic diff. Object key and primitive-array ordering is
* canonicalized before comparison and therefore cannot create a false change.
*
* @param {Readonly<Record<string, unknown>>} before
* @param {Readonly<Record<string, unknown>>} after
*/
export function diffRegistrySnapshots(before, after) {
const changes = /** @type {Array<Record<string, unknown>>} */ ([]);
let impact = "none";
const beforeRegistries =
/** @type {Map<string, Record<string, unknown>>} */ (new Map(
/** @type {Array<Record<string, unknown>>} */ (before.registries ?? []).map(
(registry) => [String(registry.registryId), registry],
),
));
const afterRegistries =
/** @type {Map<string, Record<string, unknown>>} */ (new Map(
/** @type {Array<Record<string, unknown>>} */ (after.registries ?? []).map(
(registry) => [String(registry.registryId), registry],
),
));
export function diffRegistrySnapshots(
before: Readonly<Record<string, unknown>>,
after: Readonly<Record<string, unknown>>,
): RegistryDiff {
const changes: RegistryChange[] = [];
let impact: CompatibilityImpact = "none";
const beforeRegistryRows = (before.registries ?? []) as RegistryRecord[];
const beforeRegistries = new Map<string, RegistryRecord>(
beforeRegistryRows.map((registry) => [
String(registry.registryId),
registry,
]),
);
const afterRegistryRows = (after.registries ?? []) as RegistryRecord[];
const afterRegistries = new Map<string, RegistryRecord>(
afterRegistryRows.map((registry) => [
String(registry.registryId),
registry,
]),
);
const registryIds = new Set([
...beforeRegistries.keys(),
...afterRegistries.keys(),
@@ -116,10 +135,8 @@ export function diffRegistrySnapshots(before, after) {
continue;
}
const previousContract =
/** @type {Record<string, unknown>} */ (previous.contract ?? {});
const currentContract =
/** @type {Record<string, unknown>} */ (current.contract ?? {});
const previousContract = (previous.contract ?? {}) as Record<string, unknown>;
const currentContract = (current.contract ?? {}) as Record<string, unknown>;
const contractFields = new Set([
...Object.keys(previousContract),
...Object.keys(currentContract),
@@ -156,16 +173,16 @@ export function diffRegistrySnapshots(before, after) {
}
const breakingFields = new Set(
/** @type {string[]} */ (
currentContract.breakingFields ?? []
),
(currentContract.breakingFields ?? []) as string[],
);
const beforeRows =
/** @type {Record<string, Record<string, unknown>>} */ (
previous.rows ?? {}
);
const afterRows =
/** @type {Record<string, Record<string, unknown>>} */ (current.rows ?? {});
const beforeRows = (previous.rows ?? {}) as Record<
string,
Record<string, unknown>
>;
const afterRows = (current.rows ?? {}) as Record<
string,
Record<string, unknown>
>;
const rowNames = new Set([
...Object.keys(beforeRows),
...Object.keys(afterRows),
@@ -209,8 +226,8 @@ export function diffRegistrySnapshots(before, after) {
) {
continue;
}
let kind;
let changeImpact;
let kind: string;
let changeImpact: CompatibilityImpact;
if (!beforeHas) {
kind = "field-added";
changeImpact = "additive";
@@ -261,11 +278,10 @@ export function diffRegistrySnapshots(before, after) {
});
}
/**
* @param {Readonly<Record<string, unknown>>} snapshot
* @param {Readonly<Record<string, unknown>>} approval
*/
export function verifyRegistryBaselineApproval(snapshot, approval) {
export function verifyRegistryBaselineApproval(
snapshot: Readonly<Record<string, unknown>>,
approval: Readonly<Record<string, unknown>>,
) {
const actualDigest = registrySnapshotDigest(snapshot);
const approvedDigest = approval.snapshotDigest;
return Object.freeze({
@@ -281,17 +297,15 @@ export function verifyRegistryBaselineApproval(snapshot, approval) {
});
}
/**
* @param {ReturnType<typeof diffRegistrySnapshots>} diff
* @param {Readonly<Record<string, unknown>>} evidenceFile
*/
export function validateBreakingEvidence(diff, evidenceFile) {
const evidence = new Map(
/** @type {Array<Record<string, unknown>>} */ (
evidenceFile.changes ?? []
).map((entry) => [entry.changeId, entry]),
export function validateBreakingEvidence(
diff: RegistryDiff,
evidenceFile: Readonly<Record<string, unknown>>,
) {
const entries = (evidenceFile.changes ?? []) as Array<Record<string, unknown>>;
const evidence = new Map<string, Record<string, unknown>>(
entries.map((entry) => [String(entry.changeId), entry]),
);
const failures = [];
const failures: string[] = [];
for (const change of diff.changes.filter(
(entry) => entry.impact === "breaking",
)) {
@@ -307,7 +321,8 @@ export function validateBreakingEvidence(diff, evidenceFile) {
"rollback",
"owner",
]) {
if (typeof entry[field] !== "string" || entry[field].trim().length === 0) {
const value = entry[field];
if (typeof value !== "string" || value.trim().length === 0) {
failures.push(
`breaking change ${change.changeId} missing non-empty ${field}`,
);
-547
View File
@@ -1,547 +0,0 @@
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
/** @param {unknown} value @returns {unknown} */
export function canonicalizeSupplyChainValue(value) {
if (Array.isArray(value)) {
return value
.map(canonicalizeSupplyChainValue)
.sort((left, right) =>
JSON.stringify(left).localeCompare(JSON.stringify(right)),
);
}
if (value && typeof value === "object") {
return Object.fromEntries(
Object.entries(value)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, item]) => [key, canonicalizeSupplyChainValue(item)]),
);
}
return value;
}
/** @param {unknown} value */
export function supplyChainDigest(value) {
return createHash("sha256")
.update(JSON.stringify(canonicalizeSupplyChainValue(value)))
.digest("hex");
}
/** @param {string} lockfile */
export function parsePnpmLockfilePackages(lockfile) {
const entries =
/** @type {Array<{name: string, version: string, integrity: string}>} */ (
[]
);
let inPackages = false;
/** @type {{name: string, version: string, integrity: string} | null} */
let current = null;
for (const line of lockfile.split(/\r?\n/)) {
if (line === "packages:") {
inPackages = true;
continue;
}
if (line === "snapshots:") {
if (current) entries.push(current);
break;
}
if (!inPackages) continue;
const packageMatch = line.match(/^ {2}(\S.*):$/);
if (packageMatch) {
if (current) entries.push(current);
const key = packageMatch[1].replace(/^['"]|['"]$/g, "");
const separator = key.lastIndexOf("@");
current = {
name: key.slice(0, separator),
version: key.slice(separator + 1),
integrity: "",
};
continue;
}
const integrityMatch = line.match(/\bintegrity:\s*([^,}\s]+)/);
if (current && integrityMatch) {
current.integrity = integrityMatch[1];
}
}
return entries.sort((left, right) =>
`${left.name}@${left.version}`.localeCompare(
`${right.name}@${right.version}`,
),
);
}
/** @param {string} integrity */
export function isValidSha512Integrity(integrity) {
if (!integrity.startsWith("sha512-")) return false;
try {
return Buffer.from(integrity.slice("sha512-".length), "base64").length === 64;
} catch {
return false;
}
}
/**
* @param {unknown} raw
* @returns {string}
*/
export function normalizeLicense(raw) {
if (typeof raw === "string" && raw.trim()) return raw.trim();
if (
raw &&
typeof raw === "object" &&
"type" in raw &&
typeof raw.type === "string"
) {
return raw.type;
}
if (Array.isArray(raw)) {
const licenses = raw.map(normalizeLicense).filter(
(license) => license !== "NOASSERTION",
);
return licenses.length > 0 ? licenses.join(" OR ") : "NOASSERTION";
}
return "NOASSERTION";
}
/**
* @param {Record<string, unknown>} root
* @param {Readonly<Record<string, string>>} directProduction
* @param {Readonly<Record<string, string>>} directDevelopment
*/
export async function flattenPnpmDependencyTree(
root,
directProduction,
directDevelopment,
) {
const records =
/** @type {Map<string, {
* name: string,
* version: string,
* direct: boolean,
* scope: "production" | "development",
* optional: boolean,
* packagePath: string,
* dependencies: Set<string>
* }>} */ (new Map());
const directIds = new Set();
for (const [name, rawDependency] of Object.entries(
/** @type {Record<string, unknown>} */ (root.dependencies ?? {}),
)) {
if (
Object.hasOwn(directProduction, name) &&
rawDependency &&
typeof rawDependency === "object" &&
!Array.isArray(rawDependency)
) {
directIds.add(
`${name}@${String(
/** @type {Record<string, unknown>} */ (rawDependency).version ?? "",
)}`,
);
}
}
for (const [name, rawDependency] of Object.entries(
/** @type {Record<string, unknown>} */ (root.devDependencies ?? {}),
)) {
if (
Object.hasOwn(directDevelopment, name) &&
rawDependency &&
typeof rawDependency === "object" &&
!Array.isArray(rawDependency)
) {
directIds.add(
`${name}@${String(
/** @type {Record<string, unknown>} */ (rawDependency).version ?? "",
)}`,
);
}
}
/**
* @param {Record<string, unknown>} node
* @param {"production" | "development"} scope
* @param {boolean} optionalPath
*/
function visit(node, scope, optionalPath) {
for (const [groupName, group] of Object.entries({
dependencies: node.dependencies,
devDependencies: node.devDependencies,
optionalDependencies: node.optionalDependencies,
})) {
if (!group || typeof group !== "object" || Array.isArray(group)) continue;
for (const [name, rawDependency] of Object.entries(group)) {
if (
!rawDependency ||
typeof rawDependency !== "object" ||
Array.isArray(rawDependency)
) {
continue;
}
const dependency =
/** @type {Record<string, unknown>} */ (rawDependency);
const version = String(dependency.version ?? "");
const packagePath = String(dependency.path ?? "");
const identity = `${name}@${version}`;
const childScope =
scope === "production" && groupName !== "devDependencies"
? "production"
: "development";
const childOptional =
optionalPath || groupName === "optionalDependencies";
const previous = records.get(identity);
const dependencies = previous?.dependencies ?? new Set();
for (const childGroup of [
dependency.dependencies,
dependency.optionalDependencies,
]) {
if (
!childGroup ||
typeof childGroup !== "object" ||
Array.isArray(childGroup)
) {
continue;
}
for (const [childName, rawChild] of Object.entries(childGroup)) {
if (
rawChild &&
typeof rawChild === "object" &&
!Array.isArray(rawChild)
) {
dependencies.add(
`${childName}@${String(rawChild.version ?? "")}`,
);
}
}
}
records.set(identity, {
name,
version,
direct: directIds.has(identity),
scope:
previous?.scope === "production" || childScope === "production"
? "production"
: "development",
optional: previous ? previous.optional && childOptional : childOptional,
packagePath: previous?.packagePath || packagePath,
dependencies,
});
visit(dependency, childScope, childOptional);
}
}
}
const productionRoot = {
dependencies: Object.fromEntries(
Object.entries(
/** @type {Record<string, unknown>} */ (root.dependencies ?? {}),
).filter(([name]) => Object.hasOwn(directProduction, name)),
),
};
const developmentRoot = {
devDependencies: Object.fromEntries(
Object.entries(
/** @type {Record<string, unknown>} */ (root.devDependencies ?? {}),
).filter(([name]) => Object.hasOwn(directDevelopment, name)),
),
};
visit(productionRoot, "production", false);
visit(developmentRoot, "development", false);
const result = [];
for (const record of records.values()) {
let license = "NOASSERTION";
let optional = record.optional;
if (record.packagePath) {
try {
const manifest = JSON.parse(
await readFile(`${record.packagePath}/package.json`, "utf8"),
);
license = normalizeLicense(manifest.license ?? manifest.licenses);
} catch {
// Platform-specific optional packages may not be materialized locally.
optional = true;
}
}
result.push({
name: record.name,
version: record.version,
direct: record.direct,
scope: record.scope,
optional,
license,
dependencies: [...record.dependencies].sort(),
});
}
return result.sort((left, right) =>
`${left.name}@${left.version}`.localeCompare(
`${right.name}@${right.version}`,
),
);
}
/**
* @param {Readonly<Record<string, unknown>>} before
* @param {Readonly<Record<string, unknown>>} after
*/
export function diffDependencyInventories(before, after) {
const beforeRows =
/** @type {Array<Record<string, unknown>>} */ (before.dependencies ?? []);
const afterRows =
/** @type {Array<Record<string, unknown>>} */ (after.dependencies ?? []);
const beforeMap = new Map(
beforeRows.map((row) => [`${row.name}@${row.version}`, row]),
);
const afterMap = new Map(
afterRows.map((row) => [`${row.name}@${row.version}`, row]),
);
const added = [...afterMap.keys()].filter((key) => !beforeMap.has(key));
const removed = [...beforeMap.keys()].filter((key) => !afterMap.has(key));
const changed = [];
for (const key of [...beforeMap.keys()].filter((item) => afterMap.has(item))) {
if (
supplyChainDigest(beforeMap.get(key)) !==
supplyChainDigest(afterMap.get(key))
) {
changed.push(key);
}
}
const upgrades = [];
for (const removedKey of removed) {
const previous = beforeMap.get(removedKey);
const replacement = added.find(
(addedKey) => afterMap.get(addedKey)?.name === previous?.name,
);
if (replacement) {
upgrades.push({
name: previous?.name,
from: previous?.version,
to: afterMap.get(replacement)?.version,
});
}
}
return Object.freeze({
added: Object.freeze(added.sort()),
removed: Object.freeze(removed.sort()),
changed: Object.freeze(changed.sort()),
upgrades: Object.freeze(
upgrades.sort((left, right) =>
String(left.name).localeCompare(String(right.name)),
),
),
});
}
/**
* @param {Readonly<Record<string, unknown>>} inventory
* @param {Readonly<Record<string, unknown>>} policy
*/
export function validateLicensePolicy(inventory, policy) {
const allowed = new Set(
/** @type {string[]} */ (policy.allowedLicenses ?? []),
);
const denied = /** @type {string[]} */ (policy.deniedLicensePatterns ?? []);
const failures = [];
const results = [];
for (const dependency of /** @type {Array<Record<string, unknown>>} */ (
inventory.dependencies ?? []
)) {
const license = String(dependency.license ?? "NOASSERTION");
const explicitlyDenied = denied.some((pattern) =>
new RegExp(pattern, "i").test(license),
);
const unknownAccepted =
license === "NOASSERTION" && dependency.optional === true;
const passed =
!explicitlyDenied && (allowed.has(license) || unknownAccepted);
results.push({
package: `${dependency.name}@${dependency.version}`,
license,
passed,
reason: unknownAccepted ? "platform-optional-not-materialized" : null,
});
if (!passed) {
failures.push(
`${dependency.name}@${dependency.version} has disallowed license ${license}`,
);
}
}
return Object.freeze({
passed: failures.length === 0,
failures: Object.freeze(failures),
results: Object.freeze(results),
});
}
/**
* @param {ReturnType<typeof diffDependencyInventories>} diff
* @param {Readonly<Record<string, unknown>>} inventory
* @param {Readonly<Record<string, unknown>>} evidenceFile
*/
export function validateDependencyReview(diff, inventory, evidenceFile) {
const rows =
/** @type {Array<Record<string, unknown>>} */ (inventory.dependencies ?? []);
const byIdentity = new Map(
rows.map((row) => [`${row.name}@${row.version}`, row]),
);
const evidence = new Map(
/** @type {Array<Record<string, unknown>>} */ (
evidenceFile.changes ?? []
).map((entry) => [entry.changeId, entry]),
);
const highRisk = diff.added.filter((identity) => {
const row = byIdentity.get(identity);
return row?.direct === true && row.scope === "production";
});
const failures = [];
for (const identity of highRisk) {
const changeId = `add:${identity}`;
const entry = evidence.get(changeId);
if (!entry) {
failures.push(`high-risk dependency missing review: ${changeId}`);
continue;
}
for (const field of ["owner", "reviewer", "reason", "rollback"]) {
if (typeof entry[field] !== "string" || !entry[field].trim()) {
failures.push(`${changeId} missing ${field}`);
}
}
if (entry.owner === entry.reviewer) {
failures.push(`${changeId} may not be self-approved`);
}
}
return Object.freeze({
passed: failures.length === 0,
highRisk: Object.freeze(highRisk),
failures: Object.freeze(failures),
});
}
const severityRank = new Map([
["unknown", 0],
["low", 1],
["moderate", 2],
["high", 3],
["critical", 4],
]);
/**
* @param {Readonly<Record<string, unknown>>} report
* @param {Readonly<Record<string, unknown>>} policy
* @param {Readonly<Record<string, unknown>>} exceptionFile
* @param {string} lockfileSha256
* @param {Date} [now]
*/
export function validateVulnerabilityReport(
report,
policy,
exceptionFile,
lockfileSha256,
now = new Date(),
) {
const failures = [];
if (report.scannedLockfileSha256 !== lockfileSha256) {
failures.push("vulnerability report lockfile digest mismatch");
}
if (typeof report.provider !== "string" || !report.provider.trim()) {
failures.push("vulnerability report provider missing");
}
const threshold = severityRank.get(String(policy.blockAtSeverity)) ?? 3;
const exceptions =
/** @type {Array<Record<string, unknown>>} */ (
exceptionFile.exceptions ?? []
);
const blocking = [];
for (const finding of /** @type {Array<Record<string, unknown>>} */ (
report.findings ?? []
)) {
const severity = String(finding.severity ?? "unknown").toLowerCase();
if ((severityRank.get(severity) ?? 0) < threshold) continue;
const exception = exceptions.find(
(entry) =>
entry.vulnerabilityId === finding.id &&
entry.packageName === finding.packageName,
);
const expiry =
typeof exception?.expiresAt === "string"
? Date.parse(exception.expiresAt)
: Number.NaN;
const validException =
exception &&
typeof exception.owner === "string" &&
exception.owner.trim() &&
typeof exception.reviewer === "string" &&
exception.reviewer.trim() &&
exception.owner !== exception.reviewer &&
typeof exception.reason === "string" &&
exception.reason.trim() &&
Number.isFinite(expiry) &&
expiry > now.getTime();
if (!validException) {
blocking.push(
`${finding.id}:${finding.packageName}@${finding.version}:${severity}`,
);
}
}
return Object.freeze({
passed: failures.length === 0 && blocking.length === 0,
failures: Object.freeze(failures),
blocking: Object.freeze(blocking),
});
}
/**
* @param {Readonly<Record<string, unknown>>} sbom
* @param {Readonly<Record<string, unknown>>} inventory
* @param {Readonly<Record<string, unknown>>} provenance
* @param {string} distDigest
*/
export function verifySupplyChainCoherence(
sbom,
inventory,
provenance,
distDigest,
) {
const failures = [];
const componentCount = Array.isArray(sbom.components)
? sbom.components.length
: -1;
const dependencyCount = Array.isArray(inventory.dependencies)
? inventory.dependencies.length
: -2;
if (componentCount !== dependencyCount) {
failures.push("SBOM component count does not match inventory");
}
const metadata =
/** @type {Record<string, unknown>} */ (sbom.metadata ?? {});
const properties =
/** @type {Array<{name?: string, value?: string}>} */ (
metadata.properties ?? []
);
if (properties.find(
/** @param {{name?: string, value?: string}} property */
(property) =>
property.name === "ca:lockfileSha256" &&
property.value === inventory.lockfileSha256,
) === undefined) {
failures.push("SBOM lockfile digest does not match inventory");
}
const subject =
/** @type {Array<Record<string, unknown>>} */ (provenance.subject ?? [])[0];
const subjectDigest =
/** @type {Record<string, unknown>} */ (subject?.digest ?? {});
if (subjectDigest.sha256 !== distDigest) {
failures.push("provenance subject does not match built dist digest");
}
const predicate =
/** @type {Record<string, unknown>} */ (provenance.predicate ?? {});
const materials =
/** @type {Record<string, unknown>} */ (predicate.materials ?? {});
if (materials.lockfileSha256 !== inventory.lockfileSha256) {
failures.push("provenance lockfile material does not match inventory");
}
return Object.freeze({
passed: failures.length === 0,
failures: Object.freeze(failures),
});
}
+514
View File
@@ -0,0 +1,514 @@
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
export type DependencyScope = "production" | "development";
export type LockfilePackage = Readonly<{
name: string;
version: string;
integrity: string;
}>;
export type DependencyInventoryRow = Readonly<{
name: string;
version: string;
direct: boolean;
scope: DependencyScope;
optional: boolean;
license: string;
dependencies: readonly string[];
}>;
export type DependencyUpgrade = Readonly<{
name: string;
from: string;
to: string;
}>;
export type DependencyInventoryDiff = Readonly<{
added: readonly string[];
removed: readonly string[];
changed: readonly string[];
upgrades: readonly DependencyUpgrade[];
}>;
type MutableDependencyRecord = {
name: string;
version: string;
direct: boolean;
scope: DependencyScope;
optional: boolean;
packagePath: string;
dependencies: Set<string>;
};
type Document = Readonly<Record<string, unknown>>;
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function recordValue(value: unknown): Record<string, unknown> {
return isRecord(value) ? value : {};
}
function recordRows(value: unknown): Record<string, unknown>[] {
return Array.isArray(value) ? value.filter(isRecord) : [];
}
function stringRows(value: unknown): string[] {
return Array.isArray(value)
? value.filter((entry): entry is string => typeof entry === "string")
: [];
}
function dependencyIdentity(row: Readonly<Record<string, unknown>>): string {
return `${String(row.name ?? "")}@${String(row.version ?? "")}`;
}
export function canonicalizeSupplyChainValue(value: unknown): unknown {
if (Array.isArray(value)) {
return value
.map(canonicalizeSupplyChainValue)
.sort((left, right) =>
String(JSON.stringify(left)).localeCompare(String(JSON.stringify(right))),
);
}
if (isRecord(value)) {
return Object.fromEntries(
Object.entries(value)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, item]) => [key, canonicalizeSupplyChainValue(item)]),
);
}
return value;
}
export function supplyChainDigest(value: unknown): string {
return createHash("sha256")
.update(JSON.stringify(canonicalizeSupplyChainValue(value)))
.digest("hex");
}
export function parsePnpmLockfilePackages(
lockfile: string,
): LockfilePackage[] {
const entries: LockfilePackage[] = [];
let inPackages = false;
let current: { name: string; version: string; integrity: string } | null = null;
for (const line of lockfile.split(/\r?\n/)) {
if (line === "packages:") {
inPackages = true;
continue;
}
if (line === "snapshots:") {
if (current) entries.push(current);
break;
}
if (!inPackages) continue;
const packageMatch = line.match(/^ {2}(\S.*):$/);
if (packageMatch?.[1]) {
if (current) entries.push(current);
const key = packageMatch[1].replace(/^['"]|['"]$/g, "");
const separator = key.lastIndexOf("@");
current = {
name: key.slice(0, separator),
version: key.slice(separator + 1),
integrity: "",
};
continue;
}
const integrityMatch = line.match(/\bintegrity:\s*([^,}\s]+)/);
if (current && integrityMatch?.[1]) {
current.integrity = integrityMatch[1];
}
}
return entries.sort((left, right) =>
`${left.name}@${left.version}`.localeCompare(
`${right.name}@${right.version}`,
),
);
}
export function isValidSha512Integrity(integrity: string): boolean {
if (!integrity.startsWith("sha512-")) return false;
try {
return Buffer.from(integrity.slice("sha512-".length), "base64").length === 64;
} catch {
return false;
}
}
export function normalizeLicense(raw: unknown): string {
if (typeof raw === "string" && raw.trim()) return raw.trim();
if (isRecord(raw) && typeof raw.type === "string") return raw.type;
if (Array.isArray(raw)) {
const licenses = raw
.map(normalizeLicense)
.filter((license) => license !== "NOASSERTION");
return licenses.length > 0 ? licenses.join(" OR ") : "NOASSERTION";
}
return "NOASSERTION";
}
export async function flattenPnpmDependencyTree(
root: Record<string, unknown>,
directProduction: Readonly<Record<string, string>>,
directDevelopment: Readonly<Record<string, string>>,
): Promise<DependencyInventoryRow[]> {
const records = new Map<string, MutableDependencyRecord>();
const directIds = new Set<string>();
for (const [name, rawDependency] of Object.entries(
recordValue(root.dependencies),
)) {
if (Object.hasOwn(directProduction, name) && isRecord(rawDependency)) {
directIds.add(`${name}@${String(rawDependency.version ?? "")}`);
}
}
for (const [name, rawDependency] of Object.entries(
recordValue(root.devDependencies),
)) {
if (Object.hasOwn(directDevelopment, name) && isRecord(rawDependency)) {
directIds.add(`${name}@${String(rawDependency.version ?? "")}`);
}
}
function visit(
node: Record<string, unknown>,
scope: DependencyScope,
optionalPath: boolean,
): void {
const groups = {
dependencies: node.dependencies,
devDependencies: node.devDependencies,
optionalDependencies: node.optionalDependencies,
};
for (const [groupName, group] of Object.entries(groups)) {
for (const [name, rawDependency] of Object.entries(recordValue(group))) {
if (!isRecord(rawDependency)) continue;
const version = String(rawDependency.version ?? "");
const packagePath = String(rawDependency.path ?? "");
const identity = `${name}@${version}`;
const childScope: DependencyScope =
scope === "production" && groupName !== "devDependencies"
? "production"
: "development";
const childOptional =
optionalPath || groupName === "optionalDependencies";
const previous = records.get(identity);
const dependencies = previous?.dependencies ?? new Set<string>();
for (const childGroup of [
rawDependency.dependencies,
rawDependency.optionalDependencies,
]) {
for (const [childName, rawChild] of Object.entries(
recordValue(childGroup),
)) {
if (isRecord(rawChild)) {
dependencies.add(
`${childName}@${String(rawChild.version ?? "")}`,
);
}
}
}
records.set(identity, {
name,
version,
direct: directIds.has(identity),
scope:
previous?.scope === "production" || childScope === "production"
? "production"
: "development",
optional: previous
? previous.optional && childOptional
: childOptional,
packagePath: previous?.packagePath || packagePath,
dependencies,
});
visit(rawDependency, childScope, childOptional);
}
}
}
const productionRoot: Record<string, unknown> = {
dependencies: Object.fromEntries(
Object.entries(recordValue(root.dependencies)).filter(([name]) =>
Object.hasOwn(directProduction, name),
),
),
};
const developmentRoot: Record<string, unknown> = {
devDependencies: Object.fromEntries(
Object.entries(recordValue(root.devDependencies)).filter(([name]) =>
Object.hasOwn(directDevelopment, name),
),
),
};
visit(productionRoot, "production", false);
visit(developmentRoot, "development", false);
const result: DependencyInventoryRow[] = [];
for (const record of records.values()) {
let license = "NOASSERTION";
let optional = record.optional;
if (record.packagePath) {
try {
const parsed: unknown = JSON.parse(
await readFile(`${record.packagePath}/package.json`, "utf8"),
);
const manifest = recordValue(parsed);
license = normalizeLicense(manifest.license ?? manifest.licenses);
} catch {
// Platform-specific optional packages may not be materialized locally.
optional = true;
}
}
result.push({
name: record.name,
version: record.version,
direct: record.direct,
scope: record.scope,
optional,
license,
dependencies: [...record.dependencies].sort(),
});
}
return result.sort((left, right) =>
`${left.name}@${left.version}`.localeCompare(
`${right.name}@${right.version}`,
),
);
}
export function diffDependencyInventories(
before: Document,
after: Document,
): DependencyInventoryDiff {
const beforeRows = recordRows(before.dependencies);
const afterRows = recordRows(after.dependencies);
const beforeMap = new Map(
beforeRows.map((row) => [dependencyIdentity(row), row] as const),
);
const afterMap = new Map(
afterRows.map((row) => [dependencyIdentity(row), row] as const),
);
const added = [...afterMap.keys()].filter((key) => !beforeMap.has(key));
const removed = [...beforeMap.keys()].filter((key) => !afterMap.has(key));
const changed: string[] = [];
for (const key of [...beforeMap.keys()].filter((item) => afterMap.has(item))) {
if (supplyChainDigest(beforeMap.get(key)) !== supplyChainDigest(afterMap.get(key))) {
changed.push(key);
}
}
const upgrades: DependencyUpgrade[] = [];
for (const removedKey of removed) {
const previous = beforeMap.get(removedKey);
if (!previous) continue;
const replacement = added.find(
(addedKey) => afterMap.get(addedKey)?.name === previous.name,
);
const next = replacement ? afterMap.get(replacement) : undefined;
if (next) {
upgrades.push({
name: String(previous.name ?? ""),
from: String(previous.version ?? ""),
to: String(next.version ?? ""),
});
}
}
return Object.freeze({
added: Object.freeze(added.sort()),
removed: Object.freeze(removed.sort()),
changed: Object.freeze(changed.sort()),
upgrades: Object.freeze(
upgrades.sort((left, right) => left.name.localeCompare(right.name)),
),
});
}
export function validateLicensePolicy(
inventory: Document,
policy: Document,
) {
const allowed = new Set(stringRows(policy.allowedLicenses));
const denied = stringRows(policy.deniedLicensePatterns);
const failures: string[] = [];
const results: Array<Readonly<{
package: string;
license: string;
passed: boolean;
reason: string | null;
}>> = [];
for (const dependency of recordRows(inventory.dependencies)) {
const license = String(dependency.license ?? "NOASSERTION");
const explicitlyDenied = denied.some((pattern) =>
new RegExp(pattern, "i").test(license),
);
const unknownAccepted =
license === "NOASSERTION" && dependency.optional === true;
const passed =
!explicitlyDenied && (allowed.has(license) || unknownAccepted);
results.push({
package: dependencyIdentity(dependency),
license,
passed,
reason: unknownAccepted ? "platform-optional-not-materialized" : null,
});
if (!passed) {
failures.push(
`${dependencyIdentity(dependency)} has disallowed license ${license}`,
);
}
}
return Object.freeze({
passed: failures.length === 0,
failures: Object.freeze(failures),
results: Object.freeze(results),
});
}
export function validateDependencyReview(
diff: DependencyInventoryDiff,
inventory: Document,
evidenceFile: Document,
) {
const byIdentity = new Map(
recordRows(inventory.dependencies).map(
(row) => [dependencyIdentity(row), row] as const,
),
);
const evidence = new Map<string, Record<string, unknown>>();
for (const entry of recordRows(evidenceFile.changes)) {
if (typeof entry.changeId === "string") evidence.set(entry.changeId, entry);
}
const highRisk = diff.added.filter((identity) => {
const row = byIdentity.get(identity);
return row?.direct === true && row.scope === "production";
});
const failures: string[] = [];
for (const identity of highRisk) {
const changeId = `add:${identity}`;
const entry = evidence.get(changeId);
if (!entry) {
failures.push(`high-risk dependency missing review: ${changeId}`);
continue;
}
for (const field of ["owner", "reviewer", "reason", "rollback"] as const) {
const value = entry[field];
if (typeof value !== "string" || !value.trim()) {
failures.push(`${changeId} missing ${field}`);
}
}
if (entry.owner === entry.reviewer) {
failures.push(`${changeId} may not be self-approved`);
}
}
return Object.freeze({
passed: failures.length === 0,
highRisk: Object.freeze(highRisk),
failures: Object.freeze(failures),
});
}
const severityRank: ReadonlyMap<string, number> = new Map([
["unknown", 0],
["low", 1],
["moderate", 2],
["high", 3],
["critical", 4],
]);
export function validateVulnerabilityReport(
report: Document,
policy: Document,
exceptionFile: Document,
lockfileSha256: string,
now: Date = new Date(),
) {
const failures: string[] = [];
if (report.scannedLockfileSha256 !== lockfileSha256) {
failures.push("vulnerability report lockfile digest mismatch");
}
if (typeof report.provider !== "string" || !report.provider.trim()) {
failures.push("vulnerability report provider missing");
}
const threshold = severityRank.get(String(policy.blockAtSeverity)) ?? 3;
const exceptions = recordRows(exceptionFile.exceptions);
const blocking: string[] = [];
for (const finding of recordRows(report.findings)) {
const severity = String(finding.severity ?? "unknown").toLowerCase();
if ((severityRank.get(severity) ?? 0) < threshold) continue;
const exception = exceptions.find(
(entry) =>
entry.vulnerabilityId === finding.id &&
entry.packageName === finding.packageName,
);
const expiry =
typeof exception?.expiresAt === "string"
? Date.parse(exception.expiresAt)
: Number.NaN;
const validException = Boolean(
exception &&
typeof exception.owner === "string" &&
exception.owner.trim() &&
typeof exception.reviewer === "string" &&
exception.reviewer.trim() &&
exception.owner !== exception.reviewer &&
typeof exception.reason === "string" &&
exception.reason.trim() &&
Number.isFinite(expiry) &&
expiry > now.getTime(),
);
if (!validException) {
blocking.push(
`${String(finding.id)}:${String(finding.packageName)}@${String(finding.version)}:${severity}`,
);
}
}
return Object.freeze({
passed: failures.length === 0 && blocking.length === 0,
failures: Object.freeze(failures),
blocking: Object.freeze(blocking),
});
}
export function verifySupplyChainCoherence(
sbom: Document,
inventory: Document,
provenance: Document,
distDigest: string,
) {
const failures: string[] = [];
const componentCount = Array.isArray(sbom.components)
? sbom.components.length
: -1;
const dependencyCount = Array.isArray(inventory.dependencies)
? inventory.dependencies.length
: -2;
if (componentCount !== dependencyCount) {
failures.push("SBOM component count does not match inventory");
}
const metadata = recordValue(sbom.metadata);
const properties = recordRows(metadata.properties);
if (
properties.find(
(property) =>
property.name === "ca:lockfileSha256" &&
property.value === inventory.lockfileSha256,
) === undefined
) {
failures.push("SBOM lockfile digest does not match inventory");
}
const subject = recordRows(provenance.subject)[0];
const subjectDigest = recordValue(subject?.digest);
if (subjectDigest.sha256 !== distDigest) {
failures.push("provenance subject does not match built dist digest");
}
const predicate = recordValue(provenance.predicate);
const materials = recordValue(predicate.materials);
if (materials.lockfileSha256 !== inventory.lockfileSha256) {
failures.push("provenance lockfile material does not match inventory");
}
return Object.freeze({
passed: failures.length === 0,
failures: Object.freeze(failures),
});
}
+63
View File
@@ -0,0 +1,63 @@
import path from "node:path";
import type { Plugin } from "vite";
type ModuleInventoryChunk = Readonly<{
fileName: string;
modules: readonly string[];
}>;
/**
* Rollup knows the exact source-module set for every emitted chunk. Persisting
* that graph makes optional-runtime exclusion verifiable without relying on
* minified names, error strings, or source maps.
*/
export function viteModuleInventoryPlugin(
repositoryRoot = process.cwd(),
): Plugin {
return {
name: "frontend-module-inventory",
generateBundle(_options, bundle) {
const chunks: ModuleInventoryChunk[] = Object.values(bundle)
.filter((output) => output.type === "chunk")
.map((chunk) => ({
fileName: chunk.fileName,
modules: Object.freeze(
[...new Set(
Object.keys(chunk.modules).map((moduleId) =>
normalizeModuleId(moduleId, repositoryRoot),
),
)].sort(),
),
}))
.sort((left, right) => left.fileName.localeCompare(right.fileName));
this.emitFile({
type: "asset",
fileName: ".vite/module-inventory.json",
source: `${JSON.stringify(
{
schemaVersion: 1,
chunks,
},
null,
2,
)}\n`,
});
},
};
}
function normalizeModuleId(
moduleId: string,
repositoryRoot: string,
): string {
const withoutQuery = moduleId.replace(/^\0/u, "").split("?", 1)[0] ?? "";
if (!path.isAbsolute(withoutQuery)) {
return withoutQuery.replaceAll("\\", "/");
}
const relative = path.relative(repositoryRoot, withoutQuery);
return relative.startsWith("..")
? `external:${path.basename(withoutQuery)}`
: relative.replaceAll("\\", "/");
}
-86
View File
@@ -1,86 +0,0 @@
import { spawnSync } from "node:child_process";
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
const gateId = process.argv
.slice(2)
.find((argument) => /^FE-GATE-\d{3}$/.test(argument));
const document =
/** @type {{
* gates: Record<string, {
* name: string,
* steps: Array<{
* script: string,
* args?: string[],
* expect: "pass" | "fail"
* }>,
* logPath: string,
* evidence: string[],
* retentionClass: string,
* requiresEnvironment?: string[]
* }>
* }} */ (JSON.parse(await readFile("config/ci/gates.json", "utf8")));
const gate = gateId ? document.gates[gateId] : undefined;
if (!gateId || !gate) {
process.stderr.write("Usage: ci:gate -- FE-GATE-001..FE-GATE-026\n");
process.exit(2);
}
const output = [];
let passed = true;
for (const variable of gate.requiresEnvironment ?? []) {
if (!process.env[variable]) {
output.push(`missing required environment: ${variable}`);
passed = false;
}
}
if (passed) {
for (const step of gate.steps) {
const result = spawnSync(
"corepack",
["pnpm", step.script, ...(step.args ?? [])],
{ encoding: "utf8", env: process.env },
);
output.push(
`$ corepack pnpm ${step.script} ${(step.args ?? []).join(" ")}`.trim(),
result.stdout,
result.stderr,
);
const exitedSuccessfully = result.status === 0;
const expectationMet =
step.expect === "pass" ? exitedSuccessfully : !exitedSuccessfully;
if (!expectationMet) {
output.push(
`expectation failed: expected ${step.expect}, exit=${result.status}`,
);
passed = false;
break;
}
}
}
await mkdir(path.dirname(gate.logPath), { recursive: true });
await writeFile(gate.logPath, `${output.filter(Boolean).join("\n")}\n`);
if (passed) {
for (const evidencePath of gate.evidence) {
try {
await access(evidencePath);
} catch {
output.push(`missing evidence: ${evidencePath}`);
passed = false;
}
}
if (!passed) {
await writeFile(gate.logPath, `${output.filter(Boolean).join("\n")}\n`);
}
}
if (!passed) {
process.stderr.write(`${gateId} ${gate.name}: FAIL\n`);
process.exit(1);
}
process.stdout.write(
`${gateId} ${gate.name}: PASS (${gate.retentionClass})\n`,
);
+208
View File
@@ -0,0 +1,208 @@
import { spawnSync } from "node:child_process";
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import {
ciCheckoutIdentityFailures,
ciBuildEnvironmentFailures,
isValidCommitSha,
isValidSourceDateEpoch,
} from "./lib/build-environment.ts";
type GateStep = Readonly<{
script: string;
args?: readonly string[];
expect: "pass" | "fail";
}>;
type GateDefinition = Readonly<{
name: string;
steps: readonly GateStep[];
logPath: string;
evidence: readonly string[];
retentionClass: string;
requiresEnvironment?: readonly string[];
}>;
type GateDocument = Readonly<{
gates: Readonly<Record<string, GateDefinition>>;
}>;
const gateId = process.argv
.slice(2)
.find((argument) => /^FE-GATE-\d{3}$/.test(argument));
const document = parseGateDocument(
JSON.parse(await readFile("config/ci/gates.json", "utf8")),
);
const gate = gateId ? document.gates[gateId] : undefined;
if (!gateId || !gate) {
process.stderr.write("Usage: ci:gate -- FE-GATE-001..FE-GATE-026\n");
process.exit(2);
}
const output: string[] = [];
let passed = true;
const gateEnvironment = { ...process.env };
if (gateEnvironment.CI === "true") {
const commitMetadata = spawnSync(
"git",
["show", "-s", "--format=%H%n%ct", "HEAD"],
{ encoding: "utf8" },
);
const [commitSha = "", sourceDateEpoch = ""] =
commitMetadata.stdout.trim().split(/\r?\n/);
if (
commitMetadata.status === 0 &&
isValidCommitSha(commitSha) &&
isValidSourceDateEpoch(sourceDateEpoch)
) {
if (!gateEnvironment.SOURCE_DATE_EPOCH?.trim()) {
gateEnvironment.SOURCE_DATE_EPOCH = sourceDateEpoch;
output.push(`derived SOURCE_DATE_EPOCH=${sourceDateEpoch} from HEAD`);
}
for (const failure of ciCheckoutIdentityFailures(gateEnvironment, {
commitSha,
sourceDateEpoch,
})) {
output.push(failure);
passed = false;
}
} else {
output.push(
"unable to resolve the checked-out commit identity and timestamp",
commitMetadata.stderr,
);
passed = false;
}
}
for (const failure of ciBuildEnvironmentFailures(gateEnvironment)) {
output.push(failure);
passed = false;
}
for (const variable of gate.requiresEnvironment ?? []) {
if (!gateEnvironment[variable]) {
output.push(`missing required environment: ${variable}`);
passed = false;
}
}
if (passed) {
for (const step of gate.steps) {
const result = spawnSync(
"corepack",
["pnpm", step.script, ...(step.args ?? [])],
{ encoding: "utf8", env: gateEnvironment },
);
output.push(
`$ corepack pnpm ${step.script} ${(step.args ?? []).join(" ")}`.trim(),
result.stdout,
result.stderr,
);
const exitedSuccessfully = result.status === 0;
const expectationMet =
step.expect === "pass" ? exitedSuccessfully : !exitedSuccessfully;
if (!expectationMet) {
output.push(
`expectation failed: expected ${step.expect}, exit=${result.status}`,
);
passed = false;
break;
}
}
}
await mkdir(path.dirname(gate.logPath), { recursive: true });
await writeFile(gate.logPath, `${output.filter(Boolean).join("\n")}\n`);
if (passed) {
for (const evidencePath of gate.evidence) {
try {
await access(evidencePath);
} catch {
output.push(`missing evidence: ${evidencePath}`);
passed = false;
}
}
if (!passed) {
await writeFile(gate.logPath, `${output.filter(Boolean).join("\n")}\n`);
}
}
if (!passed) {
process.stderr.write(`${gateId} ${gate.name}: FAIL\n`);
process.exit(1);
}
process.stdout.write(
`${gateId} ${gate.name}: PASS (${gate.retentionClass})\n`,
);
function parseGateDocument(value: unknown): GateDocument {
if (!isRecord(value) || !isRecord(value.gates)) {
throw new TypeError("CI gate registry must be an object");
}
const gates: Record<string, GateDefinition> = {};
for (const [gateId, candidate] of Object.entries(value.gates)) {
if (!isRecord(candidate)) throw new TypeError(`Invalid CI gate: ${gateId}`);
const steps = parseGateSteps(candidate.steps, gateId);
const evidence = parseStringArray(candidate.evidence, `${gateId}.evidence`);
const requiresEnvironment =
candidate.requiresEnvironment === undefined
? undefined
: parseStringArray(
candidate.requiresEnvironment,
`${gateId}.requiresEnvironment`,
);
if (
typeof candidate.name !== "string" ||
typeof candidate.logPath !== "string" ||
typeof candidate.retentionClass !== "string"
) {
throw new TypeError(`CI gate metadata is invalid: ${gateId}`);
}
gates[gateId] = {
name: candidate.name,
steps,
logPath: candidate.logPath,
evidence,
retentionClass: candidate.retentionClass,
...(requiresEnvironment ? { requiresEnvironment } : {}),
};
}
return { gates };
}
function parseGateSteps(value: unknown, gateId: string): GateStep[] {
if (!Array.isArray(value)) {
throw new TypeError(`CI gate steps are invalid: ${gateId}`);
}
return value.map((candidate, index) => {
if (
!isRecord(candidate) ||
typeof candidate.script !== "string" ||
(candidate.expect !== "pass" && candidate.expect !== "fail")
) {
throw new TypeError(`Invalid CI gate step: ${gateId}[${index}]`);
}
const args =
candidate.args === undefined
? undefined
: parseStringArray(candidate.args, `${gateId}[${index}].args`);
return {
script: candidate.script,
expect: candidate.expect,
...(args ? { args } : {}),
};
});
}
function parseStringArray(value: unknown, label: string): string[] {
if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
throw new TypeError(`${label} must be a string array`);
}
return value;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
@@ -2,14 +2,66 @@ import { createHash } from "node:crypto";
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
import path from "node:path";
/** @param {string} name @param {string} fallback */
function argumentValue(name, fallback) {
type SecretFinding = Readonly<{
ruleId: string;
file: string;
line: number;
fingerprint: string;
}>;
type AllowlistEntry = Readonly<{
path: string;
ruleId: string;
owner: string;
reason: string;
expiresAt: string;
}>;
type SecretPolicy = Readonly<{
excludedPaths: readonly string[];
trackedRoots: readonly string[];
generatedRoots: readonly string[];
allowlist: readonly AllowlistEntry[];
}>;
function argumentValue(name: string, fallback: string): string {
const index = process.argv.indexOf(name);
return index >= 0 && process.argv[index + 1]
? process.argv[index + 1]
: fallback;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function strings(value: unknown): string[] {
return Array.isArray(value)
? value.filter((entry): entry is string => typeof entry === "string")
: [];
}
function parsePolicy(value: unknown): SecretPolicy {
const document = isRecord(value) ? value : {};
const allowlist = Array.isArray(document.allowlist)
? document.allowlist.map((rawEntry) => {
const entry = isRecord(rawEntry) ? rawEntry : {};
return {
path: typeof entry.path === "string" ? entry.path : "",
ruleId: typeof entry.ruleId === "string" ? entry.ruleId : "",
owner: typeof entry.owner === "string" ? entry.owner : "",
reason: typeof entry.reason === "string" ? entry.reason : "",
expiresAt:
typeof entry.expiresAt === "string" ? entry.expiresAt : "",
};
})
: [];
return Object.freeze({
excludedPaths: Object.freeze(strings(document.excludedPaths)),
trackedRoots: Object.freeze(strings(document.trackedRoots)),
generatedRoots: Object.freeze(strings(document.generatedRoots)),
allowlist: Object.freeze(allowlist),
});
}
const policyPath = argumentValue(
"--policy",
"config/security/secret-scan-policy.json",
@@ -18,16 +70,11 @@ const artifactPath = argumentValue(
"--artifact",
"artifacts/security/scan.sarif",
);
const policy = JSON.parse(await readFile(policyPath, "utf8"));
const findings =
/** @type {Array<{
* ruleId: string,
* file: string,
* line: number,
* fingerprint: string
* }>} */ ([]);
const policyFailures = [];
const patterns = [
const rawPolicy: unknown = JSON.parse(await readFile(policyPath, "utf8"));
const policy = parsePolicy(rawPolicy);
const findings: SecretFinding[] = [];
const policyFailures: string[] = [];
const patterns: readonly Readonly<{ id: string; expression: RegExp }>[] = [
{
id: "private-key",
expression: /-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/g,
@@ -41,18 +88,17 @@ const patterns = [
},
];
/** @param {string} target @returns {Promise<string[]>} */
async function filesWithin(target) {
async function filesWithin(target: string): Promise<string[]> {
try {
const metadata = await stat(target);
if (metadata.isFile()) return [target];
const entries = await readdir(target, { withFileTypes: true });
const nested = /** @type {string[][]} */ (await Promise.all(
const nested: string[][] = await Promise.all(
entries.map((entry) => {
const child = path.join(target, entry.name);
return entry.isDirectory() ? filesWithin(child) : [child];
}),
));
);
return nested.flat();
} catch {
return [];
@@ -60,24 +106,15 @@ async function filesWithin(target) {
}
const excluded = new Set(
/** @type {string[]} */ (policy.excludedPaths ?? []).map((entry) =>
entry.replaceAll("\\", "/"),
),
policy.excludedPaths.map((entry) => entry.replaceAll("\\", "/")),
);
const allowlist =
/** @type {Array<{
* path: string,
* ruleId: string,
* owner: string,
* reason: string,
* expiresAt: string
* }>} */ (policy.allowlist ?? []);
const allowlist = policy.allowlist;
for (const entry of allowlist) {
const expiry = Date.parse(entry.expiresAt);
if (
!entry.path.startsWith("tests/") ||
!entry.owner?.trim() ||
!entry.reason?.trim() ||
!entry.owner.trim() ||
!entry.reason.trim() ||
!Number.isFinite(expiry) ||
expiry <= Date.now()
) {
@@ -87,10 +124,7 @@ for (const entry of allowlist) {
}
}
const roots = [
...(/** @type {string[]} */ (policy.trackedRoots ?? [])),
...(/** @type {string[]} */ (policy.generatedRoots ?? [])),
];
const roots = [...policy.trackedRoots, ...policy.generatedRoots];
const scanFiles = (
await Promise.all(roots.map((root) => filesWithin(root)))
).flat();
@@ -104,7 +138,7 @@ for (const scanFile of [...new Set(scanFiles)].sort()) {
) {
continue;
}
let content;
let content: string;
try {
content = await readFile(scanFile, "utf8");
} catch {
@@ -120,13 +154,14 @@ for (const scanFile of [...new Set(scanFiles)].sort()) {
Date.parse(entry.expiresAt) > Date.now(),
);
if (isAllowed) continue;
const prefix = content.slice(0, match.index);
const matchIndex = match.index ?? 0;
const prefix = content.slice(0, matchIndex);
findings.push({
ruleId: pattern.id,
file: normalized,
line: prefix.split(/\r?\n/).length,
fingerprint: createHash("sha256")
.update(`${pattern.id}:${normalized}:${String(match.index)}`)
.update(`${pattern.id}:${normalized}:${String(matchIndex)}`)
.digest("hex"),
});
}
@@ -150,9 +185,7 @@ const sarif = {
results: [
...findings.map((finding) => ({
ruleId: finding.ruleId,
message: {
text: "Potential secret material must be removed.",
},
message: { text: "Potential secret material must be removed." },
partialFingerprints: {
primaryLocationLineHash: finding.fingerprint,
},
@@ -5,14 +5,14 @@ import path from "node:path";
const root = path.resolve(process.argv[2] ?? "artifacts/storybook/static");
const port = Number(process.argv[3] ?? 6006);
const contentTypes = /** @type {Readonly<Record<string, string>>} */ ({
const contentTypes: Readonly<Record<string, string>> = {
".css": "text/css; charset=utf-8",
".html": "text/html; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".json": "application/json; charset=utf-8",
".svg": "image/svg+xml",
".png": "image/png",
});
};
await access(root);
const server = createServer(async (request, response) => {
@@ -0,0 +1,379 @@
import { spawnSync } from "node:child_process";
import {
cp,
mkdir,
readFile,
readdir,
rm,
symlink,
writeFile,
} from "node:fs/promises";
import path from "node:path";
const fixtureRoot = path.resolve(
".tmp/browser-file-storage-runtime-removal",
);
const pnpmCli = requireEnvironment("npm_execpath");
const runtimePaths = [
"src/application/ports/browser-file-storage",
"src/application/ports/browser-transfer",
"src/adapters/browser-file-storage",
"src/adapters/browser-files",
"src/adapters/browser-transfer",
"src/adapters/cache-storage",
"src/adapters/storage/indexeddb",
"src/adapters/storage/opfs",
"tests/browser-capabilities",
"tests/fixtures/browser-file-storage-boundaries",
] as const;
const runtimeSourceRoots = runtimePaths.filter((entry) =>
entry.startsWith("src/"),
);
const copyTargets = [
"src",
"tests",
"recipes",
"scripts",
"config",
"schemas",
"public",
".gitea",
".storybook",
"index.html",
"package.json",
"tsconfig.base.json",
"tsconfig.json",
"tsconfig.app.json",
"tsconfig.node.json",
"tsconfig.test.json",
"tsconfig.recipes.json",
"vite.config.ts",
"vitest.config.ts",
"playwright.config.ts",
"playwright.capabilities.config.ts",
"playwright.dev.config.ts",
"playwright.storybook.config.ts",
"playwright.visual.config.ts",
"eslint.config.ts",
".dependency-cruiser.json",
".nvmrc",
] as const;
function requireEnvironment(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`${name} is required for runtime removal verification`);
}
return value;
}
function runPnpm(script: string): boolean {
return (
spawnSync(process.execPath, [pnpmCli, script], {
cwd: fixtureRoot,
stdio: "inherit",
}).status === 0
);
}
async function sourceFiles(directory: string): Promise<string[]> {
const entries = await readdir(directory, { withFileTypes: true });
return (
await Promise.all(
entries.map(async (entry): Promise<string[]> => {
const target = path.join(directory, entry.name);
if (entry.isDirectory()) {
return await sourceFiles(target);
}
return /\.(?:[cm]?ts|tsx)$/u.test(entry.name)
? [path.resolve(target)]
: [];
}),
)
).flat();
}
function staticImportSpecifiers(source: string): string[] {
return [
...source.matchAll(
/(?:from\s*|import\s*\(\s*|import\s*)["']([^"']+)["']/gu,
),
]
.map((match) => match[1])
.filter((specifier): specifier is string =>
typeof specifier === "string",
);
}
function isWithin(target: string, root: string): boolean {
const relative = path.relative(root, target);
return (
relative === "" ||
(!relative.startsWith("..") && !path.isAbsolute(relative))
);
}
function resolvedImport(
importer: string,
specifier: string,
sourceSet: ReadonlySet<string>,
): string | null {
if (!specifier.startsWith(".")) return null;
const base = path.resolve(path.dirname(importer), specifier);
const candidates = [
base,
`${base}.ts`,
`${base}.tsx`,
`${base}.mts`,
`${base}.cts`,
path.join(base, "index.ts"),
path.join(base, "index.tsx"),
];
return candidates.find((candidate) => sourceSet.has(candidate)) ?? base;
}
async function runtimeImportGraph(root: string): Promise<Readonly<{
dependentTests: readonly string[];
importingFiles: readonly string[];
}>> {
const files = await sourceFiles(root);
const sourceSet = new Set(files);
const runtimeRoots = runtimeSourceRoots.map((entry) =>
path.resolve(root, entry),
);
const imports = new Map<string, readonly string[]>();
for (const file of files) {
const source = await readFile(file, "utf8");
imports.set(
file,
staticImportSpecifiers(source)
.map((specifier) =>
resolvedImport(file, specifier, sourceSet),
)
.filter((target): target is string => target !== null),
);
}
const memo = new Map<string, boolean>();
const reachesRuntime = (
file: string,
visiting = new Set<string>(),
): boolean => {
if (runtimeRoots.some((root) => isWithin(file, root))) return true;
const known = memo.get(file);
if (known !== undefined) return known;
if (visiting.has(file)) return false;
visiting.add(file);
const reaches = (imports.get(file) ?? []).some(
(dependency) =>
runtimeRoots.some((runtimeRoot) =>
isWithin(dependency, runtimeRoot),
) ||
(sourceSet.has(dependency) &&
reachesRuntime(dependency, visiting)),
);
visiting.delete(file);
memo.set(file, reaches);
return reaches;
};
const testsRoot = path.resolve(root, "tests");
const dependentTests = files.filter(
(file) => isWithin(file, testsRoot) && reachesRuntime(file),
);
const importingFiles = files.filter(
(file) =>
!runtimeRoots.some((runtimeRoot) =>
isWithin(file, runtimeRoot),
) &&
(imports.get(file) ?? []).some((dependency) =>
runtimeRoots.some((runtimeRoot) =>
isWithin(dependency, runtimeRoot),
),
),
);
return Object.freeze({
dependentTests: Object.freeze(dependentTests),
importingFiles: Object.freeze(importingFiles),
});
}
async function assertNoRuntimeImports(root: string): Promise<void> {
const graph = await runtimeImportGraph(root);
if (graph.importingFiles.length > 0) {
throw new Error(
`Removed browser file/storage runtime is still imported by: ${graph.importingFiles
.map((file) => path.relative(root, file))
.join(", ")}`,
);
}
}
async function removeRuntimeDependentTests(
root: string,
): Promise<number> {
const graph = await runtimeImportGraph(root);
await Promise.all(
graph.dependentTests.map(async (file) => {
if (isWithin(file, path.resolve(root, "tests"))) {
await rm(file, { force: true });
}
}),
);
return graph.dependentTests.length;
}
await rm(fixtureRoot, { recursive: true, force: true });
await mkdir(fixtureRoot, { recursive: true });
for (const target of copyTargets) {
await cp(target, path.join(fixtureRoot, target), { recursive: true });
}
await symlink(
path.resolve("node_modules"),
path.join(fixtureRoot, "node_modules"),
"dir",
);
const removedRuntimeTests =
await removeRuntimeDependentTests(fixtureRoot);
for (const runtimePath of runtimePaths) {
await rm(path.join(fixtureRoot, runtimePath), {
recursive: true,
force: true,
});
}
const catalogPath = path.join(
fixtureRoot,
"config/recipes/frontend-capability-recipes.json",
);
const catalog = JSON.parse(await readFile(catalogPath, "utf8")) as {
recipes: Array<Record<string, unknown>>;
};
const browserFileRuntimeRecipeIds = new Set([
"offline-indexeddb",
"service-worker-pwa",
"file-transfer",
]);
let removedRuntimeEntries = 0;
for (const recipe of catalog.recipes) {
if (
typeof recipe.id !== "string" ||
!browserFileRuntimeRecipeIds.has(recipe.id) ||
!Object.hasOwn(recipe, "referenceRuntime")
) {
continue;
}
delete recipe.referenceRuntime;
removedRuntimeEntries += 1;
}
if (removedRuntimeEntries !== 3) {
throw new Error(
`Expected three reference runtime catalog entries, removed ${removedRuntimeEntries}`,
);
}
await writeFile(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`);
const packagePath = path.join(fixtureRoot, "package.json");
const packageDocument = JSON.parse(await readFile(packagePath, "utf8")) as {
scripts: Record<string, string>;
};
for (const script of [
"test:browser-capabilities",
"verify:browser-capability-evidence",
"check:browser-file-storage-boundaries",
"test:browser-file-storage-removal",
]) {
delete packageDocument.scripts[script];
}
await writeFile(
packagePath,
`${JSON.stringify(packageDocument, null, 2)}\n`,
);
await rm(path.join(fixtureRoot, "playwright.capabilities.config.ts"), {
force: true,
});
await rm(
path.join(fixtureRoot, "scripts/check-browser-file-storage-boundaries.ts"),
{ force: true },
);
await rm(
path.join(fixtureRoot, "scripts/verify-browser-capability-evidence.ts"),
{ force: true },
);
await rm(
path.join(fixtureRoot, "scripts/test-browser-file-storage-runtime-removal.ts"),
{ force: true },
);
const gatesPath = path.join(fixtureRoot, "config/ci/gates.json");
const gatesDocument = JSON.parse(
await readFile(gatesPath, "utf8"),
) as {
gates: Record<
string,
{
steps: Array<{ script: string }>;
evidence: string[];
}
>;
};
for (const gate of Object.values(gatesDocument.gates)) {
gate.steps = gate.steps.filter(
({ script }) =>
![
"test:browser-capabilities",
"verify:browser-capability-evidence",
"check:browser-file-storage-boundaries",
"test:browser-file-storage-removal",
].includes(script),
);
gate.evidence = gate.evidence.filter(
(evidence) =>
!evidence.includes("browser-capabilities") &&
!evidence.includes("browser-file-storage-runtime-removal"),
);
}
await writeFile(
gatesPath,
`${JSON.stringify(gatesDocument, null, 2)}\n`,
);
await assertNoRuntimeImports(fixtureRoot);
const checks: Array<readonly [string, boolean]> = [
["typecheck", runPnpm("check:types")],
["lint", runPnpm("lint")],
["architecture", runPnpm("check:architecture")],
["test", runPnpm("test:all")],
["build", runPnpm("build")],
["optional-catalog", runPnpm("check:optional-recipes:source")],
["ci-contract", runPnpm("check:ci")],
];
const passed = checks.every(([, result]) => result);
await mkdir("artifacts/tests", { recursive: true });
await writeFile(
"artifacts/tests/browser-file-storage-runtime-removal.xml",
`<?xml version="1.0" encoding="UTF-8"?>\n` +
`<testsuite name="browser-file-storage-runtime-removal" tests="${checks.length}" failures="${passed ? 0 : 1}">` +
checks
.map(
([name, result]) =>
`<testcase name="${name}">${result ? "" : "<failure />"}</testcase>`,
)
.join("") +
`</testsuite>\n`,
);
await rm(fixtureRoot, { recursive: true, force: true });
if (!passed) {
process.stderr.write(
`Browser file/storage runtime removal failed: ${checks
.filter(([, result]) => !result)
.map(([name]) => name)
.join(", ")}\n`,
);
process.exit(1);
}
process.stdout.write(
`Browser file/storage runtime removal: PASS (${checks.length} base checks, ${removedRuntimeTests} runtime-dependent tests removed by import graph)\n`,
);
@@ -11,7 +11,7 @@ import {
import path from "node:path";
const fixtureRoot = path.resolve(".tmp/optional-recipe-removal");
const pnpmCli = /** @type {string} */ (process.env.npm_execpath);
const pnpmCli = requireEnvironment("npm_execpath");
const copyTargets = [
"src",
"tests",
@@ -19,6 +19,7 @@ const copyTargets = [
"scripts",
"config",
"public",
".storybook",
"index.html",
"package.json",
"tsconfig.base.json",
@@ -27,15 +28,25 @@ const copyTargets = [
"tsconfig.node.json",
"tsconfig.test.json",
"tsconfig.recipes.json",
"vite.config.js",
"vitest.config.js",
"playwright.config.js",
"eslint.config.js",
".dependency-cruiser.cjs",
"vite.config.ts",
"vitest.config.ts",
"playwright.config.ts",
"playwright.dev.config.ts",
"playwright.storybook.config.ts",
"playwright.visual.config.ts",
"eslint.config.ts",
".dependency-cruiser.json",
];
/** @param {string} script */
function runPnpm(script) {
function requireEnvironment(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`${name} is required to run removal verification`);
}
return value;
}
function runPnpm(script: string): boolean {
return (
spawnSync(process.execPath, [pnpmCli, script], {
cwd: fixtureRoot,
@@ -44,8 +55,7 @@ function runPnpm(script) {
);
}
/** @param {string} directory @returns {Promise<string[]>} */
async function filesBelow(directory) {
async function filesBelow(directory: string): Promise<string[]> {
const entries = await readdir(directory, { withFileTypes: true });
const groups = await Promise.all(
entries.map((entry) => {
@@ -68,14 +78,13 @@ await rm(path.join(fixtureRoot, "tests/recipes"), {
force: true,
});
const checks = [
const checks: Array<[string, boolean]> = [
["typecheck", runPnpm("check:types")],
["architecture", runPnpm("check:architecture")],
["test", runPnpm("test:all")],
["build", runPnpm("build")],
];
/** @type {string[]} */
const residue = [];
const residue: string[] = [];
for (const file of await filesBelow(path.join(fixtureRoot, "dist"))) {
if (!/\.(?:js|css|html|json)$/.test(file)) continue;
const content = await readFile(file, "utf8");
@@ -5,8 +5,22 @@ import process from "node:process";
import { chromium } from "@playwright/test";
import { evaluateLabBudget } from "../src/application/policies/performance-budgets.js";
import { ROUTE_REGISTRY } from "../src/features/installed-feature-contracts.js";
import { evaluateLabBudget } from "../src/application/policies/performance-budgets.ts";
import { ROUTE_REGISTRY } from "../src/features/installed-feature-contracts.ts";
type ContractPerformanceEvidence = {
lcpMs: number;
cls: number;
};
type ContractPerformanceWindow = Window & {
__contractPerformance?: ContractPerformanceEvidence;
};
type LayoutShiftEntry = PerformanceEntry & {
hadRecentInput: boolean;
value: number;
};
const server = spawn(
"corepack",
@@ -54,20 +68,25 @@ try {
await cdp.send("Emulation.setCPUThrottlingRate", { rate: 4 });
await page.addInitScript(() => {
const evidence = { lcpMs: 0, cls: 0 };
/** @type {any} */ (window).__contractPerformance = evidence;
(window as ContractPerformanceWindow).__contractPerformance = evidence;
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) evidence.lcpMs = entry.startTime;
}).observe({ type: "largest-contentful-paint", buffered: true });
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!(/** @type {any} */ (entry)).hadRecentInput) {
evidence.cls += /** @type {any} */ (entry).value;
const layoutShift = entry as LayoutShiftEntry;
if (!layoutShift.hadRecentInput) {
evidence.cls += layoutShift.value;
}
}
}).observe({ type: "layout-shift", buffered: true });
});
await page.goto(baseUrl, { waitUntil: "networkidle" });
const targetLabel = Object.values(ROUTE_REGISTRY).find(
const performanceRoutes: ReadonlyArray<{
access: string;
navigationLabel: string | null;
}> = Object.values(ROUTE_REGISTRY);
const targetLabel = performanceRoutes.find(
(definition) => definition.access === "integration-defined",
)?.navigationLabel;
if (!targetLabel) {
@@ -78,8 +97,11 @@ try {
await page.getByRole("heading", { name: "세션이 필요합니다." }).waitFor();
const namedInteractionMs = performance.now() - interactionStarted;
const paint = await page.evaluate(
() => /** @type {any} */ (window).__contractPerformance,
() => (window as ContractPerformanceWindow).__contractPerformance,
);
if (!paint) {
throw new Error("Browser performance evidence was not initialized.");
}
const contextMetadata = {
runner: {
platform: process.platform,
+356
View File
@@ -0,0 +1,356 @@
import { spawnSync } from "node:child_process";
import {
cp,
mkdir,
readFile,
readdir,
rm,
symlink,
writeFile,
} from "node:fs/promises";
import path from "node:path";
const fixtureRoot = path.resolve(".tmp/realtime-runtime-removal");
const pnpmCli = requireEnvironment("npm_execpath");
const runtimePaths = [
"src/application/ports/realtime",
"src/application/ports/out/web-push-control.ts",
"src/application/policies/bounded-polling.ts",
"src/contracts/realtime-events.ts",
"src/contracts/realtime-streams.ts",
"src/contracts/web-push.ts",
"src/adapters/realtime",
"src/adapters/web-push",
"tests/fixtures/realtime-boundaries",
] as const;
const runtimeSourceRoots = runtimePaths.filter((entry) =>
entry.startsWith("src/"),
);
const runtimeScripts = [
"check:realtime-boundaries",
"check:realtime-boundaries:fixture",
"test:realtime-removal",
] as const;
const copyTargets = [
"src",
"tests",
"recipes",
"scripts",
"config",
"schemas",
"public",
".gitea",
".storybook",
"index.html",
"package.json",
"tsconfig.base.json",
"tsconfig.json",
"tsconfig.app.json",
"tsconfig.node.json",
"tsconfig.test.json",
"tsconfig.recipes.json",
"vite.config.ts",
"vitest.config.ts",
"playwright.config.ts",
"playwright.capabilities.config.ts",
"playwright.dev.config.ts",
"playwright.storybook.config.ts",
"playwright.visual.config.ts",
"eslint.config.ts",
".dependency-cruiser.json",
".nvmrc",
] as const;
function requireEnvironment(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`${name} is required for runtime removal verification`);
}
return value;
}
function runPnpm(script: string): boolean {
return (
spawnSync(process.execPath, [pnpmCli, script], {
cwd: fixtureRoot,
stdio: "inherit",
}).status === 0
);
}
async function sourceFiles(directory: string): Promise<string[]> {
const entries = await readdir(directory, { withFileTypes: true });
return (
await Promise.all(
entries.map(async (entry): Promise<string[]> => {
const target = path.join(directory, entry.name);
if (entry.isDirectory()) return await sourceFiles(target);
return /\.(?:[cm]?ts|tsx)$/u.test(entry.name)
? [path.resolve(target)]
: [];
}),
)
).flat();
}
function staticImportSpecifiers(source: string): string[] {
return [
...source.matchAll(
/(?:from\s*|import\s*\(\s*|import\s*)["']([^"']+)["']/gu,
),
]
.map((match) => match[1])
.filter((specifier): specifier is string =>
typeof specifier === "string",
);
}
function isWithin(target: string, root: string): boolean {
const relative = path.relative(root, target);
return (
relative === "" ||
(!relative.startsWith("..") && !path.isAbsolute(relative))
);
}
function resolvedImport(
importer: string,
specifier: string,
sourceSet: ReadonlySet<string>,
): string | null {
if (!specifier.startsWith(".")) return null;
const base = path.resolve(path.dirname(importer), specifier);
const candidates = [
base,
`${base}.ts`,
`${base}.tsx`,
`${base}.mts`,
`${base}.cts`,
path.join(base, "index.ts"),
path.join(base, "index.tsx"),
];
return candidates.find((candidate) => sourceSet.has(candidate)) ?? base;
}
async function runtimeImportGraph(root: string): Promise<Readonly<{
dependentTests: readonly string[];
importingFiles: readonly string[];
}>> {
const files = await sourceFiles(root);
const sourceSet = new Set(files);
const runtimeRoots = runtimeSourceRoots.map((entry) =>
path.resolve(root, entry),
);
const imports = new Map<string, readonly string[]>();
for (const file of files) {
const source = await readFile(file, "utf8");
imports.set(
file,
staticImportSpecifiers(source)
.map((specifier) => resolvedImport(file, specifier, sourceSet))
.filter((target): target is string => target !== null),
);
}
const memo = new Map<string, boolean>();
const reachesRuntime = (
file: string,
visiting = new Set<string>(),
): boolean => {
if (runtimeRoots.some((root) => isWithin(file, root))) return true;
const known = memo.get(file);
if (known !== undefined) return known;
if (visiting.has(file)) return false;
visiting.add(file);
const reaches = (imports.get(file) ?? []).some(
(dependency) =>
runtimeRoots.some((runtimeRoot) =>
isWithin(dependency, runtimeRoot),
) ||
(sourceSet.has(dependency) &&
reachesRuntime(dependency, visiting)),
);
visiting.delete(file);
memo.set(file, reaches);
return reaches;
};
const testsRoot = path.resolve(root, "tests");
return Object.freeze({
dependentTests: Object.freeze(
files.filter(
(file) => isWithin(file, testsRoot) && reachesRuntime(file),
),
),
importingFiles: Object.freeze(
files.filter(
(file) =>
!runtimeRoots.some((runtimeRoot) =>
isWithin(file, runtimeRoot),
) &&
(imports.get(file) ?? []).some((dependency) =>
runtimeRoots.some((runtimeRoot) =>
isWithin(dependency, runtimeRoot),
),
),
),
),
});
}
async function removeRuntimeDependentTests(root: string): Promise<number> {
const graph = await runtimeImportGraph(root);
await Promise.all(
graph.dependentTests.map(async (file) => {
if (isWithin(file, path.resolve(root, "tests"))) {
await rm(file, { force: true });
}
}),
);
return graph.dependentTests.length;
}
async function assertNoRuntimeImports(root: string): Promise<void> {
const graph = await runtimeImportGraph(root);
if (graph.importingFiles.length > 0) {
throw new Error(
`Removed realtime runtime is still imported by: ${graph.importingFiles
.map((file) => path.relative(root, file))
.join(", ")}`,
);
}
}
await rm(fixtureRoot, { recursive: true, force: true });
await mkdir(fixtureRoot, { recursive: true });
for (const target of copyTargets) {
await cp(target, path.join(fixtureRoot, target), { recursive: true });
}
await symlink(
path.resolve("node_modules"),
path.join(fixtureRoot, "node_modules"),
"dir",
);
const removedRuntimeTests =
await removeRuntimeDependentTests(fixtureRoot);
for (const runtimePath of runtimePaths) {
await rm(path.join(fixtureRoot, runtimePath), {
recursive: true,
force: true,
});
}
const outputPortsIndexPath = path.join(
fixtureRoot,
"src/application/ports/out/index.ts",
);
const outputPortsIndex = await readFile(outputPortsIndexPath, "utf8");
await writeFile(
outputPortsIndexPath,
outputPortsIndex.replace(
'export type { WebPushControlPort } from "./web-push-control.ts";\n',
"",
),
);
const catalogPath = path.join(
fixtureRoot,
"config/recipes/frontend-capability-recipes.json",
);
const catalog = JSON.parse(await readFile(catalogPath, "utf8")) as {
recipes: Array<Record<string, unknown>>;
};
const realtimeRecipe = catalog.recipes.find(
(recipe) => recipe.id === "realtime",
);
if (!realtimeRecipe || !Object.hasOwn(realtimeRecipe, "referenceRuntime")) {
throw new Error("Expected realtime reference runtime catalog entry");
}
delete realtimeRecipe.referenceRuntime;
await writeFile(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`);
const packagePath = path.join(fixtureRoot, "package.json");
const packageDocument = JSON.parse(await readFile(packagePath, "utf8")) as {
scripts: Record<string, string>;
};
for (const script of runtimeScripts) {
delete packageDocument.scripts[script];
}
await writeFile(
packagePath,
`${JSON.stringify(packageDocument, null, 2)}\n`,
);
for (const scriptPath of [
"scripts/check-realtime-boundaries.ts",
"scripts/check-realtime-boundary-fixtures.ts",
"scripts/lib/realtime-boundaries.ts",
"scripts/test-realtime-runtime-removal.ts",
]) {
await rm(path.join(fixtureRoot, scriptPath), { force: true });
}
const gatesPath = path.join(fixtureRoot, "config/ci/gates.json");
const gatesDocument = JSON.parse(await readFile(gatesPath, "utf8")) as {
gates: Record<
string,
{
steps: Array<{ script: string }>;
evidence: string[];
}
>;
};
for (const gate of Object.values(gatesDocument.gates)) {
gate.steps = gate.steps.filter(
({ script }) =>
!runtimeScripts.some((runtimeScript) => runtimeScript === script),
);
gate.evidence = gate.evidence.filter(
(evidence) =>
!evidence.includes("realtime-boundaries") &&
!evidence.includes("realtime-runtime-removal"),
);
}
await writeFile(
gatesPath,
`${JSON.stringify(gatesDocument, null, 2)}\n`,
);
await assertNoRuntimeImports(fixtureRoot);
const checks: Array<readonly [string, boolean]> = [
["typecheck", runPnpm("check:types")],
["lint", runPnpm("lint")],
["architecture", runPnpm("check:architecture")],
["test", runPnpm("test:all")],
["build", runPnpm("build")],
["optional-catalog", runPnpm("check:optional-recipes:source")],
["ci-contract", runPnpm("check:ci")],
];
const passed = checks.every(([, result]) => result);
await mkdir("artifacts/tests", { recursive: true });
await writeFile(
"artifacts/tests/realtime-runtime-removal.xml",
`<?xml version="1.0" encoding="UTF-8"?>\n` +
`<testsuite name="realtime-runtime-removal" tests="${checks.length}" failures="${passed ? 0 : 1}">` +
checks
.map(
([name, result]) =>
`<testcase name="${name}">${result ? "" : "<failure />"}</testcase>`,
)
.join("") +
`</testsuite>\n`,
);
await rm(fixtureRoot, { recursive: true, force: true });
if (!passed) {
process.stderr.write(
`Realtime runtime removal failed: ${checks
.filter(([, result]) => !result)
.map(([name]) => name)
.join(", ")}\n`,
);
process.exit(1);
}
process.stdout.write(
`Realtime runtime removal: PASS (${checks.length} base checks, ${removedRuntimeTests} runtime-dependent tests removed by import graph)\n`,
);
@@ -11,15 +11,17 @@ import {
import path from "node:path";
const fixtureRoot = path.resolve(".tmp/reference-feature-removal");
const pnpmCli = /** @type {string} */ (process.env.npm_execpath);
const pnpmCli = requireEnvironment("npm_execpath");
const featureSource = "src/features/reference-feature";
const featureTests = "tests/features/reference-feature";
const featureOwnedPaths = [
featureSource,
featureTests,
"tests/e2e/reference-form.spec.js",
"tests/e2e/reference-route.spec.js",
"tests/e2e/reference-form.spec.ts",
"tests/e2e/reference-route.spec.ts",
"tests/mocks",
"tests/fixtures/typecheck/invalid-feature-input.ts",
"tests/fixtures/typecheck/invalid-reference-operation.ts",
];
const copyTargets = [
"src",
@@ -28,6 +30,7 @@ const copyTargets = [
"scripts",
"config",
"public",
".storybook",
"index.html",
"package.json",
"tsconfig.base.json",
@@ -36,19 +39,21 @@ const copyTargets = [
"tsconfig.node.json",
"tsconfig.test.json",
"tsconfig.recipes.json",
"vite.config.js",
"vitest.config.js",
"playwright.config.js",
"eslint.config.js",
".dependency-cruiser.cjs",
"vite.config.ts",
"vitest.config.ts",
"playwright.config.ts",
"playwright.dev.config.ts",
"playwright.storybook.config.ts",
"playwright.visual.config.ts",
"eslint.config.ts",
".dependency-cruiser.json",
];
const emptyContracts = `import { PLATFORM_ROUTE_RUNTIME_CONTRACT } from "../contracts/route-runtime-contract.js";
import { PLATFORM_ROUTE_REGISTRY } from "../contracts/routes.js";
import { PLATFORM_SCHEMA_REGISTRY } from "../contracts/schema-registry.js";
const emptyContracts = `import { PLATFORM_ROUTE_RUNTIME_CONTRACT } from "../contracts/route-runtime-contract.ts";
import { PLATFORM_ROUTE_REGISTRY, type RouteDefinition } from "../contracts/routes.ts";
import { PLATFORM_SCHEMA_REGISTRY } from "../contracts/schema-registry.ts";
export const INSTALLED_FEATURE_CONTRACTS =
/** @type {readonly unknown[]} */ (Object.freeze([]));
export const INSTALLED_FEATURE_CONTRACTS: readonly unknown[] = Object.freeze([]);
export const ROUTE_REGISTRY = PLATFORM_ROUTE_REGISTRY;
export const ROUTE_RUNTIME_CONTRACT = PLATFORM_ROUTE_RUNTIME_CONTRACT;
export const API_OPERATIONS = Object.freeze({});
@@ -59,28 +64,22 @@ export const NAVIGATION_ROUTES = Object.freeze(
.filter((definition) => definition.navigationOrder !== null)
.sort(
(left, right) =>
/** @type {number} */ (left.navigationOrder) -
/** @type {number} */ (right.navigationOrder),
left.navigationOrder! - right.navigationOrder!,
),
);
/** @param {string} routeId */
export function getRoute(routeId) {
const registry =
/** @type {Readonly<Record<string, import("../contracts/routes.js").RouteDefinition>>} */ (
ROUTE_REGISTRY
);
export function getRoute(routeId: string): RouteDefinition {
const registry = ROUTE_REGISTRY as Readonly<Record<string, RouteDefinition>>;
const selected = registry[routeId];
if (!selected) throw new Error(\`Unregistered route: \${routeId}\`);
return selected;
}
/** @param {string} routeId */
export function routePath(routeId) {
export function routePath(routeId: string): string {
return getRoute(routeId).path;
}
`;
const emptyRuntimes = `import { PLATFORM_ROUTE_CODECS } from "../presentation/routes/platform-route-codecs.js";
import { PLATFORM_ROUTE_RUNTIME } from "../presentation/routes/route-runtime.js";
const emptyRuntimes = `import { PLATFORM_ROUTE_CODECS } from "../presentation/routes/platform-route-codecs.ts";
import { PLATFORM_ROUTE_RUNTIME } from "../presentation/routes/route-runtime.tsx";
export const ROUTE_CODECS = PLATFORM_ROUTE_CODECS;
export const ROUTE_RUNTIME = PLATFORM_ROUTE_RUNTIME;
@@ -101,8 +100,29 @@ const emptyMessages = `export const INSTALLED_MESSAGE_CATALOGS = Object.freeze({
});
`;
/** @param {string} directory @returns {Promise<string[]>} */
async function filesBelow(directory) {
type CoveragePolicy = {
criticalModules: Array<{ path?: string }>;
};
type EvidenceContribution = Readonly<{ owner?: string }>;
type EvidencePolicy = Record<
"scenarioCatalogs" | "sourceContracts",
unknown
>;
type GovernanceConsumer = Readonly<{ path?: string }>;
type GovernanceRegistry = Record<string, unknown> & {
consumers?: unknown;
consumerDirectories?: unknown;
};
type RemovalGovernance = { registries: GovernanceRegistry[] };
function requireEnvironment(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`${name} is required for sample removal`);
return value;
}
async function filesBelow(directory: string): Promise<string[]> {
const entries = await readdir(directory, { withFileTypes: true });
const groups = await Promise.all(
entries.map((entry) => {
@@ -113,8 +133,7 @@ async function filesBelow(directory) {
return groups.flat();
}
/** @param {string} script @param {string[]} [extra] */
function runPnpm(script, extra = []) {
function runPnpm(script: string, extra: string[] = []): boolean {
const result = spawnSync(process.execPath, [pnpmCli, script, ...extra], {
cwd: fixtureRoot,
stdio: "inherit",
@@ -136,7 +155,7 @@ for (const ownedPath of featureOwnedPaths) {
});
}
await writeFile(
path.join(fixtureRoot, "src/features/installed-feature-contracts.js"),
path.join(fixtureRoot, "src/features/installed-feature-contracts.ts"),
emptyContracts,
);
await writeFile(
@@ -148,32 +167,96 @@ await writeFile(
emptyAdapters,
);
await writeFile(
path.join(fixtureRoot, "src/features/installed-feature-messages.js"),
path.join(fixtureRoot, "src/features/installed-feature-messages.ts"),
emptyMessages,
);
const vitestConfigFile = path.join(fixtureRoot, "vitest.config.ts");
const vitestConfig = await readFile(vitestConfigFile, "utf8");
const featureCoverageInclude =
` "${featureSource}/adapters/reference-http-gateway.ts",\n`;
if (!vitestConfig.includes(featureCoverageInclude)) {
throw new Error("Reference feature coverage include is not registered");
}
await writeFile(
vitestConfigFile,
vitestConfig.replace(featureCoverageInclude, ""),
);
const coveragePolicyFile = path.join(
fixtureRoot,
"config/testing/risk-coverage.json",
);
const coveragePolicy = JSON.parse(
await readFile(coveragePolicyFile, "utf8"),
) as CoveragePolicy;
const retainedCriticalModules = coveragePolicy.criticalModules.filter(
(modulePolicy) => !modulePolicy.path?.startsWith(`${featureSource}/`),
);
if (
retainedCriticalModules.length === coveragePolicy.criticalModules.length
) {
throw new Error("Reference feature coverage policy is not registered");
}
coveragePolicy.criticalModules = retainedCriticalModules;
await writeFile(
coveragePolicyFile,
`${JSON.stringify(coveragePolicy, null, 2)}\n`,
);
const evidencePolicyFile = path.join(
fixtureRoot,
"config/testing/test-evidence.json",
);
const evidencePolicy = JSON.parse(
await readFile(evidencePolicyFile, "utf8"),
) as EvidencePolicy;
let removedEvidenceContributions = 0;
for (const policyKey of ["scenarioCatalogs", "sourceContracts"] as const) {
const contributions = evidencePolicy[policyKey];
if (!Array.isArray(contributions)) {
throw new Error(`Test evidence policy is missing ${policyKey}`);
}
evidencePolicy[policyKey] = contributions.filter((candidate: unknown) => {
const contribution = candidate as EvidenceContribution;
const retained = contribution.owner !== "reference-feature";
if (!retained) removedEvidenceContributions += 1;
return retained;
});
}
if (removedEvidenceContributions === 0) {
throw new Error("Reference feature test evidence policy is not registered");
}
await writeFile(
evidencePolicyFile,
`${JSON.stringify(evidencePolicy, null, 2)}\n`,
);
const governanceFile = path.join(
fixtureRoot,
"config/contracts/registry-governance.json",
);
const removalGovernance = JSON.parse(await readFile(governanceFile, "utf8"));
const removalGovernance = JSON.parse(
await readFile(governanceFile, "utf8"),
) as RemovalGovernance;
removalGovernance.registries = removalGovernance.registries.map(
/** @param {Record<string, unknown>} registry */
(registry) => ({
...registry,
...(Array.isArray(registry.consumers)
? {
consumers: registry.consumers.filter(
/** @param {{path?: string}} consumer */
(consumer) =>
!consumer.path?.includes("features/reference-feature"),
(candidate: unknown) => {
const consumer = candidate as GovernanceConsumer;
return !consumer.path?.includes("features/reference-feature");
},
),
}
: {}),
...(Array.isArray(registry.consumerDirectories)
? {
consumerDirectories: registry.consumerDirectories.filter(
/** @param {string} directory */
(directory) =>
(directory: unknown) =>
typeof directory !== "string" ||
!directory.includes("features/reference-feature"),
),
}
@@ -185,8 +268,7 @@ await writeFile(
`${JSON.stringify(removalGovernance, null, 2)}\n`,
);
/** @type {string[]} */
const residue = [];
const residue: string[] = [];
for (const root of ["src", "tests"]) {
for (const file of await filesBelow(path.join(fixtureRoot, root))) {
const relative = path.relative(fixtureRoot, file);
@@ -201,24 +283,25 @@ for (const root of ["src", "tests"]) {
}
}
const checks = [
const checks: Array<[string, boolean]> = [
["typecheck", runPnpm("check:types")],
["architecture", runPnpm("check:architecture")],
["registry-structure", runPnpm("check:registries:structure")],
["unit-integration", runPnpm("test:all")],
["coverage", runPnpm("test:coverage")],
["test-evidence-source", runPnpm("check:test-evidence:source")],
[
"home-smoke",
runPnpm("exec", [
"vitest",
"run",
"tests/component/router.test.jsx",
"tests/component/router.test.tsx",
"--reporter=default",
]),
],
["build", runPnpm("build")],
];
/** @type {string[]} */
const builtResidue = [];
const builtResidue: string[] = [];
for (const file of await filesBelow(path.join(fixtureRoot, "dist"))) {
if (!/\.(?:js|css|html|json)$/.test(file)) continue;
const content = await readFile(file, "utf8");
@@ -230,10 +313,10 @@ for (const file of await filesBelow(path.join(fixtureRoot, "dist"))) {
}
const routeCatalog = await import(
`${new URL(
"../src/features/installed-feature-contracts.js",
"../src/features/installed-feature-contracts.ts",
`file://${fixtureRoot}/scripts/`,
).href}?removed=${Date.now()}`
);
) as { ROUTE_REGISTRY: Readonly<Record<string, unknown>> };
const routeIds = Object.keys(routeCatalog.ROUTE_REGISTRY);
const routeAbsent = routeIds.every((routeId) => !routeId.startsWith("REFERENCE_"));
checks.push(["route-absent", routeAbsent]);
@@ -1,7 +1,7 @@
import { spawnSync } from "node:child_process";
import { readFile, writeFile } from "node:fs/promises";
import { supplyChainDigest } from "./lib/supply-chain.mjs";
import { supplyChainDigest } from "./lib/supply-chain.ts";
const owner = process.env.DEPENDENCY_BASELINE_OWNER;
const reason = process.env.DEPENDENCY_BASELINE_REASON;
@@ -12,10 +12,10 @@ if (!owner?.trim() || !reason?.trim()) {
process.exit(2);
}
const commands = /** @type {Array<[string, string[]]>} */ ([
const commands: Array<[string, string[]]> = [
["corepack", ["pnpm", "build"]],
["node", ["scripts/generate-supply-chain.mjs", "--no-baseline"]],
]);
["node", ["scripts/generate-supply-chain.ts", "--no-baseline"]],
];
for (const [command, args] of commands) {
const result = spawnSync(command, args, { stdio: "inherit" });
if (result.status !== 0) process.exit(result.status ?? 1);
@@ -1,7 +1,7 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { registrySnapshotDigest } from "./lib/registry-compatibility.mjs";
import { registrySnapshotDigest } from "./lib/registry-compatibility.ts";
const inputPath =
process.argv[2] ?? "artifacts/quality/registry-current-snapshot.json";
@@ -3,18 +3,19 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
import {
MANUAL_A11Y_ROUTE_IDS,
validateManualA11yEvidence,
} from "./lib/manual-a11y-evidence.mjs";
} from "./lib/manual-a11y-evidence.ts";
/** @type {Array<{
* routeId: string;
* path: string;
* reviewer: string | null;
* reviewedAt: string | null;
* releaseId: string | null;
* failures: readonly string[];
* passed: boolean;
* }>} */
const results = [];
type ManualA11yResult = Readonly<{
routeId: string;
path: string;
reviewer: string | null;
reviewedAt: string | null;
releaseId: string | null;
failures: readonly string[];
passed: boolean;
}>;
const results: ManualA11yResult[] = [];
for (const routeId of MANUAL_A11Y_ROUTE_IDS) {
const path = `artifacts/tests/a11y-manual/${routeId}.md`;
const evidence = await readFile(path, "utf8");
@@ -0,0 +1,89 @@
import { readFile } from "node:fs/promises";
const evidencePath =
"artifacts/tests/browser-capabilities/results.xml";
const xml = await readFile(evidencePath, "utf8");
const failures: string[] = [];
const rootAttributes =
/<testsuites\b([^>]*)>/u.exec(xml)?.[1] ?? "";
const root = attributes(rootAttributes);
for (const field of ["failures", "errors", "skipped"] as const) {
if (root[field] !== "0") {
failures.push(`root ${field} must be zero`);
}
}
if (!positiveInteger(root.tests)) {
failures.push("root tests must be positive");
}
const casesByEngine = new Map<string, Set<string>>();
for (const match of xml.matchAll(
/<testsuite\b([^>]*)>([\s\S]*?)<\/testsuite>/gu,
)) {
const suite = attributes(match[1] ?? "");
const engine = suite.hostname;
if (!engine) continue;
const cases =
casesByEngine.get(engine) ?? new Set<string>();
for (const testCase of (match[2] ?? "").matchAll(
/<testcase\b([^>]*)>/gu,
)) {
const data = attributes(testCase[1] ?? "");
if (data.classname && data.name) {
cases.add(`${data.classname}::${data.name}`);
}
}
casesByEngine.set(engine, cases);
}
const expectedEngines = ["chromium", "firefox", "webkit"] as const;
const baseline = casesByEngine.get(expectedEngines[0]);
for (const engine of expectedEngines) {
const cases = casesByEngine.get(engine);
if (!cases || cases.size === 0) {
failures.push(`${engine} has no executed browser-capability cases`);
continue;
}
if (
baseline &&
(cases.size !== baseline.size ||
[...baseline].some((testCase) => !cases.has(testCase)))
) {
failures.push(`${engine} case set differs from chromium`);
}
}
for (const engine of casesByEngine.keys()) {
if (!expectedEngines.includes(engine as (typeof expectedEngines)[number])) {
failures.push(`unexpected browser project ${engine}`);
}
}
if (/<skipped\b/u.test(xml) || /<failure\b/u.test(xml)) {
failures.push("browser-capability evidence contains skipped/failure nodes");
}
if (failures.length > 0) {
process.stderr.write(
`Browser capability evidence failed:\n- ${failures.join("\n- ")}\n`,
);
process.exit(1);
}
process.stdout.write(
`Browser capability evidence: PASS (${baseline?.size ?? 0} cases x ${expectedEngines.length} engines, zero skipped)\n`,
);
function attributes(source: string): Record<string, string> {
return Object.fromEntries(
[...source.matchAll(/([A-Za-z][A-Za-z0-9_-]*)="([^"]*)"/gu)].map(
(match) => [match[1] ?? "", match[2] ?? ""],
),
);
}
function positiveInteger(value: string | undefined): boolean {
return (
typeof value === "string" &&
/^\d+$/u.test(value) &&
Number(value) > 0
);
}
@@ -1,8 +1,27 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
type DocumentationReview = Readonly<{
sourcePath: string;
sha256: string;
thresholdSatisfied: boolean;
verdict: string;
score: number;
}>;
type ReviewLedger = Readonly<{
evidenceReport: Readonly<{
repoPath: string;
canonicalSha256: string;
}>;
reviews: Record<string, DocumentationReview>;
reviewer: string;
standard: string;
status: string;
}>;
const ledger = JSON.parse(
await readFile("docs/architecture/review-ledger.json", "utf8"),
);
) as ReviewLedger;
const evidence = await readFile(ledger.evidenceReport.repoPath, "utf8");
const results = [];
for (const [diagram, review] of Object.entries(ledger.reviews)) {
@@ -1,13 +1,62 @@
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
import { classifyLiveHostingBaseUrl } from "./lib/hosting-probe.mjs";
import { classifyLiveHostingBaseUrl } from "./lib/hosting-probe.ts";
const cachePolicy = JSON.parse(
await readFile("config/hosting/cache-policy.json", "utf8"),
);
const securityPolicy = JSON.parse(
await readFile("config/hosting/security-headers.json", "utf8"),
type Document = Record<string, unknown>;
type ResponseHeaders = Record<string, Record<string, string>>;
type HostingMode = "live" | "invalid-live" | "fixture";
type ProbeResult = Readonly<{
surface: string;
header: string;
expected: unknown;
observed: unknown;
reason?: string;
passed: boolean;
}>;
function isRecord(value: unknown): value is Document {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function recordValue(value: unknown): Document {
return isRecord(value) ? value : {};
}
function strings(value: unknown): string[] {
return Array.isArray(value)
? value.filter((entry): entry is string => typeof entry === "string")
: [];
}
function stringRecord(value: unknown): Record<string, string> {
return Object.fromEntries(
Object.entries(recordValue(value)).filter(
(entry): entry is [string, string] => typeof entry[1] === "string",
),
);
}
function responseHeaders(value: unknown): ResponseHeaders {
return Object.fromEntries(
Object.entries(recordValue(value)).map(([surface, headers]) => [
surface,
stringRecord(headers),
]),
);
}
async function readDocument(file: string): Promise<Document> {
const parsed: unknown = JSON.parse(await readFile(file, "utf8"));
if (!isRecord(parsed)) throw new Error(`${file} must be a JSON object`);
return parsed;
}
const cachePolicy = await readDocument("config/hosting/cache-policy.json");
const cacheSurfaces = recordValue(cachePolicy.surfaces);
const securityPolicy = await readDocument(
"config/hosting/security-headers.json",
);
const securityHeaders = stringRecord(securityPolicy.headers);
const baseUrl = process.env.HOSTING_BASE_URL;
const liveTarget = baseUrl ? classifyLiveHostingBaseUrl(baseUrl) : null;
const distFiles = (await readdir("dist", { recursive: true })).map(String);
@@ -16,18 +65,9 @@ const publicServiceWorkers = distFiles.filter((file) =>
/(?:^|\/)(?:service-worker|sw)(?:[.-][^/]*)?\.js$/i.test(file),
);
/** @type {Record<string, Record<string, string>>} */
let responses = {};
let mode;
/** @type {Array<{
* surface: string;
* header: string;
* expected: unknown;
* observed: unknown;
* reason?: string;
* passed: boolean;
* }>} */
const probeResults = [];
let responses: ResponseHeaders = {};
let mode: HostingMode;
const probeResults: ProbeResult[] = [];
if (liveTarget?.passed) {
mode = "live";
@@ -40,7 +80,6 @@ if (liveTarget?.passed) {
releaseManifest: "/release-manifest.json",
hashedAsset: `/assets/${hashedJavaScript}`,
};
responses = {};
for (const [surface, pathname] of Object.entries(paths)) {
const requestedUrl = new URL(pathname, liveTarget.url);
try {
@@ -68,7 +107,7 @@ if (liveTarget?.passed) {
value,
]),
);
} catch (error) {
} catch (error: unknown) {
probeResults.push({
surface,
header: "transport",
@@ -90,14 +129,17 @@ if (liveTarget?.passed) {
});
} else {
mode = "fixture";
responses = JSON.parse(
await readFile("config/hosting/response-headers.fixture.json", "utf8"),
).responses;
const fixture = await readDocument(
"config/hosting/response-headers.fixture.json",
);
responses = responseHeaders(fixture.responses);
}
const results = [...probeResults];
for (const [surface, policy] of Object.entries(cachePolicy.surfaces)) {
if (!("cacheControl" in policy)) continue;
const results: ProbeResult[] = [...probeResults];
for (const [surface, rawPolicy] of Object.entries(cacheSurfaces)) {
const policy = recordValue(rawPolicy);
if (typeof policy.cacheControl !== "string") continue;
const contentTypes = strings(policy.contentTypes);
const observed = responses[surface]?.["cache-control"];
results.push({
surface,
@@ -109,17 +151,17 @@ for (const [surface, policy] of Object.entries(cachePolicy.surfaces)) {
const observedContentType = responses[surface]?.["content-type"];
const observedMime = observedContentType
?.split(";", 1)[0]
.trim()
?.trim()
.toLowerCase();
results.push({
surface,
header: "content-type",
expected: policy.contentTypes,
expected: contentTypes,
observed: observedContentType,
passed: policy.contentTypes.includes(observedMime),
passed: observedMime !== undefined && contentTypes.includes(observedMime),
});
if (policy.securityHeaders) {
for (const [header, expected] of Object.entries(securityPolicy.headers)) {
if (policy.securityHeaders === true) {
for (const [header, expected] of Object.entries(securityHeaders)) {
const observedSecurity = responses[surface]?.[header.toLowerCase()];
results.push({
surface,
@@ -132,23 +174,22 @@ for (const [surface, policy] of Object.entries(cachePolicy.surfaces)) {
}
}
const sourceMapPolicy = recordValue(cacheSurfaces.sourceMap);
results.push({
surface: "sourceMap",
header: "public",
expected: false,
observed: publicSourceMaps.length > 0,
passed:
cachePolicy.surfaces.sourceMap.public === false &&
publicSourceMaps.length === 0,
passed: sourceMapPolicy.public === false && publicSourceMaps.length === 0,
});
const serviceWorkerPolicy = recordValue(cacheSurfaces.serviceWorker);
results.push({
surface: "serviceWorker",
header: "enabled",
expected: false,
observed: publicServiceWorkers.length > 0,
passed:
cachePolicy.surfaces.serviceWorker.enabled === false &&
publicServiceWorkers.length === 0,
serviceWorkerPolicy.enabled === false && publicServiceWorkers.length === 0,
});
const passed = results.every((result) => result.passed);
-164
View File
@@ -1,164 +0,0 @@
import { createHash } from "node:crypto";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { verifyCompatibilityTuple } from "../src/application/policies/compatibility.js";
import {
compareReleaseToRuntime,
RELEASE_TOKEN_REGISTRY,
} from "../src/contracts/release-tokens.js";
import {
ROUTE_REGISTRY,
ROUTE_RUNTIME_CONTRACT,
} from "../src/features/installed-feature-contracts.js";
const fixturesDocument =
/** @type {{
* fixtures: Array<{
* name: string,
* expectedCompatible: boolean,
* frontend: {
* buildId: string,
* configSchemaVersion: string,
* apiContractVersion: string,
* assetManifestHash: string,
* releaseId: string
* },
* runtime: {
* buildId: string,
* configSchemaVersion: string,
* apiContractVersion: string,
* assetManifestHash: string,
* releaseId: string
* }
* }>
* }} */ (
JSON.parse(
await readFile("config/release/coherence-fixtures.json", "utf8"),
)
);
const release = JSON.parse(await readFile("dist/release-manifest.json", "utf8"));
const runtimeConfig = JSON.parse(await readFile("dist/config.json", "utf8"));
const buildManifest = JSON.parse(
await readFile("artifacts/release/build-manifest.json", "utf8"),
);
const runtimeConfigJsonSchema = JSON.parse(
await readFile("dist/runtime-config.schema.json", "utf8"),
);
const viteManifest = await readFile("dist/.vite/manifest.json", "utf8");
const viteManifestObject =
/** @type {Record<string, {file: string, name?: string, isDynamicEntry?: boolean}>} */ (
JSON.parse(viteManifest)
);
const actualAssetManifestHash = createHash("sha256")
.update(viteManifest)
.digest("hex");
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");
}
if (
runtimeConfigJsonSchema.$schema !== "https://json-schema.org/draft/2020-12/schema" ||
runtimeConfigJsonSchema.type !== "object" ||
!runtimeConfigJsonSchema.properties
) {
artifactMismatches.push("runtimeConfigSchema");
}
if (
buildManifest.outputs?.runtimeConfigSchema !==
"dist/runtime-config.schema.json"
) {
artifactMismatches.push("buildManifest:runtimeConfigSchema");
}
const expectedChunkIds = new Set(
Object.values(ROUTE_REGISTRY).map((definition) => definition.chunkId),
);
const actualChunkIds = new Set(Object.keys(release.routeChunks ?? {}));
for (const chunkId of expectedChunkIds) {
if (!actualChunkIds.has(chunkId)) {
artifactMismatches.push(`routeChunk:missing:${chunkId}`);
}
}
for (const chunkId of actualChunkIds) {
if (!expectedChunkIds.has(chunkId)) {
artifactMismatches.push(`routeChunk:orphan:${chunkId}`);
}
}
for (const definition of Object.values(ROUTE_REGISTRY)) {
const runtime =
/** @type {Record<string, {moduleId: string}>} */ (
ROUTE_RUNTIME_CONTRACT
)[definition.routeId];
const viteEntry = Object.values(viteManifestObject).find(
(entry) => entry.name === runtime?.moduleId && entry.isDynamicEntry,
);
const routeAsset = release.routeChunks?.[definition.chunkId];
if (!runtime || !viteEntry || routeAsset !== viteEntry.file) {
artifactMismatches.push(`routeChunk:mismatch:${definition.chunkId}`);
continue;
}
if (
buildManifest.outputs?.routeChunks?.[definition.chunkId] !== routeAsset
) {
artifactMismatches.push(`buildManifest:routeChunk:${definition.chunkId}`);
}
try {
await readFile(`dist/${routeAsset}`);
} catch {
artifactMismatches.push(`routeChunk:file:${definition.chunkId}`);
}
}
const fixtures = fixturesDocument.fixtures.map((fixture) => {
const result = verifyCompatibilityTuple({
frontend: fixture.frontend,
runtime: fixture.runtime,
});
return {
name: fixture.name,
expectedCompatible: fixture.expectedCompatible,
actualCompatible: result.compatible,
mismatches: result.mismatches,
passed: result.compatible === fixture.expectedCompatible,
};
});
const artifact = {
checked: true,
compatible: artifactComparison.compatible && artifactMismatches.length === 0,
mismatches: artifactMismatches,
releaseId: release.releaseId,
};
const passed = artifact.compatible && fixtures.every((fixture) => fixture.passed);
const report = {
schemaVersion: 1,
generatedAt: new Date().toISOString(),
artifact,
fixtures,
passed,
};
await mkdir("artifacts/release", { recursive: true });
await writeFile(
"artifacts/release/verification.json",
`${JSON.stringify(report, null, 2)}\n`,
);
if (!passed) {
process.stderr.write(
`Release coherence failed: ${artifactMismatches.join(", ") || "fixture"}\n`,
);
process.exit(1);
}
process.stdout.write(
`Release coherence: PASS (${fixtures.length - 1} mixed fixtures rejected)\n`,
);
+320
View File
@@ -0,0 +1,320 @@
import { createHash } from "node:crypto";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import {
verifyCompatibilityTuple,
type CompatibilityTuple,
} from "../src/application/policies/compatibility.ts";
import {
compareReleaseToRuntime,
RELEASE_TOKEN_REGISTRY,
} from "../src/contracts/release-tokens.ts";
import {
ROUTE_REGISTRY,
ROUTE_RUNTIME_CONTRACT,
} from "../src/features/installed-feature-contracts.ts";
type CoherenceFixture = Readonly<{
name: string;
expectedCompatible: boolean;
frontend: CompatibilityTuple;
runtime: CompatibilityTuple;
}>;
type ReleaseDocument = CompatibilityTuple &
Record<string, unknown> &
Readonly<{ releaseId: string; routeChunks: Readonly<Record<string, unknown>> }>;
type RuntimeConfigDocument = Readonly<{
BUILD_ID: string;
CONFIG_SCHEMA_VERSION: string;
API_CONTRACT_VERSION: string;
RELEASE_ID: string;
}>;
type BuildManifestDocument = Readonly<
Record<string, unknown> & {
outputs?: Readonly<{
runtimeConfigSchema?: unknown;
routeChunks?: Readonly<Record<string, unknown>>;
}>;
}
>;
type ViteManifestEntry = Readonly<{
file: string;
name?: string;
isDynamicEntry?: boolean;
}>;
const fixturesDocument = parseFixturesDocument(
JSON.parse(
await readFile("config/release/coherence-fixtures.json", "utf8"),
),
);
const release = parseReleaseDocument(
JSON.parse(await readFile("dist/release-manifest.json", "utf8")),
);
const runtimeConfig = parseRuntimeConfigDocument(
JSON.parse(await readFile("dist/config.json", "utf8")),
);
const buildManifest = parseBuildManifestDocument(
JSON.parse(await readFile("artifacts/release/build-manifest.json", "utf8")),
);
const runtimeConfigJsonSchema = requireRecord(
JSON.parse(await readFile("dist/runtime-config.schema.json", "utf8")),
"runtime config JSON schema",
);
const viteManifest = await readFile("dist/.vite/manifest.json", "utf8");
const viteManifestObject = parseViteManifest(JSON.parse(viteManifest));
const actualAssetManifestHash = createHash("sha256")
.update(viteManifest)
.digest("hex");
const artifactComparison = compareReleaseToRuntime(release, runtimeConfig);
const artifactMismatches: string[] = [...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 (
typeof release.builtAt !== "string" ||
!Number.isFinite(Date.parse(release.builtAt))
) {
artifactMismatches.push("releaseToken:builtAtFormat");
}
if (release.assetManifestHash !== actualAssetManifestHash) {
artifactMismatches.push("assetManifestContent");
}
if (
runtimeConfigJsonSchema.$schema !== "https://json-schema.org/draft/2020-12/schema" ||
runtimeConfigJsonSchema.type !== "object" ||
!runtimeConfigJsonSchema.properties
) {
artifactMismatches.push("runtimeConfigSchema");
}
if (
buildManifest.outputs?.runtimeConfigSchema !==
"dist/runtime-config.schema.json"
) {
artifactMismatches.push("buildManifest:runtimeConfigSchema");
}
for (const [buildToken, releaseToken] of [
["buildId", "buildId"],
["commitSha", "commitSha"],
["releaseId", "releaseId"],
["generatedAt", "builtAt"],
]) {
if (buildManifest[buildToken] !== release[releaseToken]) {
artifactMismatches.push(`buildManifest:${buildToken}`);
}
}
const expectedChunkIds = new Set(
Object.values(ROUTE_REGISTRY).map((definition) => definition.chunkId),
);
const actualChunkIds = new Set(Object.keys(release.routeChunks));
for (const chunkId of expectedChunkIds) {
if (!actualChunkIds.has(chunkId)) {
artifactMismatches.push(`routeChunk:missing:${chunkId}`);
}
}
for (const chunkId of actualChunkIds) {
if (!expectedChunkIds.has(chunkId)) {
artifactMismatches.push(`routeChunk:orphan:${chunkId}`);
}
}
const runtimeContracts: Readonly<Record<string, { moduleId: string }>> =
ROUTE_RUNTIME_CONTRACT;
for (const definition of Object.values(ROUTE_REGISTRY)) {
const runtime = runtimeContracts[definition.routeId];
const viteEntry = Object.values(viteManifestObject).find(
(entry) => entry.name === runtime?.moduleId && entry.isDynamicEntry,
);
const routeAsset = release.routeChunks[definition.chunkId];
if (!runtime || !viteEntry || routeAsset !== viteEntry.file) {
artifactMismatches.push(`routeChunk:mismatch:${definition.chunkId}`);
continue;
}
if (
buildManifest.outputs?.routeChunks?.[definition.chunkId] !== routeAsset
) {
artifactMismatches.push(`buildManifest:routeChunk:${definition.chunkId}`);
}
try {
await readFile(`dist/${routeAsset}`);
} catch {
artifactMismatches.push(`routeChunk:file:${definition.chunkId}`);
}
}
const fixtures = fixturesDocument.fixtures.map((fixture) => {
const result = verifyCompatibilityTuple({
frontend: fixture.frontend,
runtime: fixture.runtime,
});
return {
name: fixture.name,
expectedCompatible: fixture.expectedCompatible,
actualCompatible: result.compatible,
mismatches: result.mismatches,
passed: result.compatible === fixture.expectedCompatible,
};
});
const artifact = {
checked: true,
compatible: artifactComparison.compatible && artifactMismatches.length === 0,
mismatches: artifactMismatches,
releaseId: release.releaseId,
};
const passed = artifact.compatible && fixtures.every((fixture) => fixture.passed);
const report = {
schemaVersion: 1,
generatedAt: new Date().toISOString(),
artifact,
fixtures,
passed,
};
await mkdir("artifacts/release", { recursive: true });
await writeFile(
"artifacts/release/verification.json",
`${JSON.stringify(report, null, 2)}\n`,
);
if (!passed) {
process.stderr.write(
`Release coherence failed: ${artifactMismatches.join(", ") || "fixture"}\n`,
);
process.exit(1);
}
process.stdout.write(
`Release coherence: PASS (${fixtures.length - 1} mixed fixtures rejected)\n`,
);
function parseFixturesDocument(value: unknown): Readonly<{
fixtures: readonly CoherenceFixture[];
}> {
const document = requireRecord(value, "release coherence fixtures");
if (!Array.isArray(document.fixtures)) {
throw new TypeError("release coherence fixtures must be an array");
}
return {
fixtures: document.fixtures.map((candidate, index) => {
const fixture = requireRecord(candidate, `release fixture ${index}`);
if (
typeof fixture.name !== "string" ||
typeof fixture.expectedCompatible !== "boolean"
) {
throw new TypeError(`Invalid release fixture metadata: ${index}`);
}
return {
name: fixture.name,
expectedCompatible: fixture.expectedCompatible,
frontend: parseCompatibilityTuple(
fixture.frontend,
`release fixture ${index}.frontend`,
),
runtime: parseCompatibilityTuple(
fixture.runtime,
`release fixture ${index}.runtime`,
),
};
}),
};
}
function parseReleaseDocument(value: unknown): ReleaseDocument {
const document = requireRecord(value, "release manifest");
const tuple = parseCompatibilityTuple(document, "release manifest");
const routeChunks = isRecord(document.routeChunks)
? document.routeChunks
: {};
return { ...document, ...tuple, routeChunks };
}
function parseRuntimeConfigDocument(value: unknown): RuntimeConfigDocument {
const document = requireRecord(value, "runtime config");
return {
BUILD_ID: requireString(document.BUILD_ID, "runtime config BUILD_ID"),
CONFIG_SCHEMA_VERSION: requireString(
document.CONFIG_SCHEMA_VERSION,
"runtime config CONFIG_SCHEMA_VERSION",
),
API_CONTRACT_VERSION: requireString(
document.API_CONTRACT_VERSION,
"runtime config API_CONTRACT_VERSION",
),
RELEASE_ID: requireString(
document.RELEASE_ID,
"runtime config RELEASE_ID",
),
};
}
function parseBuildManifestDocument(value: unknown): BuildManifestDocument {
const document = requireRecord(value, "build manifest");
const outputs = isRecord(document.outputs)
? {
runtimeConfigSchema: document.outputs.runtimeConfigSchema,
routeChunks: isRecord(document.outputs.routeChunks)
? document.outputs.routeChunks
: undefined,
}
: undefined;
return { ...document, ...(outputs ? { outputs } : {}) };
}
function parseCompatibilityTuple(value: unknown, label: string): CompatibilityTuple {
const document = requireRecord(value, label);
return {
buildId: requireString(document.buildId, `${label}.buildId`),
configSchemaVersion: requireString(
document.configSchemaVersion,
`${label}.configSchemaVersion`,
),
apiContractVersion: requireString(
document.apiContractVersion,
`${label}.apiContractVersion`,
),
assetManifestHash: requireString(
document.assetManifestHash,
`${label}.assetManifestHash`,
),
releaseId: requireString(document.releaseId, `${label}.releaseId`),
};
}
function parseViteManifest(
value: unknown,
): Readonly<Record<string, ViteManifestEntry>> {
const document = requireRecord(value, "Vite manifest");
const entries: Record<string, ViteManifestEntry> = {};
for (const [key, candidate] of Object.entries(document)) {
const entry = requireRecord(candidate, `Vite manifest entry ${key}`);
entries[key] = {
file: requireString(entry.file, `Vite manifest entry ${key}.file`),
...(typeof entry.name === "string" ? { name: entry.name } : {}),
...(typeof entry.isDynamicEntry === "boolean"
? { isDynamicEntry: entry.isDynamicEntry }
: {}),
};
}
return entries;
}
function requireString(value: unknown, label: string): string {
if (typeof value !== "string" || value.length === 0) {
throw new TypeError(`${label} must be a non-empty string`);
}
return value;
}
function requireRecord(
value: unknown,
label: string,
): Record<string, unknown> {
if (!isRecord(value)) throw new TypeError(`${label} must be an object`);
return value;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
@@ -2,21 +2,23 @@ import { spawnSync } from "node:child_process";
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { supplyChainDigest } from "./lib/supply-chain.mjs";
import { assertCiBuildEnvironment } from "./lib/build-environment.ts";
import { supplyChainDigest } from "./lib/supply-chain.ts";
/** @param {string} directory @returns {Promise<string[]>} */
async function filesWithin(directory) {
assertCiBuildEnvironment(process.env);
async function filesWithin(directory: string): Promise<string[]> {
const entries = await readdir(directory, { withFileTypes: true });
const nested = /** @type {string[][]} */ (await Promise.all(
const nested: string[][] = await Promise.all(
entries.map((entry) => {
const target = path.join(directory, entry.name);
return entry.isDirectory() ? filesWithin(target) : [target];
}),
));
);
return nested.flat().sort();
}
async function distDigest() {
async function distDigest(): Promise<string> {
const rows = await Promise.all(
(await filesWithin("dist")).map(async (file) => ({
path: path.relative("dist", file).replaceAll("\\", "/"),
@@ -27,7 +29,7 @@ async function distDigest() {
return supplyChainDigest(rows);
}
function build(environment = process.env) {
function build(environment: NodeJS.ProcessEnv = process.env) {
return spawnSync("corepack", ["pnpm", "build"], {
env: environment,
encoding: "utf8",
@@ -58,6 +60,11 @@ await writeFile(
{
schemaVersion: 1,
sourceDateEpoch: deterministicEnvironment.SOURCE_DATE_EPOCH,
buildId: process.env.VITE_BUILD_ID ?? "local-build",
commitSha: process.env.VITE_COMMIT_SHA ?? "local",
releaseId: process.env.RELEASE_ID ?? "local-release",
runnerImage:
process.env.CI_RUNNER_IMAGE ?? `${process.platform}-${process.arch}`,
firstDigest,
secondDigest,
restored: restoreBuild.status === 0,
@@ -7,34 +7,46 @@ import {
parsePnpmLockfilePackages,
supplyChainDigest,
verifySupplyChainCoherence,
} from "./lib/supply-chain.mjs";
} from "./lib/supply-chain.ts";
/** @param {string} directory @returns {Promise<string[]>} */
async function filesWithin(directory) {
type Document = Record<string, unknown>;
function isRecord(value: unknown): value is Document {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function parseDocument(text: string, label: string): Document {
const parsed: unknown = JSON.parse(text);
if (!isRecord(parsed)) throw new Error(`${label} must be a JSON object`);
return parsed;
}
function recordRows(value: unknown): Document[] {
return Array.isArray(value) ? value.filter(isRecord) : [];
}
async function readDocument(file: string): Promise<Document> {
return parseDocument(await readFile(file, "utf8"), file);
}
async function filesWithin(directory: string): Promise<string[]> {
const entries = await readdir(directory, { withFileTypes: true });
const nested = /** @type {string[][]} */ (await Promise.all(
const nested: string[][] = await Promise.all(
entries.map((entry) => {
const target = path.join(directory, entry.name);
return entry.isDirectory() ? filesWithin(target) : [target];
}),
));
);
return nested.flat().sort();
}
const inventory = JSON.parse(
await readFile("artifacts/release/dependency-inventory.json", "utf8"),
const inventory = await readDocument(
"artifacts/release/dependency-inventory.json",
);
const sbom = JSON.parse(
await readFile("artifacts/release/sbom.cdx.json", "utf8"),
);
const provenance = JSON.parse(
await readFile("artifacts/release/provenance.json", "utf8"),
);
const verification = JSON.parse(
await readFile(
"artifacts/security/supply-chain-verification.json",
"utf8",
),
const sbom = await readDocument("artifacts/release/sbom.cdx.json");
const provenance = await readDocument("artifacts/release/provenance.json");
const verification = await readDocument(
"artifacts/security/supply-chain-verification.json",
);
const lockfileText = await readFile("pnpm-lock.yaml", "utf8");
const lockfileSha256 = createHash("sha256")
@@ -57,7 +69,7 @@ const coherence = verifySupplyChainCoherence(
provenance,
distDigest,
);
const failures = [...coherence.failures];
const failures: string[] = [...coherence.failures];
if (
inventory.lockfileSha256 !== lockfileSha256 ||
verification.lockfileSha256 !== lockfileSha256
@@ -71,15 +83,14 @@ if (
failures.push("verification digest set is incoherent");
}
const lockRows = parsePnpmLockfilePackages(lockfileText);
const inventoryRows =
/** @type {Array<Record<string, unknown>>} */ (
inventory.dependencies ?? []
);
const inventoryByIdentity = new Map(
inventoryRows.map((entry) => [
`${entry.name}@${entry.version}`,
entry,
]),
const inventoryRows = recordRows(inventory.dependencies);
const inventoryByIdentity = new Map<string, Document>(
inventoryRows.map(
(entry) => [
`${String(entry.name ?? "")}@${String(entry.version ?? "")}`,
entry,
] as const,
),
);
if (lockRows.length !== inventoryRows.length) {
failures.push("transitive dependency count differs from lockfile");
@@ -1,6 +1,6 @@
import { mkdir, writeFile } from "node:fs/promises";
import { MANUAL_A11Y_ROUTE_IDS } from "./lib/manual-a11y-evidence.mjs";
import { MANUAL_A11Y_ROUTE_IDS } from "./lib/manual-a11y-evidence.ts";
await mkdir("artifacts/tests", { recursive: true });
await writeFile(