Compare commits
10
Commits
7f03f37ef7
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0808286bf7 | ||
|
|
654042d0fa | ||
|
|
8670f57b16 | ||
|
|
ecf13fe1da | ||
|
|
887d9a90c3 | ||
|
|
f3d7383748 | ||
|
|
52706a26a7 | ||
|
|
1954dcb704 | ||
|
|
37077489de | ||
|
|
1227a4fb5a |
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
node_modules/
|
||||
@@ -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) |
|
||||
| [`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`
|
||||
exécute le jeu d'épreuves.
|
||||
Le nom du paquet est celui qu'ADR-066 §2.2 lui a donné.
|
||||
|
||||
## 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
@@ -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
|
||||
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 |
|
||||
| 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) |
|
||||
| 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 |
|
||||
| 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 |
|
||||
|
||||
@@ -97,6 +97,37 @@ fields (§7).
|
||||
assertion; `endorsement` records the proven event. The two are irreducible:
|
||||
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
|
||||
|
||||
`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"`
|
||||
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.
|
||||
- **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
|
||||
this file at least one version before removal.
|
||||
- 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.
|
||||
|
||||
**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
|
||||
append-only; MAY annotate, reference and aggregate received assertions under
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://prismagram.com/published-language/envelope/1.0/assertion.schema.json",
|
||||
"title": "Assertion Envelope v0.1",
|
||||
"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`.",
|
||||
"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": [
|
||||
"envelope",
|
||||
@@ -25,9 +25,11 @@
|
||||
"envelope": {
|
||||
"enum": [
|
||||
"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": {
|
||||
"type": "string",
|
||||
@@ -153,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
|
||||
@@ -178,6 +184,40 @@
|
||||
"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": {
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
- **`sortie`** : les constats, en **ajout seul**, et **seulement quand l'état
|
||||
|
||||
+86
-1
@@ -177,6 +177,64 @@ 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 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) {
|
||||
const order = cfg.strates.map((s) => s.nom);
|
||||
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;
|
||||
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 +372,28 @@ 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: {
|
||||
// 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,
|
||||
};
|
||||
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(` à 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 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();
|
||||
|
||||
+31
-1
@@ -10,7 +10,7 @@
|
||||
*
|
||||
* 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 { tmpdir } from 'node:os';
|
||||
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-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 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-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
|
||||
|
||||
@@ -16,3 +16,10 @@ 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"
|
||||
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 {}
|
||||
Generated
+111
@@ -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
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "published-language",
|
||||
"version": "0.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). 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",
|
||||
"scripts": {
|
||||
"conformance": "node envelope/conformance/run.mjs",
|
||||
|
||||
Reference in New Issue
Block a user