This commit is contained in:
+65
-4
@@ -23,6 +23,20 @@ import { spawnSync } from 'node:child_process';
|
||||
import { verifyHistory } from './gate-history.mjs';
|
||||
|
||||
const COPY_SKIP = new Set(['.git', '.mosaic-test-work', '.next', '.turbo', 'coverage', 'dist']);
|
||||
const POPULATION_CRITERION_IDS = new Set([
|
||||
'RM02-EVIDENCE-SUBJECT-BINDING',
|
||||
'RM02-TYPE-STRICT-SCHEMA',
|
||||
'RM02-NONEMPTY-ANCHORED-QUANTIFICATION',
|
||||
]);
|
||||
const REQUIRED_GATE_INVENTORY = new Map([
|
||||
['quality-typecheck', 'package.json'],
|
||||
['quality-lint', 'package.json'],
|
||||
['quality-format', 'package.json'],
|
||||
['checkout-preflight', 'scripts/preflight.mjs'],
|
||||
['ci-queue-wait', 'packages/mosaic/framework/tools/git/ci-queue-wait.sh'],
|
||||
['hook-pre-commit', '.husky/pre-commit'],
|
||||
['hook-pre-push', '.husky/pre-push'],
|
||||
]);
|
||||
|
||||
function parseArgs(argv) {
|
||||
const options = {
|
||||
@@ -382,7 +396,14 @@ function validateEnvironment(environment, label, failures) {
|
||||
}
|
||||
}
|
||||
|
||||
function validateClosedSchema(manifest, failures) {
|
||||
function validateClosedSchema(manifest, failures, { fixtureProfile = false } = {}) {
|
||||
if (!fixtureProfile) {
|
||||
for (const population of ['criteria', 'gates', 'proseClaims', 'compatibilityScenarios']) {
|
||||
if (!Array.isArray(manifest[population]) || manifest[population].length === 0) {
|
||||
failures.push(`${population} population must be non-empty and anchored before evaluation`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (manifest.schemaVersion !== 1)
|
||||
failures.push(`unsupported schemaVersion ${String(manifest.schemaVersion)}`);
|
||||
rejectUnknownKeys(
|
||||
@@ -450,6 +471,7 @@ function validateClosedSchema(manifest, failures) {
|
||||
'source',
|
||||
'meaningChanges',
|
||||
'caseRefs',
|
||||
'gateRefs',
|
||||
]),
|
||||
`criterion ${criterion.id}`,
|
||||
failures,
|
||||
@@ -475,6 +497,12 @@ function validateClosedSchema(manifest, failures) {
|
||||
);
|
||||
}
|
||||
}
|
||||
if (POPULATION_CRITERION_IDS.has(criterion.id) && criterion.gateRefs === undefined) {
|
||||
failures.push(`${criterion.id}: gateRefs population binding is required`);
|
||||
}
|
||||
if (criterion.gateRefs !== undefined) {
|
||||
validateStringArray(criterion.gateRefs, `criterion ${criterion.id}.gateRefs`, failures);
|
||||
}
|
||||
if (!Array.isArray(criterion.caseRefs) || criterion.caseRefs.length === 0) {
|
||||
failures.push(`${criterion.id}: no declared exercising cases`);
|
||||
} else {
|
||||
@@ -535,6 +563,16 @@ function validateClosedSchema(manifest, failures) {
|
||||
validateFixture(scenario.fixture, `${scenario.id}.fixture`, failures);
|
||||
}
|
||||
rejectDuplicateIds(manifest.gates, 'gate', failures);
|
||||
if (!fixtureProfile) {
|
||||
for (const [requiredId, requiredSource] of REQUIRED_GATE_INVENTORY) {
|
||||
const registered = (manifest.gates ?? []).find((gate) => gate?.id === requiredId);
|
||||
if (!registered || registered.source !== requiredSource) {
|
||||
failures.push(
|
||||
`gates population is not anchored: required ${requiredId} at ${requiredSource}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const gate of manifest.gates ?? []) {
|
||||
rejectUnknownKeys(
|
||||
gate,
|
||||
@@ -546,12 +584,19 @@ function validateClosedSchema(manifest, failures) {
|
||||
'inertMutation',
|
||||
'cases',
|
||||
'discoveryAliases',
|
||||
'evidenceSubject',
|
||||
]),
|
||||
`gate ${gate.id}`,
|
||||
failures,
|
||||
);
|
||||
requireString(gate.id, `gate ${gate.id}.id`, failures);
|
||||
requireString(gate.source, `gate ${gate.id}.source`, failures);
|
||||
requireString(gate.evidenceSubject, `gate ${gate.id}.evidenceSubject`, failures);
|
||||
if (gate.evidenceSubject !== gate.id) {
|
||||
failures.push(
|
||||
`gate ${gate.id}: evidence subject ${String(gate.evidenceSubject)} does not match gate id`,
|
||||
);
|
||||
}
|
||||
rejectDuplicateIds(gate.cases, `case in gate ${gate.id}`, failures);
|
||||
if (!Array.isArray(gate.invocation) || gate.invocation.length === 0) {
|
||||
failures.push(`${gate.id}: exact invocation is missing`);
|
||||
@@ -657,8 +702,8 @@ function validateClosedSchema(manifest, failures) {
|
||||
}
|
||||
}
|
||||
|
||||
function validateStructure(manifest, failures) {
|
||||
validateClosedSchema(manifest, failures);
|
||||
function validateStructure(manifest, failures, options = {}) {
|
||||
validateClosedSchema(manifest, failures, options);
|
||||
const criteria = new Map((manifest.criteria ?? []).map((criterion) => [criterion.id, criterion]));
|
||||
const boundCriteria = new Set();
|
||||
const negativeBoundCriteria = new Set();
|
||||
@@ -686,6 +731,22 @@ function validateStructure(manifest, failures) {
|
||||
}
|
||||
}
|
||||
|
||||
const registeredGateIds = new Set((manifest.gates ?? []).map((gate) => gate.id));
|
||||
for (const criterion of criteria.values()) {
|
||||
if (criterion.gateRefs === undefined) continue;
|
||||
const referenced = new Set(criterion.gateRefs);
|
||||
for (const gateId of registeredGateIds) {
|
||||
if (!referenced.has(gateId)) {
|
||||
failures.push(`${criterion.id}: gate population binding is missing ${gateId}`);
|
||||
}
|
||||
}
|
||||
for (const gateId of referenced) {
|
||||
if (!registeredGateIds.has(gateId)) {
|
||||
failures.push(`${criterion.id}: gate population binding references unknown gate ${gateId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const claim of manifest.proseClaims ?? []) {
|
||||
if (!criteria.has(claim.criterionId)) {
|
||||
failures.push(`GATE-CLAIM:${claim.id} references unknown criterion ${claim.criterionId}`);
|
||||
@@ -996,7 +1057,7 @@ export async function verifyRegistry(options) {
|
||||
const manifestPath = path.resolve(options.root, options.manifest);
|
||||
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
||||
|
||||
validateStructure(manifest, failures);
|
||||
validateStructure(manifest, failures, options);
|
||||
if (options.structureOnly) return { failures, manifest, observations };
|
||||
|
||||
async function collectPhaseFailure(label, action) {
|
||||
|
||||
Reference in New Issue
Block a user