Author SHA1 Message Date
oat_gitadmin 8670f57b16 Merge pull request 'Instrument : les gardes nommées, comme candidats — le plus petit lecteur qui prouve la thèse' (#4) from instrument/gardes-nommees into master
gardes du contrat / conformite (push) Successful in 18s
Reviewed-on: #4
Reviewed-by: oat_gitadmin <admin@openathle.com>
2026-09-21 13:33:37 +00:00
temoin-agentandClaude Fable 5.1 ecf13fe1da Instrument : les gardes nommées, comme candidats — le plus petit lecteur qui prouve la thèse
gardes du contrat / conformite (pull_request) Successful in 17s
Une exception de domaine est une règle qui a un nom, un throw, presque
toujours une épreuve, et souvent aucune déclaration. Si le réglage le
demande (gardes_nommees, adr), le journal rend les candidats qu'aucun
parcours ne cite, avec leur strate et ce qui les cite. Rien ne devient un
constat, aucun état ne change de sens, la section n'existe pas sans les
clés : additif, ignorable.

Ce qu'un dessin de trois lecteurs (Java annoté, SQL, réconciliation)
voulait démontrer, abattu par l'objecteur mandaté, ces quarante lignes
le démontrent : huit exceptions de domaine chez openathle, zéro déclarée.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 15:25:16 +02:00
oat_gitadmin 887d9a90c3 Merge pull request 'SPEC v1.2 : la mesure nomme ce qu'elle a examiné (relations.confirms)' (#3) from contrat/relation-confirms into master
gardes du contrat / conformite (push) Successful in 18s
Reviewed-on: #3
Reviewed-by: oat_gitadmin <admin@openathle.com>
2026-09-21 12:03:40 +00:00
temoin-agentandClaude Fable 5.1 f3d7383748 SPEC v1.2 : la mesure nomme ce qu'elle a examiné (relations.confirms)
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>
2026-09-21 13:53:04 +02:00
oat_gitadmin 52706a26a7 Merge pull request 'SPEC v1.1 : une décroissance montrée nomme sa lecture' (#2) from contrat/lecture-de-decroissance into master
gardes du contrat / conformite (push) Successful in 18s
Reviewed-on: #2
Reviewed-by: oat_gitadmin <admin@openathle.com>
2026-09-19 09:00:39 +00:00
13 changed files with 190 additions and 7 deletions
+21 -2
View File
@@ -1,4 +1,4 @@
# Assertion Envelope — Specification v1.1
# Assertion Envelope — Specification v1.2
**Status**: stable. Declared v1.0 upon the first real federated exchange
between two distinct jurisdictions — openathle → Prismagram, endorsed
@@ -89,7 +89,7 @@ fields (§7).
| 10 | `jurisdiction` | string | REQUIRED. MUST equal the prefix of `id`. Lowercase ASCII, no separators |
| 11 | `at` | string | REQUIRED. RFC 3339 timestamp of the asserted state |
| 12 | `lifecycle` | string | REQUIRED. One of `proposed`, `endorsed`, `superseded`, `rejected`, `under_revision` |
| 13 | `relations` | object | REQUIRED, MAY be empty. Keys among `amends`, `supersedes`, `depends_on`, `satellite_of`; values are lists of prefixed ids |
| 13 | `relations` | object | REQUIRED, MAY be empty. Keys among `amends`, `supersedes`, `depends_on`, `satellite_of`, `confirms` (v1.2); values are lists of prefixed ids |
| 14 | `deadline` | string or null | REQUIRED. RFC 3339. When a falsifiable statement expires into a verdict |
| 15 | `falsification` | string or null | REQUIRED. What observation would make this assertion false. A hypothesis without one is not a hypothesis |
@@ -112,6 +112,22 @@ named, versioned and dated.
It is OPTIONAL because a consumer that never exposes a derived value never owes
one; it is not optional for those who do (§9).
### 4.2 `confirms` — the measure names what it examined (v1.2)
A `measure` assertion MAY carry `relations.confirms`: the prefixed ids of the
assertions it examined. The direction is deliberate and it is the whole point:
**the measure declares what it confirms; the confirmed assertion is never
edited to point back.** Three reasons, each sufficient. The confirmed
assertion already exists and is append-only. The instrument already knows what
it measured — the link is inferred, never typed. And the author of a
confirmation is the machine that measured, not the agent whose claim is being
confirmed: a producer MUST NOT emit `confirms` on anything but a `measure` of
provenance `measured`.
A consumer MAY derive, from `confirms`, which assertions have been examined
and when — and MAY treat an assertion that no measure has ever confirmed
differently from one that has. That reading is consumer-local (§4.1).
## 5. Classes
`invariant`, `step_realized`, `transition`, `traversal`, `stressor`,
@@ -151,6 +167,9 @@ a standard field is a contract change under §8.
governs it (§9). Additive and ignorable: a v1.0 consumer that meets the field
preserves it and moves on, exactly as §8 requires of any unknown field. No
existing field, class, provenance or rule changed.
- **v1.2 adds the relation kind `confirms`** (§4.2), carried by measures only.
Additive and ignorable: a consumer that does not know the kind preserves it
as any unknown key of `relations` and moves on.
- At most **two published versions per year**. Deprecation is announced in
this file at least one version before removal.
- Consumers MUST accept any message whose `envelope` shares their MAJOR and
@@ -0,0 +1,20 @@
{
"reason": "confirms is the measure's word about what it examined; an agent claiming its own assertion is confirmed is self-attestation with a pointer (SPEC §4.2).",
"message": {
"envelope": "1.2",
"id": "temoin/EVI-999",
"class": "step_realized",
"subject": "x",
"statement": "y",
"evidence": [],
"provenance": "agent:claude-code",
"confidence": 0.4,
"endorsement": null,
"jurisdiction": "temoin",
"at": "2026-09-21T08:00:00Z",
"lifecycle": "proposed",
"relations": { "confirms": ["openathle/MES-INV-011-2"] },
"deadline": null,
"falsification": null
}
}
@@ -0,0 +1,17 @@
{
"envelope": "1.2",
"id": "openathle/MES-INV-011-2",
"class": "measure",
"subject": "INV-011",
"statement": "INV-011 : vérifié à son étage (domaine) et au-dessus — défense en profondeur.",
"evidence": [{ "kind": "epreuve:domaine", "ref": "quarkus-backoffice/src/test/java/org/openathle/LicenceConfig/Domain/ValueObject/RegistryClubRefTest.java:1" }],
"provenance": "measured",
"confidence": 1.0,
"endorsement": null,
"jurisdiction": "openathle",
"at": "2026-09-10T12:12:20.466Z",
"lifecycle": "proposed",
"relations": { "confirms": ["openathle/INV-011"] },
"deadline": null,
"falsification": null
}
+5
View File
@@ -34,6 +34,11 @@ function crossFieldErrors(msg) {
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;
}
+8 -3
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://prismagram.com/published-language/envelope/1.0/assertion.schema.json",
"title": "Assertion Envelope v1.1",
"title": "Assertion Envelope v1.2",
"description": "Producer contract for envelope v1.1 messages (SPEC.md §4). One cross-field rule cannot be expressed here and is enforced by the conformance runner: `jurisdiction` MUST equal the prefix of `id`.",
"type": "object",
"required": [
@@ -26,9 +26,10 @@
"enum": [
"0.1",
"1.0",
"1.1"
"1.1",
"1.2"
],
"description": "Version of the contract. v1.0 is substantively identical to v0.1 (SPEC §8): both values validate. v1.1 adds the optional declared reading (§4.1). A message that does not declare its version is not versioned (A1 §3)."
"description": "Version of the contract. v1.0 is substantively identical to v0.1 (SPEC §8): both values validate. v1.1 adds the optional declared reading (§4.1); v1.2 adds the relation kind `confirms` (§4.2). A message that does not declare its version is not versioned (A1 §3)."
},
"id": {
"type": "string",
@@ -154,6 +155,10 @@
},
"satellite_of": {
"$ref": "#/$defs/idList"
},
"confirms": {
"$ref": "#/$defs/idList",
"description": "v1.2, §4.2. Carried by a `measure` of provenance `measured` only: the ids it examined. The measure points at what it confirms; the confirmed assertion is never edited to point back."
}
},
"additionalProperties": false
+16
View File
@@ -46,6 +46,22 @@ jamais. La grossièreté se dit dans le réglage (en commentaire) : un rang
absent de la correspondance est un rang que la juridiction ne sait pas
instrumenter, pas un rang qui va de soi.
## Les gardes nommées — des candidats, jamais des constats (optionnel)
Une exception de domaine est une règle qui a un nom, un `throw`, presque
toujours une épreuve — et souvent aucune déclaration. Si le réglage porte
`gardes_nommees` (motifs de fichiers) et `adr` (où chercher les citations),
le journal d'exécution rend `gardes_nommees: { total, sans_declaration,
candidats }` — seuls ceux qu'aucun parcours ne cite sont listés, avec leur
strate et ce qui les cite (ADR, épreuve). L'instrument **mesure une existence,
il ne déclare rien** : déclarer un candidat (un invariant dans le parcours qui
le porte) ou décider que non reste à la juridiction. Rien ne devient un
constat, aucun état ne change de sens ; sans les clés, la section n'existe pas.
Pourquoi si peu : c'est le plus petit instrument qui prouve que le code tient
des règles que nul n'a déclarées. Mesuré chez openathle le jour de son
écriture — huit exceptions de domaine, zéro citée par un parcours.
## Ce qu'il écrit — chez la juridiction, jamais ailleurs
- **`sortie`** : les constats, en **ajout seul**, et **seulement quand l'état
+55 -1
View File
@@ -177,6 +177,42 @@ function scanCitations(repo, files, ids) {
return hits;
}
/**
* Les GARDES NOMMÉES — le plus petit instrument qui prouve que le code tient
* des règles que nul n'a déclarées. Une exception de domaine a un nom, un
* `throw`, presque toujours une épreuve — et souvent aucune déclaration.
* L'instrument la rend comme CANDIDAT, jamais comme constat : il mesure une
* existence, il ne déclare rien, il ne renomme aucun état. Déclarer ou non
* reste à la juridiction (P4 : l'humain écrit ce que la règle signifie).
* Absente du réglage (`gardes_nommees`), la section n'existe pas — rien ne
* change pour qui ne l'a pas demandée.
*/
function namedGuards(repo, cfg, files) {
if (!Array.isArray(cfg.gardes_nommees) || cfg.gardes_nommees.length === 0) return null;
const res = cfg.gardes_nommees.map(globToRegExp);
const read = (p) => readFileSync(join(repo, p), 'utf8');
const mdUnder = (dir) =>
typeof dir === 'string' && existsSync(join(repo, dir)) ? walk(repo, join(repo, dir), []).filter((p) => p.endsWith('.md')) : [];
const parcoursText = mdUnder(cfg.parcours).map(read).join('\n');
const adrText = mdUnder(cfg.adr).map(read).join('\n');
const tests = files.filter((f) => f.isTest).map((f) => read(f.path));
const out = [];
for (const f of files) {
if (f.isTest || !res.some((re) => re.test(f.path))) continue;
const nom = f.path.split('/').pop().replace(/\.[^.]+$/, '');
const re = new RegExp(`\\b${escapeRe(nom)}\\b`);
out.push({
nom,
chemin: f.path,
strate: f.stratum,
dans_parcours: re.test(parcoursText),
dans_adr: re.test(adrText),
eprouve: tests.some((x) => re.test(x)),
});
}
return out.sort((a, b) => a.nom.localeCompare(b.nom));
}
function judge(hits, cfg) {
const order = cfg.strates.map((s) => s.nom);
const strata = [...new Set(hits.filter((h) => h.isTest).map((h) => h.stratum))]
@@ -297,7 +333,8 @@ function main() {
if (last.get(inv.id) === key) continue;
fresh.push(constatItem(cfg, inv, verdict, hits.get(inv.id), (counts.get(inv.id) ?? 0) + 1, meta));
}
emit(args, cfg, { journeys, invariants: invariants.size, tally, fresh, meta, sortiePath });
const gardes = namedGuards(args.repo, cfg, files);
emit(args, cfg, { journeys, invariants: invariants.size, tally, fresh, meta, sortiePath, gardes });
}
function emit(args, cfg, r) {
@@ -313,6 +350,19 @@ function emit(args, cfg, r) {
: {}),
etats: r.tally,
constats_nouveaux: r.fresh.length,
// Les candidats vont au journal, réécrit à chaque course : ce n'est pas un
// constat (rien n'est mesuré sur un invariant), c'est un inventaire du jour.
// Seuls ceux qu'aucun parcours ne cite sont listés — lisibilité (P9) ; le
// total dit combien de gardes nommées le code porte.
...(r.gardes === null
? {}
: {
gardes_nommees: {
total: r.gardes.length,
sans_declaration: r.gardes.filter((g) => !g.dans_parcours).length,
candidats: r.gardes.filter((g) => !g.dans_parcours),
},
}),
reglage_empreinte: r.meta.reglageHash,
};
if (args.dryRun) {
@@ -331,6 +381,10 @@ function emit(args, cfg, r) {
console.log(` ${r.invariants} invariant(s) déclaré(s) par ${r.journeys} parcours`);
console.log(` à son étage : ${r.tally.domicilie} · renforcé : ${r.tally.renforce} · hors de son étage : ${r.tally.mal_domicilie} · jamais éprouvé : ${r.tally.aucun_locus}`);
console.log(` constat(s) nouveau(x) : ${r.fresh.length}${args.dryRun ? '' : ` → ${cfg.sortie}`}`);
if (r.gardes !== null) {
const sans = r.gardes.filter((g) => !g.dans_parcours);
console.log(` gardes nommées : ${r.gardes.length}, dont ${sans.length} qu'aucun parcours ne déclare${sans.length ? ' — ' + sans.map((g) => g.nom).join(', ') : ''}`);
}
}
main();
+15
View File
@@ -47,6 +47,21 @@ try {
assert((byInv(first, 'INV-001')?.rangs_constates ?? []).join(',') === '4,3', 'INV-001 carries the third-party ranks its tuning declares, in stratum order');
assert(byInv(first, 'INV-003')?.rangs_constates === undefined, 'INV-003 (no locus) carries no ranks — nothing observed maps to nothing');
assert(JSON.stringify(journal().correspondance_rangs) === JSON.stringify({ domaine: 4, application: 3, interface: 1 }), 'journal publishes the full rank correspondence for consumers joining old findings');
// Named guards — candidates, never findings: a domain exception nobody declared.
const g = journal().gardes_nommees;
assert(g && g.total === 1 && g.sans_declaration === 1, 'the fixture has one named domain guard, and no journey declares it');
assert(g.candidats[0].nom === 'RuleViolatedException' && g.candidats[0].strate === 'domaine', 'the candidate carries its name and its stratum');
assert(g.candidats[0].dans_parcours === false && g.candidats[0].dans_adr === true && g.candidats[0].eprouve === false, 'the candidate says where it is cited — ADR yes, journey no, test no');
assert(!('candidats' in journal()) , 'candidates live under gardes_nommees, not at the journal root');
// Backward compatibility: a tuning without the key gets no section at all.
// On its OWN copy of the fixture: a second run on `repo` would rewrite the
// journal and falsify the assertions that follow (caught by the self-test
// itself on 2026-09-21 — a guard that shares state with what it guards).
const repo2 = mkdtempSync(join(tmpdir(), 'instrument-selftest-compat-'));
cpSync(join(here, 'selftest/fixture'), repo2, { recursive: true });
execFileSync(process.execPath, [join(here, 'run.mjs'), '--config', 'reglage-sans-gardes.yaml', '--repo', repo2], { encoding: 'utf8', stdio: 'pipe' });
const journal2 = parseYaml(readFileSync(join(repo2, 'docs/parcours/_mesures/derniere-execution.yaml'), 'utf8'));
assert(!('gardes_nommees' in journal2), 'a tuning that does not ask for named guards changes nothing');
assert(byInv(first, 'INV-002')?.etat === 'mal_domicilie', 'INV-002 mal_domicilie — proven only away from home');
assert(byInv(first, 'INV-003')?.etat === 'aucun_locus', 'INV-003 aucun_locus — named by no test');
assert(journal().constats_nouveaux === 3 && journal().invariants === 3, 'journal counts the run');
@@ -0,0 +1,3 @@
# ADR-001 — essai
Le domaine lève `RuleViolatedException` quand la règle est violée.
@@ -0,0 +1,19 @@
# Réglage de la juridiction d'essai — la forme que toute juridiction fournit.
juridiction: essai
cadence_heures: 24
parcours: docs/parcours
strates:
- nom: domaine
due: true
rang: 4
chemins: ["src/main/java/**/Domain/**", "src/test/java/**/Domain/**"]
- nom: application
rang: 3
chemins: ["src/main/java/**/Application/**", "src/test/java/**/Application/**"]
- nom: interface
rang: 1
chemins: ["app/src/**"]
epreuves: ["src/test/**", "app/src/**/*.essai.*"]
sortie: docs/parcours/_mesures/coupe.yaml
journal_execution: docs/parcours/_mesures/derniere-execution.yaml
+4
View File
@@ -16,3 +16,7 @@ strates:
epreuves: ["src/test/**", "app/src/**/*.essai.*"]
sortie: docs/parcours/_mesures/coupe.yaml
journal_execution: docs/parcours/_mesures/derniere-execution.yaml
gardes_nommees:
- "src/main/java/**/Domain/**/*Exception.java"
adr: docs/adr
@@ -0,0 +1,6 @@
package x.Domain;
/** A named domain guard: a rule with a name and a throw — declared nowhere. */
public class RuleViolatedException extends RuntimeException {
public RuleViolatedException(String why) { super(why); }
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "published-language",
"version": "1.1.0",
"version": "1.2.0",
"private": true,
"type": "module",
"description": "Contrat d'échange inter-juridictions : grammaire source (grammar/) et enveloppe d'assertion (envelope/), sous un seul numéro de version (ADR-061/A1.2 §2.6, décision 6). Public, développé ici par pull request endossée (temoin/DEC-014).",