This commit is contained in:
@@ -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 });
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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', '.');
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user