fix: complete TechLog migration evidence
This commit is contained in:
@@ -15,7 +15,8 @@ import { INSTALLED_RUNTIME_CAPABILITIES } from "../src/features/installed-runtim
|
||||
* 3. Vite app build (emptyOutDir = true)
|
||||
* 4. scan app dist and generate the static asset source
|
||||
* 5. ACTIVE only: Vite Service Worker build (emptyOutDir = false)
|
||||
* 6. generate Release Manifest V2 and the build manifest
|
||||
* 6. generate the self-contained TechLog production serving boundary
|
||||
* 7. generate Release Manifest V2 and the build manifest
|
||||
*
|
||||
* Steps 4 and 5 are skipped for `REMOVE_REGISTRATION`, `PURGE_OWNED_RESOURCES`
|
||||
* and `null`: those modes never run an active worker build.
|
||||
@@ -56,5 +57,8 @@ if (buildsActiveWorker) {
|
||||
);
|
||||
}
|
||||
|
||||
// 6. release + build manifest
|
||||
// 6. production serving boundary
|
||||
run("node", ["scripts/generate-tech-log-serving-artifact.ts"]);
|
||||
|
||||
// 7. release + build manifest
|
||||
run("node", ["scripts/generate-build-manifest.ts"]);
|
||||
|
||||
@@ -5,6 +5,7 @@ import path from "node:path";
|
||||
import { z } from "zod";
|
||||
|
||||
import { PROMOTION_FORMULA } from "../../src/application/policies/promotion-readiness.ts";
|
||||
import { MANUAL_A11Y_ROUTE_IDS } from "../lib/manual-a11y-evidence.ts";
|
||||
import {
|
||||
RELEASE_CANDIDATE_EVIDENCE_PATHS,
|
||||
RELEASE_CANDIDATE_MANIFEST_PATH,
|
||||
@@ -442,7 +443,7 @@ export type LoadCiGateContractOptions = Readonly<{
|
||||
}>;
|
||||
|
||||
const CANONICAL_GATE_SHAPE_SHA256 =
|
||||
"a4a963d0b9deffb7a0a3d755bbbcb979d72610eb74751c3a2e5eca55251e12d4";
|
||||
"492331de6d07926b1cdb569824c8feaa761eef76b9ac782c0404d40797383ef0";
|
||||
|
||||
function canonicalGateShapeSha256(gates: CiGateContract["gates"]): string {
|
||||
const normalized = gates.map(
|
||||
@@ -481,8 +482,8 @@ function canonicalAuthorityBaselineFailures(contract: CiGateContract): string[]
|
||||
`command authority baseline must contain exactly 81 definitions and 93 references; received ${contract.commands.length} definitions and ${commandReferenceCount} references`,
|
||||
);
|
||||
}
|
||||
if (contract.artifacts.length !== 105) {
|
||||
failures.push(`artifact authority baseline must contain exactly 105 artifacts; received ${contract.artifacts.length}`);
|
||||
if (contract.artifacts.length !== 126) {
|
||||
failures.push(`artifact authority baseline must contain exactly 126 artifacts; received ${contract.artifacts.length}`);
|
||||
}
|
||||
if (contract.stages.length !== 5) {
|
||||
failures.push(`stage authority baseline must contain exactly 5 stages; received ${contract.stages.length}`);
|
||||
@@ -723,6 +724,44 @@ function validateContractSemantics(
|
||||
issue(`unknown retention class ${gate.retentionClassId} for ${gate.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
const accessibilityGate = contract.gates.find(
|
||||
({ id: gateId }) => gateId === "FE-GATE-009",
|
||||
);
|
||||
const manualAccessibilityArtifacts = MANUAL_A11Y_ROUTE_IDS.map((routeId) => ({
|
||||
id: `artifact-artifacts-tests-a11y-manual-${routeId.replaceAll("_", "-")}-md`,
|
||||
path: `artifacts/tests/a11y-manual/${routeId}.md`,
|
||||
}));
|
||||
const expectedAccessibilityEvidenceArtifactIds = [
|
||||
"artifact-artifacts-tests-a11y-json",
|
||||
...manualAccessibilityArtifacts.map(({ id: artifactId }) => artifactId),
|
||||
"artifact-artifacts-tests-a11y-manual-report-json",
|
||||
];
|
||||
if (
|
||||
!accessibilityGate ||
|
||||
JSON.stringify(accessibilityGate.evidenceArtifactIds) !==
|
||||
JSON.stringify(expectedAccessibilityEvidenceArtifactIds)
|
||||
) {
|
||||
issue(
|
||||
"FE-GATE-009 manual accessibility evidence must exactly match the installed route scope",
|
||||
);
|
||||
}
|
||||
for (const expected of manualAccessibilityArtifacts) {
|
||||
const artifact = contract.artifacts.find(
|
||||
({ id: artifactId }) => artifactId === expected.id,
|
||||
);
|
||||
if (
|
||||
!artifact ||
|
||||
artifact.path !== expected.path ||
|
||||
artifact.schemaId !== "markdown" ||
|
||||
artifact.production !== "source-controlled"
|
||||
) {
|
||||
issue(
|
||||
`FE-GATE-009 manual accessibility artifact registration is invalid: ${expected.id}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const referencedRetentionClasses = new Set(
|
||||
contract.gates.map(({ retentionClassId }) => retentionClassId),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import {
|
||||
projects,
|
||||
publicRecords,
|
||||
releases,
|
||||
} from "../src/features/tech-log/adapters/static/public-content.ts";
|
||||
import { writeTechLogServingArtifact } from "./lib/tech-log-serving-artifact.ts";
|
||||
import { createTechLogServingContract } from "./lib/tech-log-serving-contract.ts";
|
||||
|
||||
const contract = createTechLogServingContract({
|
||||
projects,
|
||||
publicRecords,
|
||||
releases,
|
||||
});
|
||||
|
||||
await writeTechLogServingArtifact({ distRoot: "dist", contract });
|
||||
@@ -0,0 +1,133 @@
|
||||
// @ts-nocheck -- copied byte-for-byte into the self-contained Node deployment artifact.
|
||||
import { readFile, stat } from "node:fs/promises";
|
||||
import { createServer } from "node:http";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
const contentTypes = Object.freeze({
|
||||
".css": "text/css;charset=UTF-8",
|
||||
".html": "text/html;charset=UTF-8",
|
||||
".js": "text/javascript;charset=UTF-8",
|
||||
".json": "application/json",
|
||||
".png": "image/png",
|
||||
".svg": "image/svg+xml",
|
||||
".woff": "font/woff",
|
||||
".woff2": "font/woff2",
|
||||
});
|
||||
|
||||
function isStudioPath(pathname, prefix) {
|
||||
return pathname === prefix || pathname.startsWith(`${prefix}/`);
|
||||
}
|
||||
|
||||
async function sendFile(
|
||||
file,
|
||||
method,
|
||||
response,
|
||||
status = 200,
|
||||
) {
|
||||
const body = await readFile(file);
|
||||
response.writeHead(status, {
|
||||
"Cache-Control": "no-cache",
|
||||
"Content-Length": body.byteLength,
|
||||
"Content-Type": contentTypes[path.extname(file)] ?? "application/octet-stream",
|
||||
Vary: "Origin",
|
||||
});
|
||||
if (method === "HEAD") {
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
response.end(body);
|
||||
}
|
||||
|
||||
export function createTechLogProductionServer({
|
||||
root,
|
||||
contract,
|
||||
}) {
|
||||
const absoluteRoot = path.resolve(root);
|
||||
const publicSpaPaths = new Set(contract.publicSpaPaths);
|
||||
const studioSpaPathPatterns = contract.studioSpaPathPatterns.map(
|
||||
(pattern) => new RegExp(pattern),
|
||||
);
|
||||
const server = createServer(async (request, response) => {
|
||||
if (request.method !== "GET" && request.method !== "HEAD") {
|
||||
response.writeHead(405, { Allow: "GET, HEAD" }).end();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
||||
const pathname = decodeURIComponent(url.pathname);
|
||||
const requested = path.resolve(absoluteRoot, `.${pathname}`);
|
||||
if (
|
||||
requested !== absoluteRoot &&
|
||||
requested.startsWith(`${absoluteRoot}${path.sep}`)
|
||||
) {
|
||||
const details = await stat(requested).catch(() => null);
|
||||
if (details?.isFile()) {
|
||||
await sendFile(requested, request.method, response);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (
|
||||
publicSpaPaths.has(pathname) ||
|
||||
studioSpaPathPatterns.some((pattern) => pattern.test(pathname))
|
||||
) {
|
||||
await sendFile(path.join(absoluteRoot, "index.html"), request.method, response);
|
||||
return;
|
||||
}
|
||||
if (isStudioPath(pathname, contract.studioPathPrefix)) {
|
||||
await sendFile(
|
||||
path.join(absoluteRoot, "index.html"),
|
||||
request.method,
|
||||
response,
|
||||
contract.notFound.status,
|
||||
);
|
||||
return;
|
||||
}
|
||||
response.writeHead(contract.notFound.status, {
|
||||
"Content-Type": contract.notFound.contentType,
|
||||
});
|
||||
response.end(request.method === "HEAD" ? undefined : contract.notFound.body);
|
||||
} catch {
|
||||
response.writeHead(contract.notFound.status, {
|
||||
"Content-Type": contract.notFound.contentType,
|
||||
});
|
||||
response.end(request.method === "HEAD" ? undefined : contract.notFound.body);
|
||||
}
|
||||
});
|
||||
return server;
|
||||
}
|
||||
|
||||
function optionValue(name) {
|
||||
const index = process.argv.indexOf(name);
|
||||
return index >= 0 ? process.argv[index + 1] : undefined;
|
||||
}
|
||||
|
||||
async function startFromCommandLine() {
|
||||
const root = path.resolve(
|
||||
optionValue("--root") ?? path.dirname(fileURLToPath(import.meta.url)),
|
||||
);
|
||||
const host = optionValue("--host") ?? "127.0.0.1";
|
||||
const port = Number(optionValue("--port") ?? 4173);
|
||||
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
|
||||
throw new TypeError("--port must be an integer from 1 through 65535");
|
||||
}
|
||||
const contract = JSON.parse(
|
||||
await readFile(path.join(root, "tech-log-serving-contract.json"), "utf8"),
|
||||
);
|
||||
const server = createTechLogProductionServer({ root, contract });
|
||||
server.listen(port, host, () => {
|
||||
process.stdout.write(`TechLog production server: ${root} on http://${host}:${port}\n`);
|
||||
});
|
||||
for (const signal of ["SIGINT", "SIGTERM"]) {
|
||||
process.on(signal, () => server.close(() => process.exit(0)));
|
||||
}
|
||||
}
|
||||
|
||||
const commandPath = process.argv[1];
|
||||
if (
|
||||
commandPath &&
|
||||
import.meta.url === pathToFileURL(path.resolve(commandPath)).href
|
||||
) {
|
||||
await startFromCommandLine();
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import type { TechLogServingContract } from "./tech-log-serving-contract.ts";
|
||||
|
||||
type ServingArtifactOptions = Readonly<{
|
||||
distRoot: string;
|
||||
contract: TechLogServingContract;
|
||||
serverSourcePath?: string;
|
||||
}>;
|
||||
|
||||
export async function writeTechLogServingArtifact({
|
||||
distRoot,
|
||||
contract,
|
||||
serverSourcePath = "scripts/lib/tech-log-production-server.ts",
|
||||
}: ServingArtifactOptions): Promise<void> {
|
||||
const absoluteDistRoot = path.resolve(distRoot);
|
||||
const serverSource = await readFile(path.resolve(serverSourcePath), "utf8");
|
||||
await mkdir(absoluteDistRoot, { recursive: true });
|
||||
await Promise.all([
|
||||
writeFile(
|
||||
path.join(absoluteDistRoot, "tech-log-serving-contract.json"),
|
||||
`${JSON.stringify(contract, null, 2)}\n`,
|
||||
),
|
||||
writeFile(path.join(absoluteDistRoot, "server.mjs"), serverSource),
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
export type TechLogServingContract = Readonly<{
|
||||
schemaVersion: 1;
|
||||
publicSpaPaths: readonly string[];
|
||||
studioPathPrefix: "/studio";
|
||||
studioSpaPathPatterns: readonly string[];
|
||||
notFound: Readonly<{
|
||||
status: 404;
|
||||
contentType: "text/plain;charset=UTF-8";
|
||||
body: "Not Found";
|
||||
}>;
|
||||
}>;
|
||||
|
||||
type ServingContractInput = Readonly<{
|
||||
publicRecords: readonly Readonly<{
|
||||
path: string;
|
||||
topicSlug: string;
|
||||
}>[];
|
||||
projects: readonly Readonly<{ slug: string }>[];
|
||||
releases: readonly Readonly<{ path: string }>[];
|
||||
}>;
|
||||
|
||||
const staticPublicPaths = Object.freeze([
|
||||
"/",
|
||||
"/explore",
|
||||
"/explore/cases",
|
||||
"/explore/questions",
|
||||
"/explore/references",
|
||||
"/profile",
|
||||
"/projects",
|
||||
"/releases",
|
||||
"/search",
|
||||
]);
|
||||
|
||||
const studioSpaPathPatterns = Object.freeze([
|
||||
"^/studio$",
|
||||
"^/studio/documents$",
|
||||
"^/studio/documents/new$",
|
||||
"^/studio/documents/[^/]+/(edit|validation|preview|publish)$",
|
||||
"^/studio/publications$",
|
||||
"^/studio/publications/[^/]+/preview$",
|
||||
]);
|
||||
|
||||
function asciiCompare(left: string, right: string): number {
|
||||
return left < right ? -1 : left > right ? 1 : 0;
|
||||
}
|
||||
|
||||
export function createTechLogServingContract({
|
||||
publicRecords,
|
||||
projects,
|
||||
releases,
|
||||
}: ServingContractInput): TechLogServingContract {
|
||||
const publicSpaPaths = new Set(staticPublicPaths);
|
||||
for (const record of publicRecords) {
|
||||
publicSpaPaths.add(record.path);
|
||||
publicSpaPaths.add(`/topics/${record.topicSlug}`);
|
||||
}
|
||||
for (const project of projects) {
|
||||
const projectPath = `/projects/${project.slug}`;
|
||||
publicSpaPaths.add(projectPath);
|
||||
publicSpaPaths.add(`${projectPath}/activity`);
|
||||
publicSpaPaths.add(`${projectPath}/decisions`);
|
||||
publicSpaPaths.add(`${projectPath}/records`);
|
||||
}
|
||||
for (const release of releases) publicSpaPaths.add(release.path);
|
||||
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
publicSpaPaths: Object.freeze([...publicSpaPaths].sort(asciiCompare)),
|
||||
studioPathPrefix: "/studio",
|
||||
studioSpaPathPatterns,
|
||||
notFound: Object.freeze({
|
||||
status: 404,
|
||||
contentType: "text/plain;charset=UTF-8",
|
||||
body: "Not Found",
|
||||
}),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user