Files
temoin-agentandClaude Fable 5.1 f3d7383748
gardes du contrat / conformite (pull_request) Successful in 28s
SPEC v1.2 : la mesure nomme ce qu'elle a examiné (relations.confirms)
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>
2026-09-21 13:53:04 +02:00

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.');