gardes du contrat / conformite (pull_request) Successful in 28s
Le sens est la décision entière : c'est la MESURE qui déclare ce qu'elle confirme, jamais l'assertion confirmée qui pointe en retour. Trois raisons suffisantes chacune : l'assertion confirmée existe déjà et ne se réécrit pas ; l'instrument sait ce qu'il a mesuré — le lien est inféré, jamais saisi ; et l'auteur d'une confirmation est la machine, non l'agent dont l'affirmation est confirmée. Le runner refuse confirms sur autre chose qu'une mesure de provenance measured. Mineur et ignorable : un consommateur qui ignore la clé la préserve. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
88 lines
3.7 KiB
JavaScript
88 lines
3.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Conformance runner — Assertion Envelope v0.1 (SPEC.md §9).
|
|
*
|
|
* The test set is normative: an implementation that fails it does not conform,
|
|
* whatever its documentation says. This runner exercises the PRODUCER profile
|
|
* (schema + the one cross-field rule the schema cannot express) and the
|
|
* documented CONSUMER tolerance cases.
|
|
*
|
|
* Layout:
|
|
* producer/valid/*.json raw messages — MUST validate
|
|
* producer/invalid/*.json {reason, message} — message MUST be rejected
|
|
* consumer/*.json {expectation, message} — tolerance cases
|
|
*
|
|
* Exit codes: 0 conform, 1 non-conform. A refusal names its cause.
|
|
*/
|
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import Ajv2020 from 'ajv/dist/2020.js';
|
|
import addFormats from 'ajv-formats';
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const schema = JSON.parse(readFileSync(join(here, '../schema/assertion.schema.json'), 'utf8'));
|
|
const ajv = new Ajv2020.default({ allErrors: true, strict: true });
|
|
addFormats.default(ajv);
|
|
const validate = ajv.compile(schema);
|
|
|
|
/** The rule JSON Schema cannot express (SPEC.md §4, field 10). */
|
|
function crossFieldErrors(msg) {
|
|
const errs = [];
|
|
if (typeof msg?.id === 'string' && typeof msg?.jurisdiction === 'string') {
|
|
const prefix = msg.id.split('/')[0];
|
|
if (prefix !== msg.jurisdiction)
|
|
errs.push(`jurisdiction "${msg.jurisdiction}" must equal the prefix of id ("${prefix}")`);
|
|
}
|
|
// §4.2 (v1.2) : la mesure nomme ce qu'elle a examiné — et elle seule.
|
|
if (Array.isArray(msg?.relations?.confirms) && msg.relations.confirms.length > 0) {
|
|
if (msg.class !== 'measure' || msg.provenance !== 'measured')
|
|
errs.push(`relations.confirms is carried by a measure of provenance measured only (got class "${msg.class}", provenance "${msg.provenance}")`);
|
|
}
|
|
return errs;
|
|
}
|
|
|
|
const lire = (d) => {
|
|
try { return readdirSync(join(here, d)).filter((f) => f.endsWith('.json')).sort(); }
|
|
catch { return []; }
|
|
};
|
|
|
|
let failures = 0;
|
|
const fail = (m) => { failures += 1; console.error(` ✗ ${m}`); };
|
|
const ok = (m) => console.log(` ✓ ${m}`);
|
|
|
|
console.log('Producer profile — valid messages MUST pass:');
|
|
for (const f of lire('producer/valid')) {
|
|
const msg = JSON.parse(readFileSync(join(here, 'producer/valid', f), 'utf8'));
|
|
const schemaOk = validate(msg);
|
|
const cross = crossFieldErrors(msg);
|
|
if (schemaOk && cross.length === 0) ok(f);
|
|
else fail(`${f} rejected: ${schemaOk ? cross.join('; ') : ajv.errorsText(validate.errors)}`);
|
|
}
|
|
|
|
console.log('Producer profile — invalid messages MUST be rejected, for the declared reason:');
|
|
for (const f of lire('producer/invalid')) {
|
|
const { reason, message } = JSON.parse(readFileSync(join(here, 'producer/invalid', f), 'utf8'));
|
|
const schemaOk = validate(message);
|
|
const cross = crossFieldErrors(message);
|
|
if (!schemaOk || cross.length > 0) ok(`${f} (${reason})`);
|
|
else fail(`${f} ACCEPTED although: ${reason}`);
|
|
}
|
|
|
|
console.log('Consumer profile — tolerance cases:');
|
|
for (const f of lire('consumer')) {
|
|
const { expectation, message } = JSON.parse(readFileSync(join(here, 'consumer', f), 'utf8'));
|
|
// v0.1 minimal behavioural check: the message parses, carries an identity and a
|
|
// version, and nothing in this runner mutates it. Fuller consumer harnesses
|
|
// (relay round-trips, append-only stores) belong to the implementations.
|
|
if (typeof message?.envelope === 'string' && typeof message?.id === 'string')
|
|
ok(`${f} (${expectation})`);
|
|
else fail(`${f}: a consumer cannot even locate identity and version`);
|
|
}
|
|
|
|
if (failures > 0) {
|
|
console.error(`\nNON-CONFORM: ${failures} failure(s).`);
|
|
process.exit(1);
|
|
}
|
|
console.log('\nConform: every fixture behaved as declared.');
|