Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89f3c69413 | ||
|
|
5ad6fb032e | ||
|
|
52f4896b63 | ||
|
|
3c347d40d8 |
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"families": {
|
||||
"api": {
|
||||
"additive": {
|
||||
"before": { "required": ["id"], "properties": { "id": {} } },
|
||||
"after": {
|
||||
"required": ["id"],
|
||||
"properties": { "id": {}, "displayName": {} }
|
||||
}
|
||||
},
|
||||
"breaking": {
|
||||
"before": { "required": ["id"], "properties": { "id": {} } },
|
||||
"after": {
|
||||
"required": ["id", "name"],
|
||||
"properties": { "id": {}, "name": {} }
|
||||
}
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"additive": {
|
||||
"before": { "required": ["APP_ENV"], "properties": { "APP_ENV": {} } },
|
||||
"after": {
|
||||
"required": ["APP_ENV"],
|
||||
"properties": { "APP_ENV": {}, "OPTIONAL_FLAG": {} }
|
||||
}
|
||||
},
|
||||
"breaking": {
|
||||
"before": { "required": ["APP_ENV"], "properties": { "APP_ENV": {} } },
|
||||
"after": {
|
||||
"required": ["APP_ENV", "NEW_REQUIRED"],
|
||||
"properties": { "APP_ENV": {}, "NEW_REQUIRED": {} }
|
||||
}
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
"additive": {
|
||||
"before": { "properties": { "theme": {} } },
|
||||
"after": { "properties": { "theme": {}, "contrast": {} } }
|
||||
},
|
||||
"breaking": {
|
||||
"before": { "properties": { "theme": {} } },
|
||||
"after": { "properties": {} }
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"additive": {
|
||||
"before": { "required": ["buildId"], "properties": { "buildId": {} } },
|
||||
"after": {
|
||||
"required": ["buildId"],
|
||||
"properties": { "buildId": {}, "builtAt": {} }
|
||||
}
|
||||
},
|
||||
"breaking": {
|
||||
"before": { "required": ["buildId"], "properties": { "buildId": {} } },
|
||||
"after": {
|
||||
"required": ["buildId", "assetManifestHash"],
|
||||
"properties": { "buildId": {}, "assetManifestHash": {} }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# Contract compatibility and rollback rules
|
||||
|
||||
The blocking tuple is `(buildId, configSchemaVersion, apiContractVersion,
|
||||
assetManifestHash, releaseId)`. Versions are parsed numerically.
|
||||
|
||||
1. additive changes preserve current required fields
|
||||
2. breaking changes require a major version bump
|
||||
3. persisted cache is discarded unless an explicit tested migration exists
|
||||
4. an incompatible config or API contract blocks product mount
|
||||
5. rollback restores HTML, assets, runtime config, API compatibility, and
|
||||
release manifest as one coherent set
|
||||
+3
-1
@@ -29,7 +29,9 @@
|
||||
"verify:lockfile": "corepack pnpm install --frozen-lockfile",
|
||||
"generate:supply-chain": "node scripts/generate-supply-chain.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",
|
||||
"verify:compatibility": "node scripts/check-compatibility.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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,43 @@
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
|
||||
import { classifyObjectSchemaChange } from "../src/application/policies/compatibility.js";
|
||||
|
||||
const fixtures = JSON.parse(
|
||||
await readFile("config/compatibility/fixtures.json", "utf8"),
|
||||
);
|
||||
const results = [];
|
||||
|
||||
for (const [family, cases] of Object.entries(fixtures.families)) {
|
||||
for (const expected of ["additive", "breaking"]) {
|
||||
const fixture = cases[expected];
|
||||
const actual = classifyObjectSchemaChange(fixture.before, fixture.after);
|
||||
results.push({ family, expected, actual, passed: actual === expected });
|
||||
}
|
||||
}
|
||||
|
||||
await mkdir("artifacts/release", { recursive: true });
|
||||
await writeFile(
|
||||
"artifacts/release/compatibility.json",
|
||||
`${JSON.stringify(
|
||||
{
|
||||
schemaVersion: 1,
|
||||
generatedAt: new Date().toISOString(),
|
||||
rules: [
|
||||
"additive changes preserve required fields",
|
||||
"breaking changes require version bump and migration, discard, fallback, or rollback",
|
||||
"config and API major versions must match",
|
||||
"incompatible persisted cache is discarded by default",
|
||||
"rollback uses a coherent compatibility tuple",
|
||||
],
|
||||
results,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
|
||||
if (results.some((result) => !result.passed)) {
|
||||
process.stderr.write("Compatibility fixture classification failed.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
process.stdout.write("Compatibility fixtures: PASS\n");
|
||||
@@ -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,102 @@
|
||||
export const COMPATIBILITY_TUPLE_FIELDS = Object.freeze([
|
||||
"buildId",
|
||||
"configSchemaVersion",
|
||||
"apiContractVersion",
|
||||
"assetManifestHash",
|
||||
"releaseId",
|
||||
]);
|
||||
|
||||
/** @param {string} version */
|
||||
export function parseNumericVersion(version) {
|
||||
const match = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?$/.exec(version);
|
||||
if (!match) return null;
|
||||
return {
|
||||
major: Number(match[1]),
|
||||
minor: Number(match[2] ?? 0),
|
||||
patch: Number(match[3] ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {string} supported @param {string} actual */
|
||||
export function isVersionCompatible(supported, actual) {
|
||||
const expected = parseNumericVersion(supported);
|
||||
const candidate = parseNumericVersion(actual);
|
||||
if (!expected || !candidate) return false;
|
||||
return (
|
||||
expected.major === candidate.major &&
|
||||
candidate.minor >= expected.minor
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* frontend: {
|
||||
* buildId: string,
|
||||
* configSchemaVersion: string,
|
||||
* apiContractVersion: string,
|
||||
* assetManifestHash: string,
|
||||
* releaseId: string
|
||||
* },
|
||||
* runtime: {
|
||||
* buildId: string,
|
||||
* configSchemaVersion: string,
|
||||
* apiContractVersion: string,
|
||||
* assetManifestHash: string,
|
||||
* releaseId: string
|
||||
* }
|
||||
* }} input
|
||||
*/
|
||||
export function verifyCompatibilityTuple(input) {
|
||||
const mismatches = [];
|
||||
if (input.frontend.buildId !== input.runtime.buildId) mismatches.push("buildId");
|
||||
if (
|
||||
!isVersionCompatible(
|
||||
input.frontend.configSchemaVersion,
|
||||
input.runtime.configSchemaVersion,
|
||||
)
|
||||
) {
|
||||
mismatches.push("configSchemaVersion");
|
||||
}
|
||||
if (
|
||||
!isVersionCompatible(
|
||||
input.frontend.apiContractVersion,
|
||||
input.runtime.apiContractVersion,
|
||||
)
|
||||
) {
|
||||
mismatches.push("apiContractVersion");
|
||||
}
|
||||
if (input.frontend.assetManifestHash !== input.runtime.assetManifestHash) {
|
||||
mismatches.push("assetManifestHash");
|
||||
}
|
||||
|
||||
const releaseWarning =
|
||||
input.frontend.releaseId === input.runtime.releaseId
|
||||
? null
|
||||
: "releaseId";
|
||||
return Object.freeze({
|
||||
compatible: mismatches.length === 0,
|
||||
mismatches: Object.freeze(mismatches),
|
||||
warnings: Object.freeze(releaseWarning ? [releaseWarning] : []),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ required?: string[], properties?: Record<string, unknown> }} before
|
||||
* @param {{ required?: string[], properties?: Record<string, unknown> }} after
|
||||
*/
|
||||
export function classifyObjectSchemaChange(before, after) {
|
||||
const beforeRequired = new Set(before.required ?? []);
|
||||
const afterRequired = new Set(after.required ?? []);
|
||||
const removedProperties = Object.keys(before.properties ?? {}).filter(
|
||||
(key) => !(key in (after.properties ?? {})),
|
||||
);
|
||||
const addedRequired = [...afterRequired].filter(
|
||||
(key) => !beforeRequired.has(key),
|
||||
);
|
||||
if (removedProperties.length > 0 || addedRequired.length > 0) return "breaking";
|
||||
|
||||
const addedProperties = Object.keys(after.properties ?? {}).filter(
|
||||
(key) => !(key in (before.properties ?? {})),
|
||||
);
|
||||
return addedProperties.length > 0 ? "additive" : "none";
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
classifyObjectSchemaChange,
|
||||
isVersionCompatible,
|
||||
parseNumericVersion,
|
||||
verifyCompatibilityTuple,
|
||||
} from "../../src/application/policies/compatibility.js";
|
||||
|
||||
describe("contract compatibility", () => {
|
||||
it("uses numeric version parsing rather than lexical comparison", () => {
|
||||
expect(parseNumericVersion("1.10.0")).toEqual({ major: 1, minor: 10, patch: 0 });
|
||||
expect(isVersionCompatible("1.9", "1.10")).toBe(true);
|
||||
expect(isVersionCompatible("1.9", "2.0")).toBe(false);
|
||||
expect(isVersionCompatible("next", "1.0")).toBe(false);
|
||||
});
|
||||
|
||||
it("distinguishes additive and breaking object changes", () => {
|
||||
const base = { required: ["id"], properties: { id: {} } };
|
||||
expect(
|
||||
classifyObjectSchemaChange(base, {
|
||||
required: ["id"],
|
||||
properties: { id: {}, name: {} },
|
||||
}),
|
||||
).toBe("additive");
|
||||
expect(
|
||||
classifyObjectSchemaChange(base, {
|
||||
required: ["id", "name"],
|
||||
properties: { id: {}, name: {} },
|
||||
}),
|
||||
).toBe("breaking");
|
||||
});
|
||||
|
||||
it("treats release ID mismatch as a warning when the blocking tuple is coherent", () => {
|
||||
const frontend = {
|
||||
buildId: "build-a",
|
||||
configSchemaVersion: "1.0",
|
||||
apiContractVersion: "1.0",
|
||||
assetManifestHash: "hash-a",
|
||||
releaseId: "release-a",
|
||||
};
|
||||
expect(
|
||||
verifyCompatibilityTuple({
|
||||
frontend,
|
||||
runtime: { ...frontend, releaseId: "release-b" },
|
||||
}),
|
||||
).toEqual({
|
||||
compatible: true,
|
||||
mismatches: [],
|
||||
warnings: ["releaseId"],
|
||||
});
|
||||
});
|
||||
|
||||
it("blocks mixed build, config, API, or asset tuples", () => {
|
||||
const frontend = {
|
||||
buildId: "build-a",
|
||||
configSchemaVersion: "1.0",
|
||||
apiContractVersion: "1.0",
|
||||
assetManifestHash: "hash-a",
|
||||
releaseId: "release-a",
|
||||
};
|
||||
expect(
|
||||
verifyCompatibilityTuple({
|
||||
frontend,
|
||||
runtime: {
|
||||
...frontend,
|
||||
buildId: "build-b",
|
||||
configSchemaVersion: "2.0",
|
||||
assetManifestHash: "hash-b",
|
||||
},
|
||||
}),
|
||||
).toMatchObject({
|
||||
compatible: false,
|
||||
mismatches: ["buildId", "configSchemaVersion", "assetManifestHash"],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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