Files
DongHyeonkaandClaude Opus 5 7d1ccccbcd Merge branch 'main' into feature/techlog-ui-migration
Integrates the frontend template sync (a0fbafb → 5434760) into the TechLog UI
migration. Merged in this direction so every conflict is resolved and proved in
the worktree; main is only fast-forwarded afterwards and never holds a state
that was not verified here.

15 conflicts. The rule throughout: keep the template's mechanism, keep the
product's content, and never invent a third state neither branch would accept.

The template's product manifest and its runtime feature kill switch are adopted.
The route registry is deliberately not composed from contract.routes: the
reference feature still declares screens this product deleted, and reducing over
them would register paths with no component behind them. ROUTE_FEATURE_OWNER is
narrowed to registered routes for the same reason. The first resolution did
compose from contract.routes and was rejected by product-features.test.ts.

Three files pinned counts and a digest describing the gate contract. Neither
side's numbers describe the merged config/ci/gates.json, so they were recomputed
from it rather than chosen: 27 gates, 82 commands, 94 command references, 107
evidence references, 128 artifacts, shape sha256 5063586d.

README.md and docs/accessibility/manual-checklist.md now enumerate this
product's 27 routes, which the template's own verify:documentation requires.

product-feature-switch.test.tsx was rewritten around the invariant that still
applies here — no registered route without a component — rather than deleted
with the screens it used to exercise.

docs/operations/template-merge-2026-08-17.md records every decision, the gate
results, and the three follow-ups this merge deliberately did not decide.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 18:38:08 +09:00

445 lines
15 KiB
TypeScript

import {
access,
mkdir,
mkdtemp,
readFile,
rm,
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 = requireRemovalFixtureEnvironment("npm_execpath");
const featureSource = "src/features/reference-feature";
const featureTests = "tests/features/reference-feature";
/**
* Platform tests that must survive the sample feature's removal. Asserting they
* are still present is what stops the removal fixture from "passing" by having
* quietly deleted the platform's own coverage along with the feature.
*/
const commonTestPaths = [
"tests/unit/external-contract-runtime.test.ts",
"tests/unit/http-execution-v3.test.ts",
"tests/unit/runtime-adapters.test.ts",
"tests/integration/http-execution-v3-observability.test.ts",
];
const featureOwnedPaths = [
featureSource,
featureTests,
"tests/integration/http-scenario-catalog.test.ts",
"tests/integration/http-execution-v3-observability.test.ts",
"tests/features/tech-log/runtime-composition.test.ts",
"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 emptyContracts = `import type { RouteDefinition } from "../contracts/routes.ts";
import { PLATFORM_SCHEMA_REGISTRY } from "../contracts/schema-registry.ts";
import {
TECH_LOG_ROUTE_REGISTRY,
TECH_LOG_ROUTE_RUNTIME_CONTRACT,
TECH_LOG_ROUTE_SCHEMA_REGISTRY,
} from "./tech-log/contracts/tech-log-route-contract.ts";
export const INSTALLED_FEATURE_CONTRACTS: readonly unknown[] = Object.freeze([]);
export const ROUTE_REGISTRY = TECH_LOG_ROUTE_REGISTRY;
export const ROUTE_RUNTIME_CONTRACT = TECH_LOG_ROUTE_RUNTIME_CONTRACT;
export const API_OPERATIONS = Object.freeze({});
export const INVALIDATION_REGISTRY = Object.freeze({
topics: Object.freeze([]),
namespaces: Object.freeze([]),
edges: Object.freeze([]),
});
export const INVALIDATION_TOPIC_VERSIONS = Object.freeze([]);
export const SCHEMA_REGISTRY = Object.freeze({
...PLATFORM_SCHEMA_REGISTRY,
...TECH_LOG_ROUTE_SCHEMA_REGISTRY,
});
export const NAVIGATION_ROUTES = Object.freeze(
Object.values(ROUTE_REGISTRY)
.filter((definition) => definition.navigationOrder !== null)
.sort(
(left, right) =>
left.navigationOrder! - right.navigationOrder!,
),
);
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;
}
export function routePath(routeId: string): string {
return getRoute(routeId).path;
}
`;
const emptyRuntimes = `import { PLATFORM_ROUTE_CODECS } from "../presentation/routes/platform-route-codecs.ts";
import { TECH_LOG_ROUTE_CODECS } from "./tech-log/presentation/tech-log-route-codecs.ts";
import { TECH_LOG_ROUTE_RUNTIME } from "./tech-log/presentation/tech-log-route-runtime.tsx";
export const ROUTE_CODECS = Object.freeze({
...PLATFORM_ROUTE_CODECS,
...TECH_LOG_ROUTE_CODECS,
});
export const ROUTE_RUNTIME = TECH_LOG_ROUTE_RUNTIME;
`;
const emptyAdapters = `import { createTechLogFeatureInstalledInput } from "./tech-log/adapters/create-tech-log-feature-input.ts";
export function createInstalledFeatureInputs(_context: unknown) {
void _context;
const techLog = createTechLogFeatureInstalledInput();
return Object.freeze({ [techLog.featureId]: techLog.input });
}
`;
const emptyContractContributions = `import {
composeContractContributions,
type InstalledContractContribution,
type InstalledContractPackageIdentity,
} from "../contracts/external-contract-runtime.ts";
export const INSTALLED_CONTRACT_CONTRIBUTIONS: readonly InstalledContractContribution[] =
Object.freeze([]);
export const COMPOSED_CONTRACT_CONTRIBUTIONS = composeContractContributions(
INSTALLED_CONTRACT_CONTRIBUTIONS,
);
export const EXPECTED_CONTRACT_SET_PACKAGES: readonly InstalledContractPackageIdentity[] =
COMPOSED_CONTRACT_CONTRIBUTIONS.externalPackages;
`;
const emptyMessages = `import { TECH_LOG_MESSAGE_CATALOGS } from "./tech-log/contracts/tech-log-message-catalog.ts";
export const INSTALLED_MESSAGE_CATALOGS = Object.freeze({
"ko-KR": TECH_LOG_MESSAGE_CATALOGS["ko-KR"],
"en-US": TECH_LOG_MESSAGE_CATALOGS["en-US"],
});
`;
type CoveragePolicy = {
repositoryBaseline: number;
generatedPaths: string[];
criticalModules: Array<{
path?: string;
minimum?: Record<string, number>;
}>;
highRiskPaths: string[];
waivers: 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 runPnpm(script: string, extra: string[] = []): boolean {
return runRemovalFixturePnpm(fixtureRoot, pnpmCli, script, extra);
}
try {
await prepareRemovalFixture(fixtureRoot);
for (const excludedFixtureTest of [
"tests/integration/security-followup-archive.test.ts",
"tests/unit/ci-artifact-contract.test.ts",
"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",
"tests/unit/task3-selective-integration.test.ts",
]) {
await rm(path.join(fixtureRoot, excludedFixtureTest), { force: true });
}
const coveragePolicyFile = path.join(
fixtureRoot,
"config/testing/risk-coverage.json",
);
const coveragePolicy = JSON.parse(
await readFile(coveragePolicyFile, "utf8"),
) as CoveragePolicy;
const generatedProductionModules = new Set(coveragePolicy.generatedPaths);
const removedProductionModuleCount = (
await filesBelow(path.join(fixtureRoot, featureSource))
)
.map((file) => path.relative(fixtureRoot, file).split(path.sep).join("/"))
.filter(
(file) =>
isProductionModulePath(file) && !generatedProductionModules.has(file),
).length;
for (const ownedPath of featureOwnedPaths) {
await rm(path.join(fixtureRoot, ownedPath), {
recursive: true,
force: true,
});
}
await writeFile(
path.join(fixtureRoot, "src/features/installed-feature-contracts.ts"),
emptyContracts,
);
await writeFile(
path.join(fixtureRoot, "src/features/installed-feature-runtimes.tsx"),
emptyRuntimes,
);
await writeFile(
path.join(fixtureRoot, "src/features/installed-feature-adapters.ts"),
emptyAdapters,
);
await writeFile(
path.join(fixtureRoot, "src/features/installed-feature-messages.ts"),
emptyMessages,
);
await writeFile(
path.join(fixtureRoot, "src/features/installed-contract-contributions.ts"),
emptyContractContributions,
);
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;
coveragePolicy.highRiskPaths = coveragePolicy.highRiskPaths.filter(
(modulePath) => !modulePath.startsWith(`${featureSource}/`),
);
coveragePolicy.waivers = coveragePolicy.waivers.filter(
(waiver) => !waiver.path?.startsWith(`${featureSource}/`),
);
coveragePolicy.generatedPaths = coveragePolicy.generatedPaths.filter(
(modulePath) => !modulePath.startsWith(`${featureSource}/`),
);
coveragePolicy.repositoryBaseline -= removedProductionModuleCount;
if (coveragePolicy.repositoryBaseline <= 0) {
throw new Error("Reference feature removal produced an invalid coverage baseline");
}
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"),
) as RemovalGovernance;
removalGovernance.registries = removalGovernance.registries.map(
(registry) => ({
...registry,
...(Array.isArray(registry.consumers)
? {
consumers: registry.consumers.filter(
(candidate: unknown) => {
const consumer = candidate as GovernanceConsumer;
return !consumer.path?.includes("features/reference-feature");
},
),
}
: {}),
...(Array.isArray(registry.consumerDirectories)
? {
consumerDirectories: registry.consumerDirectories.filter(
(directory: unknown) =>
typeof directory !== "string" ||
!directory.includes("features/reference-feature"),
),
}
: {}),
}),
);
await writeFile(
governanceFile,
`${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))) {
const relative = path.relative(fixtureRoot, file);
const content = await readFile(file, "utf8");
if (
/REFERENCE_RESOURCE|reference-feature|reference-resource/i.test(
`${relative}\n${content}`,
)
) {
residue.push(relative);
}
}
}
const checks: Array<[string, boolean]> = [
[
"common-test-evidence",
(
await Promise.all(
commonTestPaths.map(async (testPath) => {
try {
await access(path.join(fixtureRoot, testPath));
return true;
} catch {
return false;
}
}),
)
).every(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")],
["ci-contract", runPnpm("check:ci")],
[
"home-smoke",
runPnpm("exec", [
"vitest",
"run",
"tests/component/router.test.tsx",
"--reporter=default",
]),
],
["build", runPnpm("build")],
];
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");
if (
/REFERENCE_RESOURCE|reference-feature|reference-resource/i.test(content)
) {
builtResidue.push(path.relative(fixtureRoot, file));
}
}
const routeCatalog = await import(
`${new URL(
"../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]);
checks.push(["fixture-id-residue", residue.length === 0]);
checks.push(["built-fixture-id-residue", builtResidue.length === 0]);
const passed = checks.every(([, result]) => result);
await mkdir("artifacts/tests", { recursive: true });
await writeFile(
"artifacts/tests/sample-removal.xml",
`<?xml version="1.0" encoding="UTF-8"?>\n` +
`<testsuite name="reference-feature-removal" tests="${checks.length}" failures="${passed ? 0 : 1}">` +
checks
.map(
([name, result]) =>
`<testcase name="${name}">${result ? "" : `<failure>${[...residue, ...builtResidue].join(", ")}</failure>`}</testcase>`,
)
.join("") +
`</testsuite>\n`,
);
if (!passed) {
const failures = checks
.filter(([, result]) => !result)
.map(([name]) => name);
process.stderr.write(
`Reference feature removal failed: ${failures.join(", ")}; residue: ${[...residue, ...builtResidue].join(", ")}\n`,
);
process.exitCode = 1;
} else {
process.stdout.write(
`Reference feature removal: PASS (${checks.length} checks, no fixture IDs)\n`,
);
}
} finally {
await rm(fixtureRoot, { recursive: true, force: true });
}