fix(quality): anchor quantified registry populations

This commit is contained in:
2026-08-01 12:39:49 -05:00
parent 32b490a712
commit fbb6191298
14 changed files with 594 additions and 37 deletions
@@ -0,0 +1,56 @@
#!/usr/bin/env node
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import { verifyHistory } from './gate-history.mjs';
const root = await mkdtemp(path.join(os.tmpdir(), 'gate-delayed-introduction-'));
function git(...args) {
const result = spawnSync(
'git',
['-c', 'user.name=gate-control', '-c', '[email protected]', ...args],
{ cwd: root, encoding: 'utf8' },
);
if (result.status !== 0) throw new Error(result.stderr || result.stdout);
return result.stdout.trim();
}
try {
git('init', '-q');
await writeFile(path.join(root, 'baseline.txt'), 'baseline\n');
git('add', '.');
git('commit', '-m', 'provider target baseline');
const baseline = git('rev-parse', 'HEAD');
git('update-ref', 'refs/remotes/origin/main', baseline);
await mkdir(path.join(root, 'scripts'), { recursive: true });
await writeFile(path.join(root, 'scripts', 'preflight.mjs'), 'process.exit(0);\n');
git('add', '.');
git('commit', '-m', 'gate change before registry');
const unregisteredCommit = git('rev-parse', 'HEAD');
await mkdir(path.join(root, 'gates'), { recursive: true });
await writeFile(path.join(root, 'gates', 'gates.manifest.json'), '{"schemaVersion":1}\n');
git('add', '.');
git('commit', '-m', 'delayed registry introduction');
const previousBranch = process.env.CI_COMMIT_BRANCH;
process.env.CI_COMMIT_BRANCH = 'feature/delayed-introduction-control';
let result;
try {
result = await verifyHistory({ root, manifest: { schemaVersion: 1 } });
} finally {
if (previousBranch === undefined) delete process.env.CI_COMMIT_BRANCH;
else process.env.CI_COMMIT_BRANCH = previousBranch;
}
const detail = result.failures.join('\n');
if (detail.includes(unregisteredCommit) && /own-tree registry cannot be read/i.test(detail)) {
process.stderr.write(`delayed registry introduction rejected: ${detail}\n`);
process.exitCode = 1;
} else {
process.stdout.write('delayed registry introduction was not rejected\n');
}
} finally {
await rm(root, { recursive: true, force: true });
}
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env node
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { verifyRegistry } from './gate-verify.mjs';
const root = process.cwd();
const manifest = JSON.parse(await readFile(path.join(root, 'gates/gates.manifest.json'), 'utf8'));
manifest.gateRoots = ['.mosaic-empty-gate-root'];
manifest.criteria = [];
manifest.gates = [];
manifest.proseClaims = [];
manifest.compatibilityScenarios = [];
const directory = await mkdtemp(path.join(os.tmpdir(), 'gate-empty-population-'));
try {
const manifestPath = path.join(directory, 'manifest.json');
await writeFile(manifestPath, `${JSON.stringify(manifest)}\n`);
const result = await verifyRegistry({
root,
manifest: manifestPath,
skipHistory: true,
structureOnly: true,
fixtureProfile: false,
});
const required = ['criteria', 'gates', 'proseClaims', 'compatibilityScenarios'];
const missing = required.filter(
(population) =>
!result.failures.some((failure) =>
failure.includes(`${population} population must be non-empty and anchored`),
),
);
if (missing.length > 0) {
process.stdout.write(`empty populations were not rejected: ${missing.join(', ')}\n`);
process.exitCode = 0;
} else {
process.stderr.write(
'empty universally quantified registry populations rejected before evaluation\n',
);
process.exitCode = 1;
}
} finally {
await rm(directory, { recursive: true, force: true });
}
+1 -1
View File
@@ -35,6 +35,6 @@ if (candidate === boundary.activationCommit) {
process.exit(0);
}
process.stderr.write(
`history boundary candidate ${candidateKind} rejected: derived activation is parent of registry introduction\n`,
`history boundary candidate ${candidateKind} rejected: derived activation is provider target merge-base\n`,
);
process.exit(1);
+26 -4
View File
@@ -23,6 +23,11 @@ export async function listProspectiveCommits(root, activationCommit, head = 'HEA
}
export function deriveHistoryBoundary(root, head = 'HEAD') {
const targetRef = 'refs/remotes/origin/main';
const target = git(root, ['rev-parse', '--verify', targetRef], { allowFailure: true });
if (target.status !== 0 || !target.stdout.trim()) {
throw new Error(`history boundary cannot be derived: provider target ${targetRef} is absent`);
}
const introductions = git(root, [
'log',
'--first-parent',
@@ -40,13 +45,23 @@ export function deriveHistoryBoundary(root, head = 'HEAD') {
throw new Error('history boundary cannot be derived: registry introduction is absent');
}
const introductionCommit = introductions[0];
const parent = git(root, ['rev-parse', `${introductionCommit}^`], { allowFailure: true });
if (parent.status !== 0 || !parent.stdout.trim()) {
const headOnTarget = git(root, ['merge-base', '--is-ancestor', head, targetRef], {
allowFailure: true,
});
const activation =
headOnTarget.status === 0
? git(root, ['rev-parse', `${head}^`], { allowFailure: true })
: git(root, ['merge-base', head, targetRef], { allowFailure: true });
if (activation.status !== 0 || !activation.stdout.trim()) {
throw new Error(
`history boundary cannot be derived: registry introduction ${introductionCommit} has no parent`,
`history boundary cannot be derived: provider target merge-base for ${head} is unavailable`,
);
}
return { activationCommit: parent.stdout.trim(), introductionCommit };
return {
activationCommit: activation.stdout.trim(),
introductionCommit,
targetRef,
};
}
export async function readManifestAtCommit(root, commit) {
@@ -342,6 +357,13 @@ export async function verifyHistory({ root, manifest }) {
return { failures, observations };
}
const onMain = isMainCommit(root, head);
// RM-02 history bootstrap boundary (Builds 1-2), kept adjacent in both directions:
// DOES: anchor feature history to the provider target merge-base, sound against an author who
// cannot rewrite main.
// DOES NOT: establish integrity when main itself is compromised; Builds 1-2 own that residual.
observations.push(
`RM-02 HISTORY BOOTSTRAP BOUNDARY ${head}: DOES: anchor the audited range to provider target ${boundary.targetRef} at merge-base ${boundary.activationCommit}, sound against a branch author who cannot rewrite main; DOES NOT: protect against compromise or rewrite of main; residual owner Builds 1-2`,
);
// RM-02 execution boundary (RM-60, cross-reference RM-59), kept adjacent in both directions:
// DOES: run every registered current-tree gate and declared inerting mutation on PR CI,
// unprivileged and fail-closed.
+46
View File
@@ -289,6 +289,13 @@ test('PR verification states the RM-60 boundary without executing an intermediat
manifest: { schemaVersion: 1 },
});
assert.deepEqual(result.failures, []);
assert.ok(
result.observations.some((observation) =>
/HISTORY BOOTSTRAP BOUNDARY.*DOES:.*provider target.*sound.*cannot rewrite main.*DOES NOT:.*compromise.*main.*Builds 1-2/i.test(
observation,
),
),
);
assert.ok(
result.observations.some((observation) =>
/DOES:.*current tree.*DOES NOT:.*isolated.*RM-60.*RM-59/i.test(observation),
@@ -306,6 +313,42 @@ test('PR verification states the RM-60 boundary without executing an intermediat
}
});
test('target merge-base includes gate changes committed before registry introduction', async () => {
const root = `${fixtureRoot}-delayed-introduction`;
await rm(root, { recursive: true, force: true });
await mkdir(root, { recursive: true });
git(root, 'init', '-q');
git(root, 'config', 'user.name', 'gate-test');
git(root, 'config', 'user.email', '[email protected]');
await writeFile(path.join(root, 'baseline.txt'), 'baseline\n');
git(root, 'add', '.');
git(root, 'commit', '-m', 'target baseline');
const baseline = git(root, 'rev-parse', 'HEAD');
git(root, 'update-ref', 'refs/remotes/origin/main', baseline);
await mkdir(path.join(root, 'scripts'), { recursive: true });
await writeFile(path.join(root, 'scripts', 'preflight.mjs'), 'process.exit(0);\n');
git(root, 'add', '.');
git(root, 'commit', '-m', 'gate change before registry');
const preRegistryGateChange = git(root, 'rev-parse', 'HEAD');
await mkdir(path.join(root, 'gates'), { recursive: true });
await writeFile(path.join(root, 'gates', 'gates.manifest.json'), '{"schemaVersion":1}\n');
git(root, 'add', '.');
git(root, 'commit', '-m', 'delayed registry introduction');
const previousBranch = process.env.CI_COMMIT_BRANCH;
process.env.CI_COMMIT_BRANCH = 'feature/delayed-introduction';
try {
const result = await verifyHistory({ root, manifest: { schemaVersion: 1 } });
assert.match(
result.failures.join('\n'),
new RegExp(`${preRegistryGateChange}.*own-tree registry cannot be read`, 'i'),
);
} finally {
if (previousBranch === undefined) delete process.env.CI_COMMIT_BRANCH;
else process.env.CI_COMMIT_BRANCH = previousBranch;
}
});
test('derived history boundary includes the registry-introduction commit', async () => {
const root = `${fixtureRoot}-derived-boundary`;
await rm(root, { recursive: true, force: true });
@@ -317,6 +360,7 @@ test('derived history boundary includes the registry-introduction commit', async
git(root, 'add', '.');
git(root, 'commit', '-m', 'baseline');
const baseline = git(root, 'rev-parse', 'HEAD');
git(root, 'update-ref', 'refs/remotes/origin/main', baseline);
await mkdir(path.join(root, 'gates'), { recursive: true });
await writeFile(path.join(root, 'gates', 'gates.manifest.json'), '{"schemaVersion":1}\n');
git(root, 'add', '.');
@@ -330,6 +374,7 @@ test('derived history boundary includes the registry-introduction commit', async
assert.deepEqual(deriveHistoryBoundary(root, head), {
activationCommit: baseline,
introductionCommit: introduction,
targetRef: 'refs/remotes/origin/main',
});
assert.deepEqual(await listProspectiveCommits(root, baseline, head), [introduction, head]);
});
@@ -380,6 +425,7 @@ test('globally invalid provider evidence fails when HEAD is the only prospective
await writeFile(path.join(root, 'baseline.txt'), 'baseline\n');
git(root, 'add', '.');
git(root, 'commit', '-m', 'baseline');
git(root, 'update-ref', 'refs/remotes/origin/main', git(root, 'rev-parse', 'HEAD'));
await mkdir(path.join(root, 'gates'), { recursive: true });
await writeFile(path.join(root, 'gates', 'gates.manifest.json'), '{"schemaVersion":1}\n');
git(root, 'add', '.');
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env node
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { verifyRegistry } from './gate-verify.mjs';
const mode = process.argv[2];
const root = process.cwd();
const source = JSON.parse(await readFile(path.join(root, 'gates/gates.manifest.json'), 'utf8'));
const expectedGateIds = source.gates.map((gate) => gate.id);
if (expectedGateIds.length === 0) {
process.stderr.write('gate population control requires a non-empty anchored inventory\n');
process.exit(2);
}
async function rejectedForEveryGate(mutate, diagnostic) {
for (const gateId of expectedGateIds) {
const manifest = structuredClone(source);
const gate = manifest.gates.find((candidate) => candidate.id === gateId);
mutate(gate);
const directory = await mkdtemp(path.join(os.tmpdir(), 'gate-population-control-'));
const manifestPath = path.join(directory, 'manifest.json');
try {
await writeFile(manifestPath, `${JSON.stringify(manifest)}\n`);
const result = await verifyRegistry({
root,
manifest: manifestPath,
skipHistory: true,
structureOnly: true,
fixtureProfile: false,
});
if (!result.failures.some((failure) => diagnostic(failure, gateId))) return false;
} finally {
await rm(directory, { recursive: true, force: true });
}
}
return true;
}
let rejected;
if (mode === 'evidence-subject') {
rejected = await rejectedForEveryGate(
(gate) => {
gate.evidenceSubject = 'different-gate-subject';
},
(failure, gateId) =>
failure.includes(`gate ${gateId}: evidence subject`) &&
failure.includes('does not match gate id'),
);
} else if (mode === 'type-strict') {
rejected = await rejectedForEveryGate(
(gate) => {
gate.cases[0].actual.exitCode = '0';
},
(failure, gateId) =>
failure.includes(
`${gateId}/${source.gates.find((gate) => gate.id === gateId).cases[0].id}.actual.exitCode`,
) && failure.includes('expected an integer'),
);
} else {
process.stderr.write(`unknown gate population control ${String(mode)}\n`);
process.exit(2);
}
if (!rejected) {
process.stdout.write(`${mode} population control did not reject every registered gate\n`);
process.exit(0);
}
process.stderr.write(`${mode} population control rejected every registered gate\n`);
process.exit(1);
+65 -4
View File
@@ -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) {
+105
View File
@@ -6,6 +6,12 @@ import { spawnSync } from 'node:child_process';
import test from 'node:test';
const verifier = path.join(process.cwd(), 'scripts', 'gate-verify.mjs');
const fixtureRunner = path.join(
process.cwd(),
'scripts',
'test-support',
'gate-verify-fixture-runner.mjs',
);
const fixtureBase = path.join(process.cwd(), '.mosaic-test-work', `gate-verify-${process.pid}`);
async function fixture(name = 'case') {
@@ -38,6 +44,7 @@ function baseManifest() {
{
id: 'meta-fixture',
source: 'gates/meta-fixture.sh',
evidenceSubject: 'meta-fixture',
invocation: ['gates/meta-fixture.sh'],
deployment: { kind: 'none', reason: 'test fixture only' },
inertMutation: {
@@ -73,6 +80,14 @@ async function writeManifest(root, manifest) {
}
function verify(root, extraArgs = []) {
return spawnSync(
process.execPath,
[fixtureRunner, '--root', root, '--manifest', 'gates/gates.manifest.json', ...extraArgs],
{ cwd: root, encoding: 'utf8', env: { ...process.env, HOME: os.homedir() } },
);
}
function verifyProductionStructure(root, extraArgs = []) {
return spawnSync(
process.execPath,
[
@@ -82,6 +97,7 @@ function verify(root, extraArgs = []) {
'--manifest',
'gates/gates.manifest.json',
'--skip-history',
'--structure-only',
...extraArgs,
],
{ cwd: root, encoding: 'utf8', env: { ...process.env, HOME: os.homedir() } },
@@ -96,6 +112,95 @@ test.after(async () => {
await rm(fixtureBase, { recursive: true, force: true });
});
test('universally quantified registry checks reject empty populations before evaluation', async () => {
const root = await fixture('empty-registry-populations');
await mkdir(path.join(root, 'empty-gate-root'), { recursive: true });
const manifest = baseManifest();
manifest.gateRoots = ['empty-gate-root'];
manifest.criteria = [];
manifest.proseClaims = [];
manifest.compatibilityScenarios = [];
manifest.gates = [];
await writeManifest(root, manifest);
const result = verifyProductionStructure(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /criteria population.*non-empty.*anchored/i);
assert.match(output(result), /gates population.*non-empty.*anchored/i);
assert.match(output(result), /proseClaims population.*non-empty.*anchored/i);
assert.match(output(result), /compatibilityScenarios population.*non-empty.*anchored/i);
});
test('production verifier exposes no fixture-profile population bypass', async () => {
const root = await fixture('no-production-fixture-profile');
const result = verifyProductionStructure(root, ['--fixture-profile']);
assert.notEqual(result.status, 0);
assert.match(output(result), /unknown option: --fixture-profile/i);
});
test('anchored gate inventory and population criteria cannot shrink together', async () => {
const source = JSON.parse(
await readFile(path.join(process.cwd(), 'gates', 'gates.manifest.json'), 'utf8'),
);
const root = await fixture('shrunken-gate-population');
source.gates = source.gates.filter((gate) => gate.id !== 'hook-pre-push');
for (const criterion of source.criteria) {
if (criterion.gateRefs) {
criterion.gateRefs = criterion.gateRefs.filter((gateId) => gateId !== 'hook-pre-push');
}
criterion.caseRefs = criterion.caseRefs.filter(
(caseRef) => !caseRef.startsWith('hook-pre-push/'),
);
}
await writeManifest(root, source);
const result = verifyProductionStructure(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /gates population is not anchored.*hook-pre-push/i);
});
test('general population criteria cannot delete their gateRefs binding', async () => {
const requiredCriteria = [
'RM02-EVIDENCE-SUBJECT-BINDING',
'RM02-TYPE-STRICT-SCHEMA',
'RM02-NONEMPTY-ANCHORED-QUANTIFICATION',
];
for (const criterionId of requiredCriteria) {
const source = JSON.parse(
await readFile(path.join(process.cwd(), 'gates', 'gates.manifest.json'), 'utf8'),
);
const root = await fixture(`missing-gate-refs-${criterionId}`);
delete source.criteria.find((criterion) => criterion.id === criterionId).gateRefs;
await writeManifest(root, source);
const result = verifyProductionStructure(root);
assert.notEqual(result.status, 0);
assert.match(
output(result),
new RegExp(`${criterionId}.*gateRefs.*required`, 'i'),
criterionId,
);
}
});
test('general population criteria must span every registered gate', async () => {
const source = JSON.parse(
await readFile(path.join(process.cwd(), 'gates', 'gates.manifest.json'), 'utf8'),
);
const root = await fixture('incomplete-gate-refs');
source.criteria.find((criterion) => criterion.id === 'RM02-EVIDENCE-SUBJECT-BINDING').gateRefs =
source.criteria
.find((criterion) => criterion.id === 'RM02-EVIDENCE-SUBJECT-BINDING')
.gateRefs.filter((gateId) => gateId !== 'quality-lint');
await writeManifest(root, source);
const result = verifyProductionStructure(root);
assert.notEqual(result.status, 0);
assert.match(
output(result),
/RM02-EVIDENCE-SUBJECT-BINDING.*gate population binding is missing quality-lint/i,
);
});
test('an externally inerted failure branch makes verification nonzero and names the gate', async () => {
const root = await fixture('external-inert');
await writeGate(root, '#!/bin/sh\nexit 0\n');
@@ -0,0 +1,28 @@
#!/usr/bin/env node
import path from 'node:path';
import { verifyRegistry } from '../gate-verify.mjs';
let root;
let manifest = 'gates/gates.manifest.json';
let structureOnly = false;
for (let index = 0; index < process.argv.slice(2).length; index += 1) {
const args = process.argv.slice(2);
const value = args[index];
if (value === '--root') root = path.resolve(args[++index]);
else if (value === '--manifest') manifest = args[++index];
else if (value === '--structure-only') structureOnly = true;
else throw new Error(`unknown fixture-runner option: ${value}`);
}
if (!root) throw new Error('fixture runner requires --root');
const { failures, observations } = await verifyRegistry({
root,
manifest,
skipHistory: true,
structureOnly,
fixtureProfile: true,
});
for (const observation of observations) process.stdout.write(`${observation}\n`);
for (const failure of failures) process.stderr.write(`GATE VERIFY FAILED: ${failure}\n`);
if (failures.length > 0) process.exitCode = 1;