From f19be639a3745d29b7c08a5338134224326a8a78 Mon Sep 17 00:00:00 2001 From: DongHyeonka Date: Tue, 18 Aug 2026 09:03:14 +0900 Subject: [PATCH] ci: run the TechLog contract drift gate in CI and test:all `check:tech-log-contract` existed and worked but nothing ran it. No gate in `config/ci/gates.json` referenced it and `test:all` did not chain it, so a hand-edit of the vendored canonical yaml or of `generated.ts` passed every gate the repository actually executes -- the exact regression the digest pin exists to prevent. FE-GATE-010 (architecture/contract governance) now owns the command, beside `check-registries` and `check-ci`, and `test:all` runs it in front of `test:tech-log`. The canonical authority baseline moves to 83 command definitions / 95 references and the gate-shape SHA-256 is recomputed with `canonicalGateShapeSha256`; the recomputation was first verified by reproducing the previous constant from the previous gates.json. `tests/features/tech-log/contract-generation.test.ts` now fails if either wiring is removed again. Co-Authored-By: Claude Opus 5 (1M context) --- config/ci/gates.json | 6 ++++ package.json | 2 +- scripts/contracts/ci-gates.ts | 13 ++++++-- .../tech-log/contract-generation.test.ts | 32 +++++++++++++++++++ tests/unit/ci-workflow-generation.test.ts | 5 +-- .../unit/task3-selective-integration.test.ts | 5 +-- 6 files changed, 55 insertions(+), 8 deletions(-) diff --git a/config/ci/gates.json b/config/ci/gates.json index 76759fc..a1d6d2c 100644 --- a/config/ci/gates.json +++ b/config/ci/gates.json @@ -323,6 +323,11 @@ "expectedExitCode": 1, "expectedDiagnosticId": "duplicates routeId=DUPLICATE" }, + { + "id": "check-tech-log-contract", + "script": "check:tech-log-contract", + "expect": "pass" + }, { "id": "build", "script": "build", @@ -1950,6 +1955,7 @@ "check-registries-baseline-fixture", "check-registries-fixture", "check-routes-fixture", + "check-tech-log-contract", "check-ci" ], "logArtifactId": "artifact-artifacts-quality-gates-FE-GATE-010-txt", diff --git a/package.json b/package.json index bc2c822..5086c29 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "check:v8-coverage-counter-semantics": "node scripts/check-v8-coverage-counter-semantics.ts", "test:coverage": "corepack pnpm check:v8-coverage-counter-semantics && vitest run tests/runtime-schema tests/unit tests/component tests/integration tests/features/reference-feature tests/features/tech-log --coverage --maxWorkers=4 --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/coverage.xml && node scripts/check-risk-coverage.ts", "check:coverage:fixture": "node scripts/check-risk-coverage.ts --summary tests/fixtures/coverage/below-threshold.json --artifact artifacts/quality/risk-coverage-fixture.json", - "test:all": "corepack pnpm test:runtime-schema && corepack pnpm test:unit && corepack pnpm test:component && corepack pnpm test:integration && corepack pnpm test:reference-feature && corepack pnpm test:tech-log && corepack pnpm test:recipes", + "test:all": "corepack pnpm test:runtime-schema && corepack pnpm test:unit && corepack pnpm test:component && corepack pnpm test:integration && corepack pnpm test:reference-feature && corepack pnpm check:tech-log-contract && corepack pnpm test:tech-log && corepack pnpm test:recipes", "verify:lockfile": "corepack pnpm install --frozen-lockfile --ignore-scripts", "check:frozen-lockfile:fixture": "node scripts/check-frozen-lockfile-fixture.ts", "generate:artifact-schemas": "node scripts/generate-artifact-schemas.ts", diff --git a/scripts/contracts/ci-gates.ts b/scripts/contracts/ci-gates.ts index a8f1247..ddb9996 100644 --- a/scripts/contracts/ci-gates.ts +++ b/scripts/contracts/ci-gates.ts @@ -448,7 +448,11 @@ const CANONICAL_GATE_SHAPE_SHA256 = // describes the merged one. Recomputed from the merged config/ci/gates.json. // Task 11: recomputed again after FE-GATE-009 gained the // TECH_LOG_STUDIO_ASSETS manual accessibility evidence artifact. - "3dda8421822eb094ceb06693ed8143cd9298947fa86681bbd941726a0d9674a9"; + // Final fix wave item 1: recomputed again after FE-GATE-010 gained + // `check-tech-log-contract`. Recomputed with `canonicalGateShapeSha256` + // below, verified by first reproducing the previous constant from the + // previous `config/ci/gates.json` before hashing the new one. + "f3cc90758084e16757e71a8a3fe772d8e408f0c51b044294f329309027ce6563"; function canonicalGateShapeSha256(gates: CiGateContract["gates"]): string { const normalized = gates.map( @@ -482,9 +486,12 @@ function canonicalAuthorityBaselineFailures(contract: CiGateContract): string[] if (contract.gates.length !== 27) { failures.push(`gate authority baseline must contain exactly 27 gates; received ${contract.gates.length}`); } - if (contract.commands.length !== 82 || commandReferenceCount !== 94) { + // Final fix wave, item 1: FE-GATE-010 gained `check-tech-log-contract`, the + // drift gate that pins the vendored canonical Studio contract to its digest. + // Until it was referenced by a gate it ran only when typed by hand. + if (contract.commands.length !== 83 || commandReferenceCount !== 95) { failures.push( - `command authority baseline must contain exactly 82 definitions and 94 references; received ${contract.commands.length} definitions and ${commandReferenceCount} references`, + `command authority baseline must contain exactly 83 definitions and 95 references; received ${contract.commands.length} definitions and ${commandReferenceCount} references`, ); } // Template merge. 126 product artifacts plus the two the template added. diff --git a/tests/features/tech-log/contract-generation.test.ts b/tests/features/tech-log/contract-generation.test.ts index af59cdf..c02fc67 100644 --- a/tests/features/tech-log/contract-generation.test.ts +++ b/tests/features/tech-log/contract-generation.test.ts @@ -4,8 +4,11 @@ import { createHash } from "node:crypto"; import { test } from "vitest"; import canonicalSource from "../../../src/features/tech-log/contracts/studio/canonical-source.json" with { type: "json" }; +import ciGates from "../../../config/ci/gates.json" with { type: "json" }; +import packageDocument from "../../../package.json" with { type: "json" }; const YAML_PATH = "src/features/tech-log/contracts/studio/studio-api.openapi.yaml"; +const DRIFT_GATE_SCRIPT = "check:tech-log-contract"; test("vendored contract matches the recorded canonical digest", () => { const bytes = readFileSync(YAML_PATH); @@ -32,3 +35,32 @@ test("vendored contract declares the CSRF header", () => { const yaml = readFileSync(YAML_PATH, "utf8"); assert.ok(yaml.includes("X-CSRF-TOKEN")); }); + +// 이 브랜치의 중심 산출물은 "canonical 계약이 다시 갈라지지 못하게 빌드로 막는다" +// (§선택한 접근 A)이다. 스크립트가 존재하는 것만으로는 그 약속이 지켜지지 않는다. +// 누군가 손으로 vendor yaml이나 generated.ts를 고쳐도, 실제로 실행되는 게이트가 +// 하나도 그것을 보지 않으면 digest 고정은 의미를 잃는다. 아래 두 테스트가 +// "실행 경로에 실제로 연결돼 있는가"를 검증한다. +test("the contract drift check is a real CI gate command, not just a package script", () => { + const command = ciGates.commands.find((entry) => entry.script === DRIFT_GATE_SCRIPT); + assert.ok(command, `config/ci/gates.json declares no ${DRIFT_GATE_SCRIPT} command`); + assert.equal(command.expect, "pass"); + + const owningGates = ciGates.gates.filter((gate) => + (gate.commandIds as readonly string[]).includes(command.id), + ); + assert.equal( + owningGates.length, + 1, + `${command.id} must be referenced by exactly one gate; found ${owningGates.length}`, + ); +}); + +test("test:all runs the contract drift check alongside the TechLog suite", () => { + const segments = packageDocument.scripts["test:all"].split(" && ").map((value) => value.trim()); + assert.ok( + segments.includes(`corepack pnpm ${DRIFT_GATE_SCRIPT}`), + `test:all does not run ${DRIFT_GATE_SCRIPT}: ${packageDocument.scripts["test:all"]}`, + ); + assert.ok(segments.includes("corepack pnpm test:tech-log"), packageDocument.scripts["test:all"]); +}); diff --git a/tests/unit/ci-workflow-generation.test.ts b/tests/unit/ci-workflow-generation.test.ts index f331947..25af7bd 100644 --- a/tests/unit/ci-workflow-generation.test.ts +++ b/tests/unit/ci-workflow-generation.test.ts @@ -189,8 +189,9 @@ describe("CI gate contract", () => { ), ); expect(contract.jobs).toHaveLength(9); - expect(contract.commands).toHaveLength(82); - expect(contract.gates.reduce((total, gate) => total + gate.commandIds.length, 0)).toBe(94); + // Final fix wave item 1: FE-GATE-010 gained `check-tech-log-contract`. + expect(contract.commands).toHaveLength(83); + expect(contract.gates.reduce((total, gate) => total + gate.commandIds.length, 0)).toBe(95); expect(contract.commands.filter(({ expect }) => expect === "fail")).toHaveLength(23); // Template merge. Recounted from the merged config/ci/gates.json rather // than taking either side's number. diff --git a/tests/unit/task3-selective-integration.test.ts b/tests/unit/task3-selective-integration.test.ts index 0e3a097..207d88f 100644 --- a/tests/unit/task3-selective-integration.test.ts +++ b/tests/unit/task3-selective-integration.test.ts @@ -185,9 +185,10 @@ describe("selective Task 3 contract closure", () => { // product's 26/81; the artifact set is the product's 126 plus the // template's 2. Task 11 added TECH_LOG_STUDIO_ASSETS's manual // accessibility evidence, bringing the total to 129. + // Final fix wave item 1 added `check-tech-log-contract` to FE-GATE-010. expect(canonical.gates).toHaveLength(27); - expect(canonical.commands).toHaveLength(82); - expect(canonical.gates.reduce((sum, gate) => sum + gate.commandIds.length, 0)).toBe(94); + expect(canonical.commands).toHaveLength(83); + expect(canonical.gates.reduce((sum, gate) => sum + gate.commandIds.length, 0)).toBe(95); expect(canonical.artifacts).toHaveLength(129); expect(canonical.stages).toHaveLength(5); expect(canonical.retention.classes).toHaveLength(5);