refactor: adapter 구현중..

This commit is contained in:
DongHyeonka
2026-08-13 16:02:21 +09:00
parent 30ceac23c1
commit 4dc033cf33
72 changed files with 13370 additions and 1549 deletions
+123 -17
View File
@@ -9,7 +9,11 @@ import {
RELEASE_CANDIDATE_EVIDENCE_PATHS,
RELEASE_CANDIDATE_MANIFEST_PATH,
} from "../lib/release-candidate.ts";
import { validatePackageScriptGraph } from "../lib/package-script-graph.ts";
import {
validateInstallScriptPolicy,
validateNpmScopeEnvironment,
validatePackageScriptGraph,
} from "../lib/package-script-graph.ts";
import { PROMOTED_UPLOAD_PATHS } from "./promotion-artifacts.ts";
const ciActionRegistrationSchema = z
@@ -201,7 +205,22 @@ const artifactSchemaSchema = z.discriminatedUnion("kind", [
kind: z.literal("json"),
maxBytes: z.number().int().min(1).max(268_435_456),
executableSchemaId: z.enum([
"generic-json-object",
"automated-a11y",
"manual-a11y",
"architecture-dependency-report",
"design-system-contract",
"i18n-contract",
"diagnostics-contract",
"realtime-boundaries",
"optional-recipes",
"optional-recipe-fixtures",
"registry-compatibility-fixtures",
"reproducible-build",
"supply-chain-fixtures",
"supply-chain-provider-fixtures",
"compatibility-fixtures",
"documentation-review",
"hosting-headers",
"coverage-summary-v8",
"risk-coverage-v3",
"build-manifest",
@@ -318,12 +337,6 @@ const uploadStep = z
const downloadStep = z
.object({ kind: z.literal("download"), transferId: id, path: workflowPath })
.strict();
const validateCandidateArchiveStep = z
.object({
kind: z.literal("validate-candidate-archive"),
archivePath: workflowPath,
})
.strict();
const extractStep = z
.object({
kind: z.literal("extract"),
@@ -360,7 +373,6 @@ const jobStepSchema = z.discriminatedUnion("kind", [
archiveCandidateStep,
uploadStep,
downloadStep,
validateCandidateArchiveStep,
extractStep,
providerStep,
validateProviderStep,
@@ -374,7 +386,7 @@ const jobSchema = z
displayName: workflowValue,
kind: z.enum(["gate-matrix", "gate-single", "immutable", "provider", "promotion"]),
needs: z.array(id).max(32),
condition: z.enum(["always", "merge", "release", "production", "field", "documentation"]),
condition: z.enum(["always", "needs-success", "merge", "release", "production", "field", "documentation"]),
timeoutMinutes: z.number().int().positive(),
gateIds: z.array(id).max(64),
browserGateIds: z.array(id).max(64),
@@ -455,7 +467,43 @@ function canonicalGateShapeSha256(gates: CiGateContract["gates"]): string {
return createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
}
export function parseCiGateContract(value: unknown): CiGateContract {
function canonicalAuthorityBaselineFailures(contract: CiGateContract): string[] {
const failures: string[] = [];
const commandReferenceCount = contract.gates.reduce(
(total, gate) => total + gate.commandIds.length,
0,
);
if (contract.gates.length !== 26) {
failures.push(`gate authority baseline must contain exactly 26 gates; received ${contract.gates.length}`);
}
if (contract.commands.length !== 81 || commandReferenceCount !== 93) {
failures.push(
`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.stages.length !== 5) {
failures.push(`stage authority baseline must contain exactly 5 stages; received ${contract.stages.length}`);
}
const expectedRetentionClasses = [
{ id: "merge-cycle", policy: "at least through pull-request readiness decision" },
{ id: "release-coherence", policy: "at least until the next release is promoted" },
{ id: "prod-drill", policy: "at least until the next production promotion decision" },
{ id: "field", policy: "through the 28-day window and aggregation" },
{ id: "documentation", policy: "through documentation readiness review" },
];
if (JSON.stringify(contract.retention.classes) !== JSON.stringify(expectedRetentionClasses)) {
failures.push("retention registry must contain exactly the five canonical retention classes");
}
return failures;
}
export function parseCiGateContract(
value: unknown,
options: LoadCiGateContractOptions = {},
): CiGateContract {
const result = ciGateContractSchema.safeParse(value);
if (!result.success) {
const diagnostic = result.error.issues
@@ -463,6 +511,12 @@ export function parseCiGateContract(value: unknown): CiGateContract {
.join("\n");
throw new TypeError(`CI gate contract invalid:\n${diagnostic}`);
}
if ((options.mode ?? "canonical") === "canonical") {
const failures = canonicalAuthorityBaselineFailures(result.data);
if (failures.length > 0) {
throw new TypeError(`CI gate contract invalid:\n${failures.map((failure) => `root: ${failure}`).join("\n")}`);
}
}
return result.data;
}
@@ -470,12 +524,12 @@ export async function loadCiGateContract(
root = process.cwd(),
options: LoadCiGateContractOptions = {},
): Promise<CiGateContract> {
const mode = options.mode ?? "canonical";
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";
const contract = parseCiGateContract(JSON.parse(rawContract), { mode });
if (
mode === "canonical" &&
canonicalGateShapeSha256(contract.gates) !== CANONICAL_GATE_SHAPE_SHA256
@@ -510,13 +564,57 @@ export async function loadCiGateContract(
throw new TypeError(`canonical check:ci dependency drift: ${script}`);
}
}
const graphFailures = validatePackageScriptGraph(packageDocument.scripts, "check:ci");
const contractEntryScripts = [
...new Set(contract.commands.map((command) => command.script)),
];
const graphFailures = [
...validatePackageScriptGraph(packageDocument.scripts, "check:ci"),
...contractEntryScripts.flatMap((script) =>
validatePackageScriptGraph(packageDocument.scripts, script)
),
];
if (graphFailures.length > 0) {
throw new TypeError(`CI package script graph invalid:\n${graphFailures.join("\n")}`);
throw new TypeError(
`CI package script graph invalid:\n${[...new Set(graphFailures)].join("\n")}`,
);
}
const installPolicyFailures = validateInstallScriptPolicy(
packageDocument.scripts,
contractEntryScripts,
);
if (installPolicyFailures.length > 0) {
throw new TypeError(
`CI package script install policy invalid:\n${installPolicyFailures.join("\n")}`,
);
}
return contract;
}
export async function withCiGatePreflight<Result>(
root: string,
gateId: string | undefined,
execute: (context: Readonly<{
contract: CiGateContract;
contractIndex: CiGateContractIndex;
gateId: string;
gate: CiGate;
}>) => Promise<Result> | Result,
): Promise<Result> {
const npmScopeEnvironmentFailures = validateNpmScopeEnvironment(process.env);
if (npmScopeEnvironmentFailures.length > 0) {
throw new TypeError(
`CI runner npm scope environment invalid:\n${npmScopeEnvironmentFailures.join("\n")}`,
);
}
const contract = await loadCiGateContract(root);
const contractIndex = indexCiGateContract(contract);
const gate = gateId ? contractIndex.gates.get(gateId) : undefined;
if (!gateId || !gate) {
throw new TypeError("CI gate id must be FE-GATE-001..FE-GATE-026");
}
return execute({ contract, contractIndex, gateId, gate });
}
export function indexCiGateContract(contract: CiGateContract): CiGateContractIndex {
return Object.freeze({
commands: new Map(contract.commands.map((entry) => [entry.id, entry])),
@@ -625,6 +723,14 @@ function validateContractSemantics(
issue(`unknown retention class ${gate.retentionClassId} for ${gate.id}`);
}
}
const referencedRetentionClasses = new Set(
contract.gates.map(({ retentionClassId }) => retentionClassId),
);
for (const retentionClass of contract.retention.classes) {
if (!referencedRetentionClasses.has(retentionClass.id)) {
issue(`orphan retention class: ${retentionClass.id}`);
}
}
const referencedCommands = new Set(contract.gates.flatMap(({ commandIds }) => commandIds));
for (const command of contract.commands) {
if (!referencedCommands.has(command.id)) issue(`orphan command: ${command.id}`);
@@ -751,7 +857,7 @@ function validateContractSemantics(
immutable_build: ["immutable", ["release_gate"], "release"],
vulnerability_provider: ["provider", ["immutable_build"], "always"],
provenance_provider: ["provider", ["immutable_build"], "always"],
promotion: ["promotion", ["immutable_build", "vulnerability_provider", "provenance_provider"], "always"],
promotion: ["promotion", ["immutable_build", "vulnerability_provider", "provenance_provider"], "needs-success"],
production_gate: ["gate-matrix", ["promotion"], "production"],
field_gate: ["gate-single", ["production_gate"], "field"],
documentation_gate: ["gate-single", [], "documentation"],
@@ -1026,7 +1132,7 @@ function validateJobStepKinds(
"gate-matrix": new Set(["checkout", "setup-node", "frozen-install", "browser-install", "run-gate", "upload"]),
"gate-single": new Set(["checkout", "setup-node", "frozen-install", "run-gate", "upload"]),
immutable: new Set(["checkout", "setup-node", "frozen-install", "run-gate", "archive-candidate", "upload"]),
provider: new Set(["checkout", "setup-node", "frozen-install", "download", "validate-candidate-archive", "extract", "run-provider", "validate-provider-evidence", "upload"]),
provider: new Set(["checkout", "setup-node", "frozen-install", "download", "extract", "run-provider", "validate-provider-evidence", "upload"]),
promotion: new Set(["checkout", "setup-node", "frozen-install", "download", "verify-promotion", "upload", "cleanup-promotion"]),
};
for (const step of job.steps) {