10 Commits
Author SHA1 Message Date
oat_gitadmin 0808286bf7 Merge pull request 'Gardes nommées, reprise de revue : « nommée » jamais « déclarée », exclusions, registre du refus' (#5) from instrument/gardes-nommees-reprise into master
gardes du contrat / conformite (push) Successful in 23s
Reviewed-on: #5
Reviewed-by: oat_gitadmin <admin@openathle.com>
2026-09-21 19:25:20 +00:00
temoin-agentandClaude Fable 5.1 654042d0fa Gardes nommées, reprise de revue : « nommée » jamais « déclarée », exclusions, registre du refus
gardes du contrat / conformite (pull_request) Successful in 17s
Les trois corrections retenues de la revue extérieure d'openathle#275 —
que j'avais amendées sur la branche d'une PR déjà fusionnée, donc jamais
livrées : les champs disent ce qu'ils mesurent (nommee_par_parcours,
_par_adr, _par_epreuve — un parcours peut porter la règle en prose sans
nommer la classe) ; une absence n'est pas une règle (gardes_exclues, par
motif — exclue = non observée) ; décider que non a un réceptacle
(gardes_ecartees, registre de la juridiction, lu jamais écrit). Le
journal porte ses motifs : la limite est dite là où la mesure s'écrit.
La triade est mesurée : sans_epreuve.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 21:13:02 +02:00
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
temoin-agentandClaude Opus 5 1954dcb704 SPEC v1.1 : une décroissance montrée nomme sa lecture
gardes du contrat / conformite (pull_request) Successful in 18s
Un auditeur qui voit une confiance décrue ne peut pas la reproduire s'il
ignore quelle fonction l'a produite, dans quelle version, à quelle date.
Deux lectures divergentes du même verbatim se valent alors, sans qu'on
puisse dire pourquoi : c'est l'opposabilité qui se fissure, non le fait.

Champ 15 confidence_reading (§4.1), OPTIONNEL — présent seulement quand
l'émetteur EXPOSE une valeur qu'il a dérivée ; ses quatre clés sont alors
requises (policy, version, value, as_of). Le verbatim reste intact au
champ 8. Règle de conformité au §9 : une valeur dérivée non déclarée
n'est pas conforme. Additif et ignorable — un consommateur v1.0 le
préserve et passe, comme §8 l'exige de tout champ inconnu.

Relevé par une lecture extérieure du 2026-09-19 : « c'est une ligne du
contrat, pas une ADR ». Elle avait raison sur les deux points.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 10:13:31 +02:00
oat_gitadmin 37077489de Merge pull request 'Autonomie : le dépôt public devient la source de vérité du contrat' (#1) from autonomie into master
gardes du contrat / conformite (push) Successful in 17s
Reviewed-on: #1
Reviewed-by: oat_gitadmin <admin@openathle.com>
2026-09-11 07:37:16 +00:00
temoin-agentandClaude Opus 5 1227a4fb5a Autonomie : le dépôt public devient la source de vérité du contrat
gardes du contrat / conformite (pull_request) Successful in 16s
Le développement se fait ici, par pull request endossée — le subtree
depuis le corpus hôte est mort (temoin/DEC-014, à consigner côté temoin).
Deux gardes en CI, normatives toutes deux : la suite de conformité
(SPEC §9) et l'auto-épreuve de l'instrument. Lockfile commité pour une
CI reproductible ; note de gouvernance au README.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 09:30:11 +02:00
20 changed files with 519 additions and 15 deletions
+28
View File
@@ -0,0 +1,28 @@
name: gardes du contrat
# Le dépôt public est la SOURCE DE VÉRITÉ du contrat depuis le 2026-09-11
# (temoin/DEC-014) : le développement se fait ici, par pull request endossée —
# le subtree depuis le corpus hôte est mort. Deux gardes, normatives toutes
# deux : la suite de conformité (SPEC §9 — qui la rate ne se conforme pas) et
# l'auto-épreuve de l'instrument (dix-huit vérifications sur une juridiction
# d'essai jetable).
on:
pull_request:
push:
branches: [master]
jobs:
conformite:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Dépendances (ajv pour le schéma, yaml pour l'instrument)
run: npm install --no-audit --no-fund
- name: Suite de conformité — normative
run: npm run conformance
- name: Auto-épreuve de l'instrument
run: npm run instrument:selftest
+1
View File
@@ -0,0 +1 @@
node_modules/
+15 -2
View File
@@ -12,5 +12,18 @@ A1.2). Deux artefacts, un seul numéro de version :
| [`grammar/`](grammar/) | Domicile réservé de la grammaire source (ADR-066 §2.2) | | [`grammar/`](grammar/) | Domicile réservé de la grammaire source (ADR-066 §2.2) |
| [`OPENING.md`](OPENING.md) | Déclencheur et butoir de l'extraction publique | | [`OPENING.md`](OPENING.md) | Déclencheur et butoir de l'extraction publique |
Le nom du paquet est celui qu'ADR-066 §2.2 lui a donné. `pnpm conformance` Le nom du paquet est celui qu'ADR-066 §2.2 lui a donné.
exécute le jeu d'épreuves.
## Gouvernance (depuis le 2026-09-11 — temoin/DEC-014)
Ce dépôt est la **source de vérité** du contrat. Le développement se fait
ici, par pull request endossée — une approbation humaine par fusion, aucune
poussée directe sur `master`. L'ancien rite (sources au corpus hôte,
re-publication par subtree) est mort : il coûtait trois rites par évolution.
Chaque consommateur **épingle** l'édition qu'il adopte (un commit de ce
dépôt) et mesure sa dérive contre la tête ; déplacer une épingle est une
pull request relue chez le consommateur, jamais un événement subi.
`npm run conformance` joue la suite normative ; `npm run instrument:selftest`
l'auto-épreuve de l'instrument. La CI exige les deux.
+47 -5
View File
@@ -1,4 +1,4 @@
# Assertion Envelope — Specification v1.0 # Assertion Envelope — Specification v1.2
**Status**: stable. Declared v1.0 upon the first real federated exchange **Status**: stable. Declared v1.0 upon the first real federated exchange
between two distinct jurisdictions — openathle → Prismagram, endorsed between two distinct jurisdictions — openathle → Prismagram, endorsed
@@ -84,12 +84,12 @@ fields (§7).
| 5 | `statement` | string | REQUIRED. What is asserted, in one sentence or few | | 5 | `statement` | string | REQUIRED. What is asserted, in one sentence or few |
| 6 | `evidence` | list of `{kind, ref}` | REQUIRED, MAY be empty. A conflict without evidence asks to be believed; evidence names where the fact lives | | 6 | `evidence` | list of `{kind, ref}` | REQUIRED, MAY be empty. A conflict without evidence asks to be believed; evidence names where the fact lives |
| 7 | `provenance` | string | REQUIRED. One of §6. Transported verbatim (§2.2) | | 7 | `provenance` | string | REQUIRED. One of §6. Transported verbatim (§2.2) |
| 8 | `confidence` | number | REQUIRED. `0.0`–`1.0`. Transported verbatim. Interpretation policies — decay, half-life, thresholds — are consumer-local readings and MUST NOT modify the transported value | | 8 | `confidence` | number | REQUIRED. `0.0`–`1.0`. Transported verbatim. Interpretation policies — decay, half-life, thresholds — are consumer-local readings and MUST NOT modify the transported value. A consumer that *exposes* such a reading declares it (field 15, §4.1) |
| 9 | `endorsement` | object or null | REQUIRED. `{by, at, rite}`; `rite` is one of `pr_approval`, `ui`, `batch`. `null` means: no human has endorsed this — which is a fact, not a defect | | 9 | `endorsement` | object or null | REQUIRED. `{by, at, rite}`; `rite` is one of `pr_approval`, `ui`, `batch`. `null` means: no human has endorsed this — which is a fact, not a defect |
| 10 | `jurisdiction` | string | REQUIRED. MUST equal the prefix of `id`. Lowercase ASCII, no separators | | 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 | | 11 | `at` | string | REQUIRED. RFC 3339 timestamp of the asserted state |
| 12 | `lifecycle` | string | REQUIRED. One of `proposed`, `endorsed`, `superseded`, `rejected`, `under_revision` | | 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 | | 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 | | 15 | `falsification` | string or null | REQUIRED. What observation would make this assertion false. A hypothesis without one is not a hypothesis |
@@ -97,6 +97,37 @@ fields (§7).
assertion; `endorsement` records the proven event. The two are irreducible: assertion; `endorsement` records the proven event. The two are irreducible:
a declared status is not an approval (ADR-070 §3.2). a declared status is not an approval (ADR-070 §3.2).
### 4.1 Declared readings (v1.1)
| # | Field | Type | Rule |
|---|---|---|---|
| 15 | `confidence_reading` | object | OPTIONAL. Present only when the sender exposes a value it has itself derived from `confidence`. Keys: `policy` (string, the reading's name), `version` (string), `value` (number `0.0`–`1.0`), `as_of` (RFC 3339 timestamp). All four REQUIRED when the object is present |
A decayed confidence is a reading, not a fact. Two auditors applying two decay
functions to the same verbatim value, on two dates, would disagree without
either being wrong — and neither could say why. This field makes the reading
quotable: the verbatim value stays in field 8, the reading stands beside it,
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 ## 5. Classes
`invariant`, `step_realized`, `transition`, `traversal`, `stressor`, `invariant`, `step_realized`, `transition`, `traversal`, `stressor`,
@@ -132,6 +163,13 @@ a standard field is a contract change under §8.
declared stable, nothing more. Consumers MUST treat `envelope: "0.1"` declared stable, nothing more. Consumers MUST treat `envelope: "0.1"`
messages as v1.0 messages; the schema accepts both values. This equivalence messages as v1.0 messages; the schema accepts both values. This equivalence
is specific to this pair and will not be repeated across future MAJORs. is specific to this pair and will not be repeated across future MAJORs.
- **v1.1 adds field 15 `confidence_reading`** (§4.1) and the consumer rule that
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 - At most **two published versions per year**. Deprecation is announced in
this file at least one version before removal. this file at least one version before removal.
- Consumers MUST accept any message whose `envelope` shares their MAJOR and - Consumers MUST accept any message whose `envelope` shares their MAJOR and
@@ -155,7 +193,10 @@ MUST NOT emit an endorsement it did not witness as a dated event; MUST NOT
emit `measured` provenance for anything a machine did not measure. emit `measured` provenance for anything a machine did not measure.
**Consumer** — MUST NOT mutate `id`, `provenance` or `confidence` of a **Consumer** — MUST NOT mutate `id`, `provenance` or `confidence` of a
received assertion; MUST NOT reject messages carrying unknown classes or received assertion; MUST, when it presents or relays a confidence value it has
derived from the transported one, carry the verbatim value unchanged **and**
declare the applied reading in `confidence_reading` (§4.1) — an undeclared
derived value is not conformant, because nobody can reproduce it; MUST NOT reject messages carrying unknown classes or
unknown fields (preserve, expose, move on); MUST record received assertions unknown fields (preserve, expose, move on); MUST record received assertions
append-only; MAY annotate, reference and aggregate received assertions under append-only; MAY annotate, reference and aggregate received assertions under
its own identity, and MUST NOT present the result as the origin's. its own identity, and MUST NOT present the result as the origin's.
@@ -163,6 +204,7 @@ its own identity, and MUST NOT present the result as the origin's.
## 10. What this contract does not do ## 10. What this contract does not do
It does not say how assertions are stored, projected, displayed or decayed — It does not say how assertions are stored, projected, displayed or decayed —
those are jurisdiction-local. It does not transport source documents: sealing those are jurisdiction-local. It says only that a decay, once shown, names
itself (§4.1): the contract governs what is claimed, never how it is computed. It does not transport source documents: sealing
and encrypting source payloads is storage, not exchange. It does not decide and encrypting source payloads is storage, not exchange. It does not decide
who is right: it keeps both parties quotable, at their own risk. who is right: it keeps both parties quotable, at their own risk.
@@ -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,21 @@
{
"reason": "A declared reading without its version is unreproducible: two auditors on two dates could not tell whether they read alike. All four keys are required when the object is present (SPEC §4.1).",
"message": {
"envelope": "1.1",
"id": "temoin/EVI-999",
"class": "measure",
"subject": "x",
"statement": "y",
"evidence": [],
"provenance": "measured",
"confidence": 1.0,
"endorsement": null,
"jurisdiction": "temoin",
"at": "2026-09-19T08:00:00Z",
"lifecycle": "proposed",
"relations": {},
"deadline": null,
"falsification": null,
"confidence_reading": { "policy": "half-life-by-class", "value": 0.87, "as_of": "2026-09-19T08:00:00Z" }
}
}
@@ -0,0 +1,23 @@
{
"envelope": "1.1",
"id": "temoin/EVI-129",
"class": "measure",
"subject": "INV-011",
"statement": "Detected mishoused on 8 September, reinforced on 10 — the loop closed.",
"evidence": [{ "kind": "coupe", "ref": "openathle/MES-INV-011-2" }],
"provenance": "measured",
"confidence": 1.0,
"endorsement": null,
"jurisdiction": "temoin",
"at": "2026-09-10T12:12:20.466Z",
"lifecycle": "proposed",
"relations": {},
"deadline": null,
"falsification": null,
"confidence_reading": {
"policy": "half-life-by-class",
"version": "1",
"value": 0.87,
"as_of": "2026-09-19T08:00:00Z"
}
}
@@ -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) if (prefix !== msg.jurisdiction)
errs.push(`jurisdiction "${msg.jurisdiction}" must equal the prefix of id ("${prefix}")`); 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; return errs;
} }
+44 -4
View File
@@ -1,8 +1,8 @@
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://prismagram.com/published-language/envelope/1.0/assertion.schema.json", "$id": "https://prismagram.com/published-language/envelope/1.0/assertion.schema.json",
"title": "Assertion Envelope v0.1", "title": "Assertion Envelope v1.2",
"description": "Producer contract for envelope v0.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`.", "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", "type": "object",
"required": [ "required": [
"envelope", "envelope",
@@ -25,9 +25,11 @@
"envelope": { "envelope": {
"enum": [ "enum": [
"0.1", "0.1",
"1.0" "1.0",
"1.1",
"1.2"
], ],
"description": "Version of the contract. v1.0 is substantively identical to v0.1 (SPEC §8): both values validate. 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": { "id": {
"type": "string", "type": "string",
@@ -153,6 +155,10 @@
}, },
"satellite_of": { "satellite_of": {
"$ref": "#/$defs/idList" "$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 "additionalProperties": false
@@ -178,6 +184,40 @@
"minLength": 1 "minLength": 1
} }
] ]
},
"confidence_reading": {
"type": "object",
"description": "OPTIONAL (SPEC §4.1, v1.1). Present only when the sender exposes a value it derived from `confidence`. A derived value shown without this object is not conformant: nobody could reproduce it.",
"required": [
"policy",
"version",
"value",
"as_of"
],
"additionalProperties": false,
"properties": {
"policy": {
"type": "string",
"minLength": 1,
"description": "Name of the reading applied — not the value it produced."
},
"version": {
"type": "string",
"minLength": 1,
"description": "Version of that reading. Two auditors on two dates must be able to tell whether they read alike."
},
"value": {
"type": "number",
"minimum": 0,
"maximum": 1,
"description": "The derived value. The verbatim one stays in `confidence`, untouched."
},
"as_of": {
"type": "string",
"format": "date-time",
"description": "When the reading was taken. A decay without a date is not reproducible."
}
}
} }
}, },
"patternProperties": { "patternProperties": {
+29
View File
@@ -46,6 +46,35 @@ 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 absent de la correspondance est un rang que la juridiction ne sait pas
instrumenter, pas un rang qui va de soi. 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 et un `throw` — et souvent
ni épreuve ni déclaration. Si le réglage porte `gardes_nommees` (motifs de
fichiers) et `adr` (où chercher les citations), le journal rend
`gardes_nommees: { motifs, total, non_nommees_par_un_parcours, sans_epreuve,
ecartees, differees, candidats }`. Trois précautions, chacune répondant à une
faute possible :
- **« Nommée », jamais « déclarée ».** Un parcours peut porter la règle en
langue naturelle sans nommer la classe ; l'instrument n'observe que la
nomination, et les champs le disent (`nommee_par_parcours`, `_par_adr`,
`_par_epreuve`). Absent ≠ non déclaré.
- **Une exception n'est pas un invariant.** Une absence (`NotFound`) n'est une
règle pour personne : la juridiction l'exclut par motif (`gardes_exclues`).
- **Décider que non a un réceptacle.** Un registre de la juridiction
(`gardes_ecartees` : `nom`, `decision: ecarte | differe`, `motif`, `jusqu_au`),
que l'instrument **lit et n'écrit jamais** : l'écarté sort des candidats et
reste dans le compte ; le différé revient à sa date.
L'instrument **mesure une existence, il ne déclare rien** ; rien ne devient un
constat, aucun état ne change de sens ; sans les clés, la section n'existe pas.
Sa limite est dans le journal (`motifs`) : ce qui ne porte pas le motif —
`require`, objets-valeurs, contraintes de schéma — lui est invisible.
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 ## Ce qu'il écrit — chez la juridiction, jamais ailleurs
- **`sortie`** : les constats, en **ajout seul**, et **seulement quand l'état - **`sortie`** : les constats, en **ajout seul**, et **seulement quand l'état
+86 -1
View File
@@ -177,6 +177,64 @@ function scanCitations(repo, files, ids) {
return hits; 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 exclus = (Array.isArray(cfg.gardes_exclues) ? cfg.gardes_exclues : []).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));
// Le réceptacle du refus : ce que la juridiction a DÉCIDÉ d'écarter ou de
// différer ne reparaît pas chaque nuit. Sans lui, la liste n'est jamais un
// delta et le bruit devient un stresseur (ADR-061 §2.3). Le registre est un
// fichier de la juridiction ; l'instrument le lit, il ne l'écrit jamais.
const decisions = new Map();
if (typeof cfg.gardes_ecartees === 'string' && existsFileAt(repo, cfg.gardes_ecartees)) {
const reg = parseYaml(read(cfg.gardes_ecartees));
for (const d of Array.isArray(reg) ? reg : []) if (d && typeof d.nom === 'string') decisions.set(d.nom, d);
}
const out = [];
for (const f of files) {
if (f.isTest || !res.some((re) => re.test(f.path)) || exclus.some((re) => re.test(f.path))) continue;
const nom = f.path.split('/').pop().replace(/\.[^.]+$/, '');
const re = new RegExp(`\\b${escapeRe(nom)}\\b`);
const d = decisions.get(nom);
const differee = d?.decision === 'differe' && typeof d.jusqu_au === 'string' && d.jusqu_au >= new Date().toISOString().slice(0, 10);
out.push({
nom,
chemin: f.path,
strate: f.stratum,
// NOMMÉE, jamais « déclarée » : un parcours peut porter la règle en
// langue naturelle sans nommer la classe. L'instrument observe la
// nomination — c'est sa limite, dite dans le nom des champs.
nommee_par_parcours: re.test(parcoursText),
nommee_par_adr: re.test(adrText),
nommee_par_epreuve: tests.some((x) => re.test(x)),
...(d?.decision === 'ecarte' ? { decision: 'ecarte', motif: d.motif ?? null } : {}),
...(differee ? { decision: 'differe', jusqu_au: d.jusqu_au } : {}),
});
}
return out.sort((a, b) => a.nom.localeCompare(b.nom));
}
function existsFileAt(repo, p) {
const st = statSync(join(repo, p), { throwIfNoEntry: false });
return st !== undefined && st.isFile();
}
function judge(hits, cfg) { function judge(hits, cfg) {
const order = cfg.strates.map((s) => s.nom); const order = cfg.strates.map((s) => s.nom);
const strata = [...new Set(hits.filter((h) => h.isTest).map((h) => h.stratum))] const strata = [...new Set(hits.filter((h) => h.isTest).map((h) => h.stratum))]
@@ -297,7 +355,8 @@ function main() {
if (last.get(inv.id) === key) continue; if (last.get(inv.id) === key) continue;
fresh.push(constatItem(cfg, inv, verdict, hits.get(inv.id), (counts.get(inv.id) ?? 0) + 1, meta)); 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) { function emit(args, cfg, r) {
@@ -313,6 +372,28 @@ function emit(args, cfg, r) {
: {}), : {}),
etats: r.tally, etats: r.tally,
constats_nouveaux: r.fresh.length, 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: {
// Ce que la mesure regarde, dit tel quel : les motifs. Ce qui n'y
// correspond pas (require, objets-valeurs, contraintes SQL) est
// invisible — la limite est dans le journal, pas seulement au README.
motifs: cfg.gardes_nommees,
...(Array.isArray(cfg.gardes_exclues) && cfg.gardes_exclues.length ? { exclus: cfg.gardes_exclues } : {}),
total: r.gardes.length,
non_nommees_par_un_parcours: r.gardes.filter((g) => !g.nommee_par_parcours).length,
sans_epreuve: r.gardes.filter((g) => !g.nommee_par_epreuve).length,
ecartees: r.gardes.filter((g) => g.decision === 'ecarte').length,
differees: r.gardes.filter((g) => g.decision === 'differe').length,
// Les candidats du jour : non nommés par un parcours, ni écartés, ni différés.
candidats: r.gardes.filter((g) => !g.nommee_par_parcours && g.decision === undefined),
},
}),
reglage_empreinte: r.meta.reglageHash, reglage_empreinte: r.meta.reglageHash,
}; };
if (args.dryRun) { if (args.dryRun) {
@@ -331,6 +412,10 @@ function emit(args, cfg, r) {
console.log(` ${r.invariants} invariant(s) déclaré(s) par ${r.journeys} parcours`); 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(` à 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}`}`); console.log(` constat(s) nouveau(x) : ${r.fresh.length}${args.dryRun ? '' : ` → ${cfg.sortie}`}`);
if (r.gardes !== null) {
const cand = r.gardes.filter((g) => !g.nommee_par_parcours && g.decision === undefined);
console.log(` gardes nommées : ${r.gardes.length} · candidates (non nommées par un parcours, ni écartées) : ${cand.length}${cand.length ? ' — ' + cand.map((g) => g.nom).join(', ') : ''}`);
}
} }
main(); main();
+31 -1
View File
@@ -10,7 +10,7 @@
* *
* Exit codes: 0 sound, 1 broken. A refusal names its cause. * Exit codes: 0 sound, 1 broken. A refusal names its cause.
*/ */
import { cpSync, mkdtempSync, readFileSync, rmSync, unlinkSync, existsSync } from 'node:fs'; import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, unlinkSync, existsSync, writeFileSync } from 'node:fs';
import { join, dirname } from 'node:path'; import { join, dirname } from 'node:path';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
@@ -47,6 +47,36 @@ 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-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(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'); 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 NAMES
// in a journey. Fields say « named », not « declared » : a journey may hold
// the rule in prose without naming the class — that is the instrument's limit.
const g = journal().gardes_nommees;
// Excluded by pattern = not observed at all: the NotFound one is neither counted nor listed.
assert(g && g.total === 1 && g.non_nommees_par_un_parcours === 1, 'the fixture has one observed named guard (the NotFound one is excluded), and no journey names it');
assert(JSON.stringify(g.motifs) === JSON.stringify(['src/main/java/**/Domain/**/*Exception.java']), 'the journal says which patterns it looked at — the lamppost is written down');
assert(g.candidats.length === 1 && g.candidats[0].nom === 'RuleViolatedException', 'the NotFound one is excluded by pattern; the other is a candidate');
const c = g.candidats[0];
assert(c.strate === 'domaine' && c.nommee_par_parcours === false && c.nommee_par_adr === true && c.nommee_par_epreuve === false, 'the candidate says who names it — ADR yes, journey no, test no');
assert(g.sans_epreuve === 1, 'the triad is measured, not asserted: the observed guard lacks a test');
assert(g.ecartees === 0 && g.differees === 0, 'no decision register yet: nothing set aside');
// 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');
// The refusal register: what the jurisdiction set aside does not come back
// every night. Read by the instrument, never written by it.
const repo3 = mkdtempSync(join(tmpdir(), 'instrument-selftest-registre-'));
cpSync(join(here, 'selftest/fixture'), repo3, { recursive: true });
mkdirSync(join(repo3, 'docs/parcours/_meta'), { recursive: true });
writeFileSync(join(repo3, 'docs/parcours/_meta/gardes-ecartees.yaml'), "- nom: RuleViolatedException\n decision: ecarte\n motif: une plomberie, pas une règle\n");
execFileSync(process.execPath, [join(here, 'run.mjs'), '--config', 'reglage.yaml', '--repo', repo3], { encoding: 'utf8', stdio: 'pipe' });
const j3 = parseYaml(readFileSync(join(repo3, 'docs/parcours/_mesures/derniere-execution.yaml'), 'utf8')).gardes_nommees;
assert(j3.total === 1 && j3.ecartees === 1 && j3.candidats.length === 0, 'a guard set aside leaves the candidates and stays in the count');
assert(byInv(first, 'INV-002')?.etat === 'mal_domicilie', 'INV-002 mal_domicilie — proven only away from home'); 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(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'); 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
+7
View File
@@ -16,3 +16,10 @@ strates:
epreuves: ["src/test/**", "app/src/**/*.essai.*"] epreuves: ["src/test/**", "app/src/**/*.essai.*"]
sortie: docs/parcours/_mesures/coupe.yaml sortie: docs/parcours/_mesures/coupe.yaml
journal_execution: docs/parcours/_mesures/derniere-execution.yaml journal_execution: docs/parcours/_mesures/derniere-execution.yaml
gardes_nommees:
- "src/main/java/**/Domain/**/*Exception.java"
gardes_exclues:
- "**/*NotFoundException.java"
gardes_ecartees: docs/parcours/_meta/gardes-ecartees.yaml
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); }
}
@@ -0,0 +1,4 @@
package x.Domain;
/** An absence is nobody's business rule: excluded by pattern. */
public class ThingNotFoundException extends RuntimeException {}
+111
View File
@@ -0,0 +1,111 @@
{
"name": "published-language",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "published-language",
"version": "0.1.0",
"license": "Apache-2.0",
"dependencies": {
"yaml": "2.9.0"
},
"devDependencies": {
"ajv": "^8.17.0",
"ajv-formats": "^3.0.0"
}
},
"node_modules/ajv": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-formats": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
"integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ajv": "^8.0.0"
},
"peerDependencies": {
"ajv": "^8.0.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz",
"integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "BSD-3-Clause"
},
"node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/yaml": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
"integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
},
"funding": {
"url": "https://github.com/sponsors/eemeli"
}
}
}
}
+2 -2
View File
@@ -1,9 +1,9 @@
{ {
"name": "published-language", "name": "published-language",
"version": "0.1.0", "version": "1.2.0",
"private": true, "private": true,
"type": "module", "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). Privé jusqu'à l'extraction publique décrite dans OPENING.md.", "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).",
"license": "Apache-2.0", "license": "Apache-2.0",
"scripts": { "scripts": {
"conformance": "node envelope/conformance/run.mjs", "conformance": "node envelope/conformance/run.mjs",