Merge branch 'feature-frontend-contract-registry-governance' into develop
This commit is contained in:
@@ -0,0 +1,158 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"registries": [
|
||||||
|
{
|
||||||
|
"registryId": "FE-REG-ROUTE",
|
||||||
|
"path": "src/contracts/routes.js",
|
||||||
|
"exportName": "ROUTE_REGISTRY",
|
||||||
|
"owner": "feature-routing-navigation-guard-contract",
|
||||||
|
"requiredFields": [
|
||||||
|
"routeId",
|
||||||
|
"path",
|
||||||
|
"paramsSchema",
|
||||||
|
"searchSchema",
|
||||||
|
"access",
|
||||||
|
"loadingSurface",
|
||||||
|
"errorSurface",
|
||||||
|
"chunkId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"registryId": "FE-REG-API",
|
||||||
|
"path": "src/contracts/api-operations.js",
|
||||||
|
"exportName": "API_OPERATIONS",
|
||||||
|
"owner": "feature-api-client-response-envelope-contract",
|
||||||
|
"requiredFields": [
|
||||||
|
"method",
|
||||||
|
"path",
|
||||||
|
"operationId",
|
||||||
|
"auth",
|
||||||
|
"timeoutMs",
|
||||||
|
"idempotency",
|
||||||
|
"requestSchema",
|
||||||
|
"responseSchema",
|
||||||
|
"owner"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"registryId": "FE-REG-ENV",
|
||||||
|
"path": "src/contracts/env.js",
|
||||||
|
"exportName": "ENV_REGISTRY",
|
||||||
|
"owner": "feature-frontend-env-runtime-config-contract",
|
||||||
|
"requiredFields": ["phase", "classification", "required", "defaultValue"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"registryId": "FE-REG-STORAGE",
|
||||||
|
"path": "src/contracts/storage-keys.js",
|
||||||
|
"exportName": "STORAGE_REGISTRY",
|
||||||
|
"owner": "feature-frontend-storage-registry-contract",
|
||||||
|
"requiredFields": [
|
||||||
|
"logicalName",
|
||||||
|
"physicalKey",
|
||||||
|
"backend",
|
||||||
|
"classification",
|
||||||
|
"schemaVersion",
|
||||||
|
"ttl",
|
||||||
|
"migration",
|
||||||
|
"quotaFallback"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"registryId": "FE-REG-ERROR",
|
||||||
|
"path": "src/contracts/errors.js",
|
||||||
|
"exportName": "ERROR_REGISTRY",
|
||||||
|
"owner": "feature-frontend-error-classification-boundary-contract",
|
||||||
|
"requiredFields": [
|
||||||
|
"kind",
|
||||||
|
"defaultRetryable",
|
||||||
|
"severity",
|
||||||
|
"userMessageKey",
|
||||||
|
"action",
|
||||||
|
"telemetryEvent",
|
||||||
|
"redaction"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"registryId": "FE-REG-QUERY",
|
||||||
|
"path": "src/contracts/query-keys.js",
|
||||||
|
"exportName": "QUERY_REGISTRY",
|
||||||
|
"owner": "feature-server-state-caching-contract",
|
||||||
|
"requiredFields": [
|
||||||
|
"namespace",
|
||||||
|
"serialization",
|
||||||
|
"identity",
|
||||||
|
"invalidation",
|
||||||
|
"version",
|
||||||
|
"persistence"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"registryId": "FE-REG-TELEMETRY",
|
||||||
|
"path": "src/contracts/telemetry.js",
|
||||||
|
"exportName": "TELEMETRY_REGISTRY",
|
||||||
|
"owner": "feature-frontend-observability-logging-trace-contract",
|
||||||
|
"requiredFields": [
|
||||||
|
"eventName",
|
||||||
|
"trigger",
|
||||||
|
"requiredAttributes",
|
||||||
|
"optionalAttributes",
|
||||||
|
"forbiddenAttributes",
|
||||||
|
"sampling",
|
||||||
|
"delivery"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"registryId": "FE-REG-RELEASE",
|
||||||
|
"path": "src/contracts/release-tokens.js",
|
||||||
|
"exportName": "RELEASE_TOKEN_REGISTRY",
|
||||||
|
"owner": "feature-frontend-release-cache-rollback-contract",
|
||||||
|
"requiredFields": ["token", "source", "compatibilityRole"],
|
||||||
|
"declaredRows": {
|
||||||
|
"appVersion": {
|
||||||
|
"token": "appVersion",
|
||||||
|
"source": "manifest",
|
||||||
|
"compatibilityRole": "human release label"
|
||||||
|
},
|
||||||
|
"buildId": {
|
||||||
|
"token": "buildId",
|
||||||
|
"source": "CI build",
|
||||||
|
"compatibilityRole": "asset and HTML coherence"
|
||||||
|
},
|
||||||
|
"commitSha": {
|
||||||
|
"token": "commitSha",
|
||||||
|
"source": "VCS",
|
||||||
|
"compatibilityRole": "source traceability"
|
||||||
|
},
|
||||||
|
"configSchemaVersion": {
|
||||||
|
"token": "configSchemaVersion",
|
||||||
|
"source": "runtime config schema",
|
||||||
|
"compatibilityRole": "boot compatibility"
|
||||||
|
},
|
||||||
|
"apiContractVersion": {
|
||||||
|
"token": "apiContractVersion",
|
||||||
|
"source": "frontend/backend agreement",
|
||||||
|
"compatibilityRole": "schema compatibility"
|
||||||
|
},
|
||||||
|
"assetManifestHash": {
|
||||||
|
"token": "assetManifestHash",
|
||||||
|
"source": "build output",
|
||||||
|
"compatibilityRole": "chunk integrity"
|
||||||
|
},
|
||||||
|
"releaseId": {
|
||||||
|
"token": "releaseId",
|
||||||
|
"source": "deploy system",
|
||||||
|
"compatibilityRole": "rollback target"
|
||||||
|
},
|
||||||
|
"builtAt": {
|
||||||
|
"token": "builtAt",
|
||||||
|
"source": "CI",
|
||||||
|
"compatibilityRole": "diagnostics only"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"compatibilityImpact": {
|
||||||
|
"allowed": ["none", "additive", "behavior-change", "breaking"],
|
||||||
|
"current": "additive"
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-1
@@ -29,7 +29,8 @@
|
|||||||
"verify:lockfile": "corepack pnpm install --frozen-lockfile",
|
"verify:lockfile": "corepack pnpm install --frozen-lockfile",
|
||||||
"generate:supply-chain": "node scripts/generate-supply-chain.mjs",
|
"generate:supply-chain": "node scripts/generate-supply-chain.mjs",
|
||||||
"scan:security": "node scripts/security-scan.mjs",
|
"scan:security": "node scripts/security-scan.mjs",
|
||||||
"check:browser-security": "node scripts/check-browser-security.mjs"
|
"check:browser-security": "node scripts/check-browser-security.mjs",
|
||||||
|
"check:registries": "node scripts/check-registries.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "5.101.4",
|
"@tanstack/react-query": "5.101.4",
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"schemaVersion",
|
||||||
|
"generatedAt",
|
||||||
|
"compatibilityImpact",
|
||||||
|
"failures",
|
||||||
|
"registries"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"schemaVersion": { "const": 1 },
|
||||||
|
"generatedAt": { "type": "string", "format": "date-time" },
|
||||||
|
"compatibilityImpact": {
|
||||||
|
"enum": ["none", "additive", "behavior-change", "breaking"]
|
||||||
|
},
|
||||||
|
"failures": { "type": "array", "maxItems": 0 },
|
||||||
|
"registries": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 8,
|
||||||
|
"maxItems": 8,
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["registryId", "owner", "source", "rowCount", "rows"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import { pathToFileURL } from "node:url";
|
||||||
|
|
||||||
|
const governance = JSON.parse(
|
||||||
|
await readFile("config/contracts/registry-governance.json", "utf8"),
|
||||||
|
);
|
||||||
|
const failures = [];
|
||||||
|
const owners = new Map();
|
||||||
|
const snapshots = [];
|
||||||
|
|
||||||
|
for (const specification of governance.registries) {
|
||||||
|
if (owners.has(specification.registryId)) {
|
||||||
|
failures.push(`duplicate owner for ${specification.registryId}`);
|
||||||
|
}
|
||||||
|
owners.set(specification.registryId, specification.owner);
|
||||||
|
|
||||||
|
let rows = specification.declaredRows;
|
||||||
|
try {
|
||||||
|
await access(specification.path);
|
||||||
|
const module = await import(
|
||||||
|
`${pathToFileURL(path.resolve(specification.path)).href}?registry-check=${Date.now()}`
|
||||||
|
);
|
||||||
|
rows = module[specification.exportName];
|
||||||
|
} catch {
|
||||||
|
if (!rows) failures.push(`missing registry source ${specification.path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rows || typeof rows !== "object" || Array.isArray(rows)) {
|
||||||
|
failures.push(`${specification.registryId} is not an object registry`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [rowName, row] of Object.entries(rows)) {
|
||||||
|
if (!row || typeof row !== "object" || Array.isArray(row)) {
|
||||||
|
failures.push(`${specification.registryId}.${rowName} is not an object`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const field of specification.requiredFields) {
|
||||||
|
if (!(field in row)) {
|
||||||
|
failures.push(`${specification.registryId}.${rowName} missing ${field}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshots.push({
|
||||||
|
registryId: specification.registryId,
|
||||||
|
owner: specification.owner,
|
||||||
|
source: specification.path,
|
||||||
|
rowCount: Object.keys(rows).length,
|
||||||
|
rows,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceFiles = [
|
||||||
|
"src/application",
|
||||||
|
"src/presentation",
|
||||||
|
"src/domain",
|
||||||
|
];
|
||||||
|
const adHocPatterns = [
|
||||||
|
{ name: "direct fetch", expression: /\bfetch\s*\(/ },
|
||||||
|
{ name: "direct localStorage", expression: /\blocalStorage\.(?:get|set|remove)Item/ },
|
||||||
|
{ name: "direct import.meta.env", expression: /\bimport\.meta\.env\./ },
|
||||||
|
{ name: "raw API path", expression: /["']\/api\// },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @param {string} directory */
|
||||||
|
async function scanDirectory(directory) {
|
||||||
|
const entries = await import("node:fs/promises").then(({ readdir }) =>
|
||||||
|
readdir(directory, { withFileTypes: true }),
|
||||||
|
);
|
||||||
|
for (const entry of entries) {
|
||||||
|
const target = path.join(directory, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
await scanDirectory(target);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!/\.(js|jsx|mjs)$/.test(entry.name)) continue;
|
||||||
|
const content = await readFile(target, "utf8");
|
||||||
|
for (const pattern of adHocPatterns) {
|
||||||
|
if (pattern.expression.test(content)) {
|
||||||
|
failures.push(`ad-hoc ${pattern.name} in ${target}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const sourceDirectory of sourceFiles) {
|
||||||
|
await scanDirectory(sourceDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
await mkdir("artifacts/quality", { recursive: true });
|
||||||
|
await writeFile(
|
||||||
|
"artifacts/quality/registries.json",
|
||||||
|
`${JSON.stringify(
|
||||||
|
{
|
||||||
|
schemaVersion: 1,
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
compatibilityImpact: governance.compatibilityImpact.current,
|
||||||
|
failures,
|
||||||
|
registries: snapshots,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}\n`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (failures.length > 0) {
|
||||||
|
process.stderr.write(`Registry governance failed:\n${failures.join("\n")}\n`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
process.stdout.write(`Registry governance: ${snapshots.length} registries PASS\n`);
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
describe("registry governance manifest", () => {
|
||||||
|
it("declares exactly eight single-owner registries and impact labels", async () => {
|
||||||
|
const governance = JSON.parse(
|
||||||
|
await readFile("config/contracts/registry-governance.json", "utf8"),
|
||||||
|
);
|
||||||
|
expect(governance.registries).toHaveLength(8);
|
||||||
|
expect(new Set(governance.registries.map((entry) => entry.registryId)).size).toBe(
|
||||||
|
8,
|
||||||
|
);
|
||||||
|
expect(governance.registries.every((entry) => entry.owner)).toBe(true);
|
||||||
|
expect(governance.compatibilityImpact.allowed).toEqual([
|
||||||
|
"none",
|
||||||
|
"additive",
|
||||||
|
"behavior-change",
|
||||||
|
"breaking",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user