fix: harden CI evidence and removal contracts

This commit is contained in:
DongHyeonka
2026-08-02 14:48:04 +09:00
parent 1bb2cc4a20
commit f49d147b01
20 changed files with 1175 additions and 767 deletions
+4 -1
View File
@@ -17,7 +17,10 @@ import {
import { validatePackageScriptGraph } from "./lib/package-script-graph.ts";
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
const contract = await loadCiGateContract(process.cwd());
const removalFixtureMode = process.argv.includes("--reduced-removal-fixture");
const contract = await loadCiGateContract(process.cwd(), {
mode: removalFixtureMode ? "removal-fixture" : "canonical",
});
const index = indexCiGateContract(contract);
const [packageDocument, nodeVersion] = await Promise.all([
readFile("package.json", "utf8").then((value) => JSON.parse(value) as { scripts?: Record<string, string> }),
+92 -5
View File
@@ -1,4 +1,5 @@
import { readFile } from "node:fs/promises";
import { createHash } from "node:crypto";
import path from "node:path";
import { z } from "zod";
@@ -246,9 +247,18 @@ const artifactSchemaSchema = z.discriminatedUnion("kind", [
.strict(),
]);
const artifactSchema = z
.object({ id, path: repositoryPath, schemaId: id })
.strict();
const artifactBaseShape = { id, path: repositoryPath, schemaId: id } as const;
const artifactSchema = z.discriminatedUnion("production", [
z.object({ ...artifactBaseShape, production: z.literal("source-controlled") }).strict(),
z
.object({
...artifactBaseShape,
production: z.literal("command-generated"),
producerCommandIds: z.array(id).min(1).max(32),
})
.strict(),
z.object({ ...artifactBaseShape, production: z.literal("runner-generated") }).strict(),
]);
const gateSchema = z
.object({
@@ -405,6 +415,35 @@ export type CiGateContractIndex = Readonly<{
jobs: ReadonlyMap<string, CiWorkflowJob>;
retentionClasses: ReadonlyMap<string, CiGateContract["retention"]["classes"][number]>;
}>;
export type LoadCiGateContractOptions = Readonly<{
mode?: "canonical" | "removal-fixture";
}>;
const CANONICAL_GATE_SHAPE_SHA256 =
"a4a963d0b9deffb7a0a3d755bbbcb979d72610eb74751c3a2e5eca55251e12d4";
function canonicalGateShapeSha256(gates: CiGateContract["gates"]): string {
const normalized = gates.map(
({
id,
name,
commandIds,
logArtifactId,
evidenceArtifactIds,
retentionClassId,
requiresEnvironment,
}) => ({
id,
name,
commandIds,
logArtifactId,
evidenceArtifactIds,
retentionClassId,
requiresEnvironment: requiresEnvironment ?? [],
}),
);
return createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
}
export function parseCiGateContract(value: unknown): CiGateContract {
const result = ciGateContractSchema.safeParse(value);
@@ -417,12 +456,22 @@ export function parseCiGateContract(value: unknown): CiGateContract {
return result.data;
}
export async function loadCiGateContract(root = process.cwd()): Promise<CiGateContract> {
export async function loadCiGateContract(
root = process.cwd(),
options: LoadCiGateContractOptions = {},
): Promise<CiGateContract> {
const [rawContract, rawPackage] = await Promise.all([
readFile(path.join(root, "config/ci/gates.json"), "utf8"),
readFile(path.join(root, "package.json"), "utf8"),
]);
const contract = parseCiGateContract(JSON.parse(rawContract));
const mode = options.mode ?? "canonical";
if (
mode === "canonical" &&
canonicalGateShapeSha256(contract.gates) !== CANONICAL_GATE_SHAPE_SHA256
) {
throw new TypeError("CI gate contract canonical gate semantic shape drift");
}
const packageDocument = z
.object({ scripts: z.record(z.string(), z.string()).default({}) })
.passthrough()
@@ -434,10 +483,23 @@ export async function loadCiGateContract(root = process.cwd()): Promise<CiGateCo
if (missing.length > 0) {
throw new TypeError(`CI gate contract missing package scripts: ${missing.join(", ")}`);
}
const expectedCheckCi = "corepack pnpm check:artifact-schemas && node scripts/check-ci-contract.ts && corepack pnpm check:ci-workflow";
const expectedCheckCi = mode === "canonical"
? "corepack pnpm check:artifact-schemas && node scripts/check-ci-contract.ts && corepack pnpm check:ci-workflow"
: "corepack pnpm check:artifact-schemas && node scripts/check-ci-contract.ts --reduced-removal-fixture && corepack pnpm check:ci-workflow";
if (packageDocument.scripts["check:ci"] !== expectedCheckCi) {
throw new TypeError("check:ci must use the exact canonical non-recursive orchestration");
}
const canonicalCheckCiDependencies = {
"check:artifact-schemas": "node scripts/generate-artifact-schemas.ts --check",
"check:ci-workflow": mode === "canonical"
? "node scripts/generate-ci-workflow.ts --check"
: "node scripts/generate-ci-workflow.ts --check --reduced-removal-fixture",
} as const;
for (const [script, expected] of Object.entries(canonicalCheckCiDependencies)) {
if (packageDocument.scripts[script] !== expected) {
throw new TypeError(`canonical check:ci dependency drift: ${script}`);
}
}
const graphFailures = validatePackageScriptGraph(packageDocument.scripts, "check:ci");
if (graphFailures.length > 0) {
throw new TypeError(`CI package script graph invalid:\n${graphFailures.join("\n")}`);
@@ -510,6 +572,16 @@ function validateContractSemantics(
if (!schemaIds.has(artifact.schemaId)) {
issue(`unknown artifact schema ${artifact.schemaId} for ${artifact.id}`);
}
if (artifact.production === "command-generated") {
if (new Set(artifact.producerCommandIds).size !== artifact.producerCommandIds.length) {
issue(`duplicate producer command reference for artifact: ${artifact.id}`);
}
for (const producerCommandId of artifact.producerCommandIds) {
if (!commandIds.has(producerCommandId)) {
issue(`unknown producer command ${producerCommandId} for ${artifact.id}`);
}
}
}
}
for (const gate of contract.gates) {
if (new Set(gate.commandIds).size !== gate.commandIds.length) {
@@ -524,6 +596,21 @@ function validateContractSemantics(
for (const artifactId of [gate.logArtifactId, ...gate.evidenceArtifactIds]) {
if (!artifactIds.has(artifactId)) issue(`unknown artifact ${artifactId} for ${gate.id}`);
}
const logArtifact = contract.artifacts.find(({ id }) => id === gate.logArtifactId);
if (logArtifact && logArtifact.production !== "runner-generated") {
issue(`gate log must be runner-generated: ${gate.id}`);
}
for (const artifactId of gate.evidenceArtifactIds) {
const artifact = contract.artifacts.find(({ id }) => id === artifactId);
if (
artifact?.production === "command-generated" &&
!artifact.producerCommandIds.some((producerCommandId) =>
gate.commandIds.includes(producerCommandId)
)
) {
issue(`gate lacks a bound producer command for ${artifact.id}: ${gate.id}`);
}
}
if (!retentionIds.has(gate.retentionClassId)) {
issue(`unknown retention class ${gate.retentionClassId} for ${gate.id}`);
}
+9 -3
View File
@@ -28,6 +28,7 @@ export type GenerateCiWorkflowOptions = Readonly<{
root: string;
contract?: CiGateContract;
check: boolean;
contractMode?: "canonical" | "removal-fixture";
}>;
export type GenerateCiWorkflowResult = Readonly<{
@@ -176,7 +177,7 @@ function renderStep(
" - name: Frozen install",
" run: |",
" corepack enable",
" corepack pnpm install --frozen-lockfile",
" corepack pnpm install --frozen-lockfile --ignore-scripts",
];
case "browser-install":
return [
@@ -335,7 +336,9 @@ export function createCiWorkflowGenerator(
const createNonce = dependencies.createNonce ?? randomUUID;
return async function generate(options: GenerateCiWorkflowOptions): Promise<GenerateCiWorkflowResult> {
const root = path.resolve(options.root);
const contract = options.contract ?? (await loadCiGateContract(root));
const contract = options.contract ?? (await loadCiGateContract(root, {
mode: options.contractMode ?? "canonical",
}));
const target = path.resolve(root, contract.providerAdapter);
if (path.relative(root, target).startsWith("..") || path.relative(root, target) === "") {
throw new TypeError(`workflow target escapes repository root: ${contract.providerAdapter}`);
@@ -449,8 +452,11 @@ function hasErrorCode(error: unknown, code: string): boolean {
const isCli = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
if (isCli) {
const check = process.argv.includes("--check");
const contractMode = process.argv.includes("--reduced-removal-fixture")
? "removal-fixture" as const
: "canonical" as const;
try {
const result = await generateCiWorkflow({ root: process.cwd(), check });
const result = await generateCiWorkflow({ root: process.cwd(), check, contractMode });
if (!result.matches) {
process.stderr.write(
`CI workflow drift: ${result.target} differs at byte ${result.firstDifferenceByte ?? 0}, line ${result.firstDifferenceLine ?? 1}\n`,
+233
View File
@@ -0,0 +1,233 @@
import { spawnSync } from "node:child_process";
import {
cp,
mkdir,
readFile,
readdir,
rm,
symlink,
writeFile,
} from "node:fs/promises";
import path from "node:path";
import {
loadCiGateContract,
parseCiGateContract,
} from "../contracts/ci-gates.ts";
import { generateCiWorkflow } from "../generate-ci-workflow.ts";
export const REMOVAL_FIXTURE_COPY_TARGETS = Object.freeze([
"src", "tests", "recipes", "scripts", "schemas", "config", "public",
".gitea", ".storybook", "index.html", "package.json", "tsconfig.base.json",
"tsconfig.json", "tsconfig.app.json", "tsconfig.node.json", "tsconfig.test.json",
"tsconfig.recipes.json", "tsconfig.web-worker.json", "tsconfig.service-worker.json",
"vite.service-worker.config.ts", "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);
export function requireRemovalFixtureEnvironment(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`${name} is required for removal verification`);
return value;
}
export async function prepareRemovalFixture(
root: string,
copyTargets: readonly string[] = REMOVAL_FIXTURE_COPY_TARGETS,
): Promise<void> {
await rm(root, { recursive: true, force: true });
await mkdir(root, { recursive: true });
for (const target of copyTargets) {
await cp(target, path.join(root, target), { recursive: true });
}
await symlink(path.resolve("node_modules"), path.join(root, "node_modules"), "dir");
}
export function runRemovalFixturePnpm(
root: string,
pnpmCli: string,
script: string,
extra: readonly string[] = [],
): boolean {
return spawnSync(process.execPath, [pnpmCli, script, ...extra], {
cwd: root,
stdio: "inherit",
env: { ...process.env, CI_CONTRACT_MODE: "removal-fixture" },
}).status === 0;
}
export async function filesBelow(directory: string): Promise<string[]> {
const entries = await readdir(directory, { withFileTypes: true });
return (await Promise.all(entries.map((entry) => {
const target = path.join(directory, entry.name);
return entry.isDirectory() ? filesBelow(target) : [target];
}))).flat();
}
function isWithin(target: string, root: string): boolean {
const relative = path.relative(root, target);
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
}
export async function runtimeImportGraph(
root: string,
runtimeSourceRoots: readonly string[],
): Promise<Readonly<{ dependentTests: readonly string[]; importingFiles: readonly string[] }>> {
const files = (await filesBelow(root))
.filter((file) => /\.(?:[cm]?ts|tsx)$/u.test(file))
.map((file) => path.resolve(file));
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");
const specifiers = [...source.matchAll(/(?:from\s*|import\s*\(\s*|import\s*)["']([^"']+)["']/gu)]
.map((match) => match[1])
.filter((specifier): specifier is string => typeof specifier === "string" && specifier.startsWith("."));
imports.set(file, specifiers.map((specifier) => {
const base = path.resolve(path.dirname(file), specifier);
return [base, `${base}.ts`, `${base}.tsx`, `${base}.mts`, `${base}.cts`, path.join(base, "index.ts"), path.join(base, "index.tsx")]
.find((candidate) => sourceSet.has(candidate)) ?? base;
}));
}
const memo = new Map<string, boolean>();
const reachesRuntime = (file: string, visiting = new Set<string>()): boolean => {
if (runtimeRoots.some((runtimeRoot) => isWithin(file, runtimeRoot))) 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))
)
)),
});
}
export async function removeRuntimeDependentTests(
root: string,
runtimeSourceRoots: readonly string[],
): Promise<number> {
const graph = await runtimeImportGraph(root, runtimeSourceRoots);
await Promise.all(graph.dependentTests.map((file) => rm(file, { force: true })));
return graph.dependentTests.length;
}
export async function assertNoRuntimeImports(
root: string,
runtimeSourceRoots: readonly string[],
capability: string,
): Promise<void> {
const graph = await runtimeImportGraph(root, runtimeSourceRoots);
if (graph.importingFiles.length > 0) {
throw new Error(`Removed ${capability} runtime is still imported by: ${graph.importingFiles.map((file) => path.relative(root, file)).join(", ")}`);
}
}
export function pruneScriptOrchestration(
scripts: Record<string, string>,
orchestrationScript: string,
removedScripts: ReadonlySet<string>,
): void {
const command = scripts[orchestrationScript];
if (!command) return;
scripts[orchestrationScript] = command.split(" && ").filter((segment) =>
![...removedScripts].some((removed) =>
new RegExp(`(?:^|\\s)(?:corepack\\s+)?pnpm\\s+${removed.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}(?:\\s|$)`, "u").test(segment)
)
).join(" && ");
}
export async function regenerateRemovalFixtureWorkflow(root: string): Promise<void> {
const contract = await loadCiGateContract(root, { mode: "removal-fixture" });
await generateCiWorkflow({ root, contract, check: false });
}
export async function pruneRemovalFixtureCiContract(options: Readonly<{
root: string;
removedScripts: ReadonlySet<string>;
removedEvidencePathFragments: readonly string[];
}>): Promise<void> {
const packagePath = path.join(options.root, "package.json");
const gatesPath = path.join(options.root, "config/ci/gates.json");
const packageDocument = JSON.parse(await readFile(packagePath, "utf8")) as {
scripts: Record<string, string>;
};
for (const script of options.removedScripts) delete packageDocument.scripts[script];
packageDocument.scripts["check:ci-workflow"] =
"node scripts/generate-ci-workflow.ts --check --reduced-removal-fixture";
packageDocument.scripts["check:ci"] =
"corepack pnpm check:artifact-schemas && node scripts/check-ci-contract.ts --reduced-removal-fixture && corepack pnpm check:ci-workflow";
const contract = structuredClone(
parseCiGateContract(JSON.parse(await readFile(gatesPath, "utf8"))),
);
const removedCommandIds = new Set(
contract.commands
.filter(({ script }) => options.removedScripts.has(script))
.map(({ id }) => id),
);
const missing = [...options.removedScripts].filter(
(script) => !contract.commands.some((command) => command.script === script),
);
if (missing.length > 0) {
throw new Error(`removal fixture CI command set is incomplete: ${missing.join(", ")}`);
}
const removedArtifactIds = new Set(
contract.artifacts
.filter(({ path: artifactPath }) =>
options.removedEvidencePathFragments.some((fragment) => artifactPath.includes(fragment))
)
.map(({ id }) => id),
);
for (const fragment of options.removedEvidencePathFragments) {
if (!contract.artifacts.some(({ path: artifactPath }) => artifactPath.includes(fragment))) {
throw new Error(`removal fixture CI evidence is missing: ${fragment}`);
}
}
contract.commands = contract.commands.filter(({ id }) => !removedCommandIds.has(id));
contract.artifacts = contract.artifacts
.filter(({ id }) => !removedArtifactIds.has(id))
.map((artifact) => artifact.production === "command-generated"
? {
...artifact,
producerCommandIds: artifact.producerCommandIds.filter(
(commandId) => !removedCommandIds.has(commandId),
),
}
: artifact)
.filter((artifact) =>
artifact.production !== "command-generated" || artifact.producerCommandIds.length > 0
);
const retainedArtifactIds = new Set(contract.artifacts.map(({ id }) => id));
for (const gate of contract.gates) {
gate.commandIds = gate.commandIds.filter((commandId) => !removedCommandIds.has(commandId));
gate.evidenceArtifactIds = gate.evidenceArtifactIds.filter((artifactId) =>
retainedArtifactIds.has(artifactId)
);
}
const referencedSchemaIds = new Set(contract.artifacts.map(({ schemaId }) => schemaId));
contract.artifactSchemas = contract.artifactSchemas.filter(({ id }) =>
referencedSchemaIds.has(id)
);
const validated = parseCiGateContract(contract);
await Promise.all([
writeFile(packagePath, `${JSON.stringify(packageDocument, null, 2)}\n`),
writeFile(gatesPath, `${JSON.stringify(validated, null, 2)}\n`),
]);
}
+50
View File
@@ -1,4 +1,6 @@
import { spawnSync } from "node:child_process";
import { lstat } from "node:fs/promises";
import path from "node:path";
import {
ciCheckoutIdentityFailures,
@@ -37,6 +39,28 @@ let passed = true;
const DEFAULT_STEP_TIMEOUT_MS = 30 * 60 * 1_000;
const MAX_STEP_OUTPUT_BYTES = 16 * 1024 * 1_024;
const LOG_DIAGNOSTIC_RESERVE_BYTES = 4_096;
const freshlyProducedArtifactIds = new Set<string>();
const commandGeneratedEvidence = gate.evidenceArtifactIds
.map((artifactId) => contractIndex.artifacts.get(artifactId))
.filter((artifact) => artifact?.production === "command-generated");
async function observeArtifactGeneration(relativePath: string): Promise<string> {
try {
const metadata = await lstat(path.join(process.cwd(), relativePath), {
bigint: true,
});
return [
metadata.dev,
metadata.ino,
metadata.size,
metadata.mtimeNs,
metadata.ctimeNs,
].join(":");
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return "missing";
throw error;
}
}
const appendOutput = (...values: readonly string[]): boolean => {
for (const value of values.filter(Boolean)) {
const addedBytes = Buffer.byteLength(value, "utf8") + 1;
@@ -97,6 +121,17 @@ if (passed) {
for (const commandId of gate.commandIds) {
const step = contractIndex.commands.get(commandId);
if (!step) throw new TypeError(`CI gate command disappeared after validation: ${commandId}`);
const producedArtifacts = commandGeneratedEvidence.filter((artifact) =>
artifact.producerCommandIds.includes(commandId)
);
const generationBefore = new Map(
await Promise.all(
producedArtifacts.map(async (artifact) => [
artifact.id,
await observeArtifactGeneration(artifact.path),
] as const),
),
);
const commandLine = `$ corepack pnpm ${step.script} ${(step.args ?? []).join(" ")}`.trim();
if (!appendOutput(commandLine) || logSchema.maxBytes - outputBytes <= LOG_DIAGNOSTIC_RESERVE_BYTES) {
appendOutput("gate aggregate output budget exhausted before command execution");
@@ -159,6 +194,21 @@ if (passed) {
passed = false;
break;
}
for (const artifact of producedArtifacts) {
const generationAfter = await observeArtifactGeneration(artifact.path);
if (generationAfter !== generationBefore.get(artifact.id)) {
freshlyProducedArtifactIds.add(artifact.id);
}
}
}
}
if (passed) {
for (const artifact of commandGeneratedEvidence) {
if (!freshlyProducedArtifactIds.has(artifact.id)) {
appendOutput(`command-generated evidence was not freshly produced: ${artifact.path}`);
passed = false;
}
}
}
@@ -1,22 +1,25 @@
import { spawnSync } from "node:child_process";
import {
cp,
mkdir,
readFile,
readdir,
rm,
symlink,
writeFile,
} from "node:fs/promises";
import path from "node:path";
import { parseCiGateContract } from "./contracts/ci-gates.ts";
import { generateCiWorkflow } from "./generate-ci-workflow.ts";
import {
assertNoRuntimeImports,
prepareRemovalFixture,
pruneRemovalFixtureCiContract,
regenerateRemovalFixtureWorkflow,
removeRuntimeDependentTests,
requireRemovalFixtureEnvironment,
runRemovalFixturePnpm,
} from "./lib/removal-fixture.ts";
const fixtureRoot = path.resolve(
".tmp/browser-file-storage-runtime-removal",
);
const pnpmCli = requireEnvironment("npm_execpath");
const pnpmCli = requireRemovalFixtureEnvironment("npm_execpath");
const runtimePaths = [
"src/application/ports/browser-file-storage",
"src/application/ports/browser-transfer",
@@ -42,216 +45,14 @@ const removedEvidencePathFragments = [
"browser-capabilities",
"browser-file-storage-runtime-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",
"tsconfig.web-worker.json",
"tsconfig.service-worker.json",
"vite.service-worker.config.ts",
"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
);
return runRemovalFixturePnpm(fixtureRoot, pnpmCli, script);
}
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",
);
await prepareRemovalFixture(fixtureRoot);
const removedRuntimeTests =
await removeRuntimeDependentTests(fixtureRoot);
await removeRuntimeDependentTests(fixtureRoot, runtimeSourceRoots);
for (const runtimePath of runtimePaths) {
await rm(path.join(fixtureRoot, runtimePath), {
recursive: true,
@@ -290,17 +91,6 @@ if (removedRuntimeEntries !== 3) {
}
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 removedScripts) {
delete packageDocument.scripts[script];
}
await writeFile(
packagePath,
`${JSON.stringify(packageDocument, null, 2)}\n`,
);
await rm(path.join(fixtureRoot, "playwright.capabilities.config.ts"), {
force: true,
});
@@ -330,67 +120,17 @@ await rm(
{ force: true },
);
const gatesPath = path.join(fixtureRoot, "config/ci/gates.json");
const gatesDocument = structuredClone(
parseCiGateContract(JSON.parse(await readFile(gatesPath, "utf8"))),
);
const removedCommandIds = new Set(
gatesDocument.commands
.filter(({ script }) => removedScripts.has(script))
.map(({ id }) => id),
);
if (removedCommandIds.size !== removedScripts.size) {
throw new Error("Browser file/storage CI command removal set is incomplete");
}
const removedArtifactIds = new Set(
gatesDocument.artifacts
.filter(({ path: artifactPath }) =>
removedEvidencePathFragments.some((fragment) =>
artifactPath.includes(fragment),
),
)
.map(({ id }) => id),
);
for (const fragment of removedEvidencePathFragments) {
if (
!gatesDocument.artifacts.some(({ path: artifactPath }) =>
artifactPath.includes(fragment),
)
) {
throw new Error(`Browser file/storage CI evidence is missing: ${fragment}`);
}
}
gatesDocument.commands = gatesDocument.commands.filter(
({ id }) => !removedCommandIds.has(id),
);
gatesDocument.artifacts = gatesDocument.artifacts.filter(
({ id }) => !removedArtifactIds.has(id),
);
for (const gate of gatesDocument.gates) {
gate.commandIds = gate.commandIds.filter(
(commandId) => !removedCommandIds.has(commandId),
);
gate.evidenceArtifactIds = gate.evidenceArtifactIds.filter(
(artifactId) => !removedArtifactIds.has(artifactId),
);
}
const referencedSchemaIds = new Set(
gatesDocument.artifacts.map(({ schemaId }) => schemaId),
);
gatesDocument.artifactSchemas = gatesDocument.artifactSchemas.filter(
({ id }) => referencedSchemaIds.has(id),
);
const validatedGates = parseCiGateContract(gatesDocument);
await writeFile(
gatesPath,
`${JSON.stringify(validatedGates, null, 2)}\n`,
);
await generateCiWorkflow({
await pruneRemovalFixtureCiContract({
root: fixtureRoot,
contract: validatedGates,
check: false,
removedScripts,
removedEvidencePathFragments,
});
await assertNoRuntimeImports(fixtureRoot);
await regenerateRemovalFixtureWorkflow(fixtureRoot);
await assertNoRuntimeImports(
fixtureRoot,
runtimeSourceRoots,
"browser file/storage",
);
const checks: Array<readonly [string, boolean]> = [
["typecheck", runPnpm("check:types")],
+45 -35
View File
@@ -1,17 +1,23 @@
import { spawnSync } from "node:child_process";
import {
cp,
mkdir,
readFile,
readdir,
rm,
symlink,
writeFile,
} from "node:fs/promises";
import path from "node:path";
import {
filesBelow,
prepareRemovalFixture,
pruneRemovalFixtureCiContract,
pruneScriptOrchestration,
regenerateRemovalFixtureWorkflow,
requireRemovalFixtureEnvironment,
runRemovalFixturePnpm,
} from "./lib/removal-fixture.ts";
const fixtureRoot = path.resolve(".tmp/optional-recipe-removal");
const pnpmCli = requireEnvironment("npm_execpath");
const pnpmCli = requireRemovalFixtureEnvironment("npm_execpath");
const copyTargets = [
"src",
"tests",
@@ -44,51 +50,55 @@ const copyTargets = [
".nvmrc",
];
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,
stdio: "inherit",
}).status === 0
);
return runRemovalFixturePnpm(fixtureRoot, pnpmCli, script);
}
async function filesBelow(directory: string): Promise<string[]> {
const entries = await readdir(directory, { withFileTypes: true });
const groups = await Promise.all(
entries.map((entry) => {
const target = path.join(directory, entry.name);
return entry.isDirectory() ? filesBelow(target) : [target];
}),
);
return groups.flat();
await prepareRemovalFixture(fixtureRoot, copyTargets);
for (const rootOnlyTest of [
"tests/unit/ci-workflow-generation.test.ts",
"tests/unit/__snapshots__/ci-workflow-generation.test.ts.snap",
]) {
await rm(path.join(fixtureRoot, rootOnlyTest), { force: true });
}
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");
await rm(path.join(fixtureRoot, "recipes"), { recursive: true, force: true });
await rm(path.join(fixtureRoot, "tests/recipes"), {
recursive: true,
force: true,
});
const removedCiScripts = new Set([
"check:types:recipes",
"test:recipes",
"check:optional-recipes",
"check:optional-recipes:source",
"check:optional-recipe-fixtures",
"test:optional-recipe-removal",
]);
const fixturePackagePath = path.join(fixtureRoot, "package.json");
const fixturePackage = JSON.parse(await readFile(fixturePackagePath, "utf8")) as {
scripts: Record<string, string>;
};
pruneScriptOrchestration(fixturePackage.scripts, "check:types", removedCiScripts);
pruneScriptOrchestration(fixturePackage.scripts, "test:all", removedCiScripts);
await writeFile(fixturePackagePath, `${JSON.stringify(fixturePackage, null, 2)}\n`);
await pruneRemovalFixtureCiContract({
root: fixtureRoot,
removedScripts: removedCiScripts,
removedEvidencePathFragments: [
"optional-recipes",
"optional-recipe-fixtures",
"optional-recipe-removal",
],
});
await regenerateRemovalFixtureWorkflow(fixtureRoot);
const checks: Array<[string, boolean]> = [
["typecheck", runPnpm("check:types")],
["architecture", runPnpm("check:architecture")],
["test", runPnpm("test:all")],
["build", runPnpm("build")],
["ci-contract", runPnpm("check:ci")],
];
const residue: string[] = [];
for (const file of await filesBelow(path.join(fixtureRoot, "dist"))) {
+18 -279
View File
@@ -1,20 +1,23 @@
import { spawnSync } from "node:child_process";
import {
cp,
mkdir,
readFile,
readdir,
rm,
symlink,
writeFile,
} from "node:fs/promises";
import path from "node:path";
import { parseCiGateContract } from "./contracts/ci-gates.ts";
import { generateCiWorkflow } from "./generate-ci-workflow.ts";
import {
assertNoRuntimeImports,
prepareRemovalFixture,
pruneRemovalFixtureCiContract,
regenerateRemovalFixtureWorkflow,
removeRuntimeDependentTests,
requireRemovalFixtureEnvironment,
runRemovalFixturePnpm,
} from "./lib/removal-fixture.ts";
const fixtureRoot = path.resolve(".tmp/realtime-runtime-removal");
const pnpmCli = requireEnvironment("npm_execpath");
const pnpmCli = requireRemovalFixtureEnvironment("npm_execpath");
const runtimePaths = [
"src/application/ports/realtime",
"src/application/ports/out/web-push-control.ts",
@@ -38,212 +41,14 @@ const removedEvidencePathFragments = [
"realtime-boundaries",
"realtime-runtime-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",
"tsconfig.web-worker.json",
"tsconfig.service-worker.json",
"vite.service-worker.config.ts",
"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
);
return runRemovalFixturePnpm(fixtureRoot, pnpmCli, script);
}
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",
);
await prepareRemovalFixture(fixtureRoot);
const removedRuntimeTests =
await removeRuntimeDependentTests(fixtureRoot);
await removeRuntimeDependentTests(fixtureRoot, runtimeSourceRoots);
for (const runtimePath of runtimePaths) {
await rm(path.join(fixtureRoot, runtimePath), {
recursive: true,
@@ -280,17 +85,6 @@ if (!realtimeRecipe || !Object.hasOwn(realtimeRecipe, "referenceRuntime")) {
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",
@@ -313,68 +107,13 @@ await rm(
{ force: true },
);
const gatesPath = path.join(fixtureRoot, "config/ci/gates.json");
const gatesDocument = structuredClone(
parseCiGateContract(JSON.parse(await readFile(gatesPath, "utf8"))),
);
const removedScripts = new Set<string>(runtimeScripts);
const removedCommandIds = new Set(
gatesDocument.commands
.filter(({ script }) => removedScripts.has(script))
.map(({ id }) => id),
);
if (removedCommandIds.size !== runtimeScripts.length) {
throw new Error("Realtime CI command removal set is incomplete");
}
const removedArtifactIds = new Set(
gatesDocument.artifacts
.filter(({ path: artifactPath }) =>
removedEvidencePathFragments.some((fragment) =>
artifactPath.includes(fragment),
),
)
.map(({ id }) => id),
);
for (const fragment of removedEvidencePathFragments) {
if (
!gatesDocument.artifacts.some(({ path: artifactPath }) =>
artifactPath.includes(fragment),
)
) {
throw new Error(`Realtime CI evidence is missing: ${fragment}`);
}
}
gatesDocument.commands = gatesDocument.commands.filter(
({ id }) => !removedCommandIds.has(id),
);
gatesDocument.artifacts = gatesDocument.artifacts.filter(
({ id }) => !removedArtifactIds.has(id),
);
for (const gate of gatesDocument.gates) {
gate.commandIds = gate.commandIds.filter(
(commandId) => !removedCommandIds.has(commandId),
);
gate.evidenceArtifactIds = gate.evidenceArtifactIds.filter(
(artifactId) => !removedArtifactIds.has(artifactId),
);
}
const referencedSchemaIds = new Set(
gatesDocument.artifacts.map(({ schemaId }) => schemaId),
);
gatesDocument.artifactSchemas = gatesDocument.artifactSchemas.filter(
({ id }) => referencedSchemaIds.has(id),
);
const validatedGates = parseCiGateContract(gatesDocument);
await writeFile(
gatesPath,
`${JSON.stringify(validatedGates, null, 2)}\n`,
);
await generateCiWorkflow({
await pruneRemovalFixtureCiContract({
root: fixtureRoot,
contract: validatedGates,
check: false,
removedScripts: new Set<string>(runtimeScripts),
removedEvidencePathFragments,
});
await assertNoRuntimeImports(fixtureRoot);
await regenerateRemovalFixtureWorkflow(fixtureRoot);
await assertNoRuntimeImports(fixtureRoot, runtimeSourceRoots, "realtime");
const checks: Array<readonly [string, boolean]> = [
["typecheck", runPnpm("check:types")],
+47 -30
View File
@@ -1,25 +1,30 @@
import { spawnSync } from "node:child_process";
import {
access,
cp,
mkdir,
mkdtemp,
readFile,
readdir,
rm,
symlink,
writeFile,
} from "node:fs/promises";
import path from "node:path";
import { isProductionModulePath } from "./lib/risk-coverage.ts";
import {
filesBelow,
prepareRemovalFixture,
pruneRemovalFixtureCiContract,
pruneScriptOrchestration,
regenerateRemovalFixtureWorkflow,
requireRemovalFixtureEnvironment,
runRemovalFixturePnpm,
} from "./lib/removal-fixture.ts";
const fixtureParent = path.resolve(".tmp");
await mkdir(fixtureParent, { recursive: true });
const fixtureRoot = await mkdtemp(
path.join(fixtureParent, "reference-feature-removal-"),
);
const pnpmCli = requireEnvironment("npm_execpath");
const pnpmCli = requireRemovalFixtureEnvironment("npm_execpath");
const featureSource = "src/features/reference-feature";
const featureTests = "tests/features/reference-feature";
const commonTestPaths = [
@@ -162,36 +167,20 @@ type GovernanceRegistry = Record<string, 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) => {
const target = path.join(directory, entry.name);
return entry.isDirectory() ? filesBelow(target) : [target];
}),
);
return groups.flat();
}
function runPnpm(script: string, extra: string[] = []): boolean {
const result = spawnSync(process.execPath, [pnpmCli, script, ...extra], {
cwd: fixtureRoot,
stdio: "inherit",
});
return result.status === 0;
return runRemovalFixturePnpm(fixtureRoot, pnpmCli, script, extra);
}
try {
for (const target of copyTargets) {
await cp(target, path.join(fixtureRoot, target), { recursive: true });
await prepareRemovalFixture(fixtureRoot, copyTargets);
for (const excludedFixtureTest of [
"tests/unit/ci-workflow-generation.test.ts",
"tests/unit/__snapshots__/ci-workflow-generation.test.ts.snap",
"tests/unit/removal-fixture.test.ts",
"tests/unit/http-scenario-evidence.test.ts",
]) {
await rm(path.join(fixtureRoot, excludedFixtureTest), { force: true });
}
await symlink(path.resolve("node_modules"), path.join(fixtureRoot, "node_modules"), "dir");
const coveragePolicyFile = path.join(
fixtureRoot,
@@ -329,6 +318,33 @@ try {
`${JSON.stringify(removalGovernance, null, 2)}\n`,
);
const removedCiScripts = new Set([
"check:types:fixture:feature-input",
"check:types:fixture:reference-operation",
"test:http-scenario-evidence",
"test:reference-feature",
]);
const fixturePackagePath = path.join(fixtureRoot, "package.json");
const fixturePackage = JSON.parse(await readFile(fixturePackagePath, "utf8")) as {
scripts: Record<string, string>;
};
pruneScriptOrchestration(fixturePackage.scripts, "test:all", removedCiScripts);
fixturePackage.scripts["test:coverage"] = fixturePackage.scripts["test:coverage"]
.replace(" tests/features/reference-feature", "");
delete fixturePackage.scripts["check:http-scenario-evidence"];
delete fixturePackage.scripts["check:http-scenario-evidence:fixture"];
await writeFile(fixturePackagePath, `${JSON.stringify(fixturePackage, null, 2)}\n`);
await pruneRemovalFixtureCiContract({
root: fixtureRoot,
removedScripts: removedCiScripts,
removedEvidencePathFragments: [
"reference-feature.xml",
"http-scenario-executions",
"http-scenario-evidence",
],
});
await regenerateRemovalFixtureWorkflow(fixtureRoot);
const residue: string[] = [];
for (const root of ["src", "tests"]) {
for (const file of await filesBelow(path.join(fixtureRoot, root))) {
@@ -366,6 +382,7 @@ try {
["unit-integration", runPnpm("test:all")],
["coverage", runPnpm("test:coverage")],
["test-evidence-source", runPnpm("check:test-evidence:source")],
["ci-contract", runPnpm("check:ci")],
[
"home-smoke",
runPnpm("exec", [