disensor 0.9.2__tar.gz → 0.9.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. {disensor-0.9.2/src/disensor.egg-info → disensor-0.9.4}/PKG-INFO +70 -19
  2. {disensor-0.9.2 → disensor-0.9.4}/README.md +69 -18
  3. {disensor-0.9.2 → disensor-0.9.4}/pyproject.toml +1 -1
  4. {disensor-0.9.2 → disensor-0.9.4}/src/disensor/GUIDE.es.md +8 -5
  5. {disensor-0.9.2 → disensor-0.9.4}/src/disensor/GUIDE.md +7 -5
  6. {disensor-0.9.2 → disensor-0.9.4}/src/disensor/__init__.py +1 -1
  7. {disensor-0.9.2 → disensor-0.9.4}/src/disensor/gate.py +79 -0
  8. disensor-0.9.4/src/disensor/residue.schema.v0.4.json +958 -0
  9. {disensor-0.9.2 → disensor-0.9.4}/src/disensor/reviewers.py +21 -4
  10. {disensor-0.9.2 → disensor-0.9.4}/src/disensor/round.py +60 -5
  11. {disensor-0.9.2 → disensor-0.9.4}/src/disensor/rules.py +128 -5
  12. {disensor-0.9.2 → disensor-0.9.4}/src/disensor/template.py +16 -1
  13. {disensor-0.9.2 → disensor-0.9.4}/src/disensor/vectors.py +159 -6
  14. {disensor-0.9.2 → disensor-0.9.4/src/disensor.egg-info}/PKG-INFO +70 -19
  15. {disensor-0.9.2 → disensor-0.9.4}/src/disensor.egg-info/SOURCES.txt +1 -0
  16. {disensor-0.9.2 → disensor-0.9.4}/tests/test_docs.py +22 -0
  17. {disensor-0.9.2 → disensor-0.9.4}/tests/test_gate.py +91 -0
  18. {disensor-0.9.2 → disensor-0.9.4}/tests/test_new_round.py +33 -0
  19. {disensor-0.9.2 → disensor-0.9.4}/tests/test_requirement.py +20 -0
  20. {disensor-0.9.2 → disensor-0.9.4}/tests/test_reviewers.py +43 -1
  21. {disensor-0.9.2 → disensor-0.9.4}/tests/test_round.py +49 -0
  22. disensor-0.9.4/tests/test_v04.py +673 -0
  23. disensor-0.9.4/tests/test_vectors.py +115 -0
  24. disensor-0.9.2/tests/test_v04.py +0 -211
  25. disensor-0.9.2/tests/test_vectors.py +0 -41
  26. {disensor-0.9.2 → disensor-0.9.4}/LICENSE +0 -0
  27. {disensor-0.9.2 → disensor-0.9.4}/setup.cfg +0 -0
  28. {disensor-0.9.2 → disensor-0.9.4}/src/disensor/__main__.py +0 -0
  29. {disensor-0.9.2 → disensor-0.9.4}/src/disensor/brief.py +0 -0
  30. {disensor-0.9.2 → disensor-0.9.4}/src/disensor/cli.py +0 -0
  31. {disensor-0.9.2 → disensor-0.9.4}/src/disensor/gitctx.py +0 -0
  32. {disensor-0.9.2 → disensor-0.9.4}/src/disensor/guide.py +0 -0
  33. {disensor-0.9.2 → disensor-0.9.4}/src/disensor/init.py +0 -0
  34. {disensor-0.9.2 → disensor-0.9.4}/src/disensor/pack.py +0 -0
  35. {disensor-0.9.2 → disensor-0.9.4}/src/disensor/pin.py +0 -0
  36. {disensor-0.9.2 → disensor-0.9.4}/src/disensor/prompts/_common.md +0 -0
  37. {disensor-0.9.2 → disensor-0.9.4}/src/disensor/prompts/architecture.md +0 -0
  38. {disensor-0.9.2 → disensor-0.9.4}/src/disensor/prompts/diff.md +0 -0
  39. {disensor-0.9.2 → disensor-0.9.4}/src/disensor/prompts/plan.md +0 -0
  40. {disensor-0.9.2 → disensor-0.9.4}/src/disensor/render.py +0 -0
  41. {disensor-0.9.2 → disensor-0.9.4}/src/disensor/residue.schema.json +0 -0
  42. {disensor-0.9.2 → disensor-0.9.4}/src/disensor/residue.schema.v0.2.json +0 -0
  43. {disensor-0.9.2 → disensor-0.9.4}/src/disensor/residue.schema.v0.3.json +0 -0
  44. {disensor-0.9.2 → disensor-0.9.4}/src/disensor/scope.py +0 -0
  45. {disensor-0.9.2 → disensor-0.9.4}/src/disensor.egg-info/dependency_links.txt +0 -0
  46. {disensor-0.9.2 → disensor-0.9.4}/src/disensor.egg-info/entry_points.txt +0 -0
  47. {disensor-0.9.2 → disensor-0.9.4}/src/disensor.egg-info/requires.txt +0 -0
  48. {disensor-0.9.2 → disensor-0.9.4}/src/disensor.egg-info/top_level.txt +0 -0
  49. {disensor-0.9.2 → disensor-0.9.4}/tests/test_brief.py +0 -0
  50. {disensor-0.9.2 → disensor-0.9.4}/tests/test_cli_errors.py +0 -0
  51. {disensor-0.9.2 → disensor-0.9.4}/tests/test_docs_sync.py +0 -0
  52. {disensor-0.9.2 → disensor-0.9.4}/tests/test_guide.py +0 -0
  53. {disensor-0.9.2 → disensor-0.9.4}/tests/test_hardening.py +0 -0
  54. {disensor-0.9.2 → disensor-0.9.4}/tests/test_init.py +0 -0
  55. {disensor-0.9.2 → disensor-0.9.4}/tests/test_pack.py +0 -0
  56. {disensor-0.9.2 → disensor-0.9.4}/tests/test_pin.py +0 -0
  57. {disensor-0.9.2 → disensor-0.9.4}/tests/test_rules.py +0 -0
  58. {disensor-0.9.2 → disensor-0.9.4}/tests/test_scope.py +0 -0
  59. {disensor-0.9.2 → disensor-0.9.4}/tests/test_upgrade.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: disensor
3
- Version: 0.9.2
3
+ Version: 0.9.4
4
4
  Summary: Adversarial plan & code review with a declared residue. Emits, validates and CI-enforces residue declarations (residue/v0.4 schema).
5
5
  Author-email: Nicolas Rocchia <nicolasrocchia@gmail.com>
6
6
  License: MIT
@@ -56,7 +56,7 @@ paper is in Spanish; the glossary at the end maps its terminology to the schema.
56
56
 
57
57
  - `spec/residue.schema.json`: the artifact schema (JSON Schema 2020-12), version residue/v0.4. Superseded versions keep their own frozen resource next to it.
58
58
  - `spec/examples/`: three example artifacts, including a real anonymised event and the minimized profile with no free text.
59
- - `src/disensor/`: Python package with the validator (rules R0 to R10), the CI gate (checks G1 to G9), the PR comment rendering, artifact and repository scaffolding (`init`), and the packaged filling guide (`GUIDE.md`).
59
+ - `src/disensor/`: Python package with the validator (rules R0 to R13), the CI gate (checks G1 to G9), the PR comment rendering, artifact and repository scaffolding (`init`), and the packaged filling guide (`GUIDE.md`).
60
60
  - `action.yml`: composite GitHub Action, ready to use.
61
61
  - `docs/integracion-claude-code.md` (Spanish only): how the real flow (Claude Code plus a reviewer from another family) emits the artifact at the close of each event.
62
62
  - `docs/antecedentes.md` (Spanish only): where the method sits relative to the literature (residual doubt and defeaters, design rationale and its capture bottleneck, multi-agent adversarial review, governance runtimes, supply chain provenance), with the verification status of each reference.
@@ -78,7 +78,7 @@ disensor new --gate diff --level B --round ../result.json # declaration from t
78
78
  disensor prompt --gate diff # the adversarial brief, to hand to a reviewer from another family
79
79
  disensor pack --gate diff --base main --head HEAD # the full package, if you drive the round yourself
80
80
  disensor new --gate diff --level B # template prefilled in .residue/
81
- disensor validate .residue/<id>.json # schema + rules R0 to R10
81
+ disensor validate .residue/<id>.json # schema + rules R0 to R13
82
82
  disensor gate --no-comment # what CI will run, locally
83
83
 
84
84
  disensor guide # the filling guide, for any agent or human
@@ -146,7 +146,7 @@ jobs:
146
146
  - uses: actions/checkout@v4
147
147
  with:
148
148
  fetch-depth: 0
149
- - uses: NicolasRocchia/disensor@v0.9.2
149
+ - uses: NicolasRocchia/disensor@v0.9.4
150
150
  ```
151
151
 
152
152
  The gate validates the declarations **the PR adds**, applies the policy and
@@ -165,7 +165,7 @@ models by hand.
165
165
 
166
166
  ```bash
167
167
  disensor reviewer suggest # what this machine has, offline
168
- disensor reviewer add codex --yes # register it, once per machine
168
+ disensor reviewer add codex --model <model> --yes # once per machine, with a model your account runs
169
169
 
170
170
  disensor round --gate diff --generator-family anthropic \
171
171
  --base main --head HEAD --result ../result.json
@@ -207,13 +207,18 @@ declare what happened.
207
207
 
208
208
  ## What the gate enforces
209
209
 
210
- Per artifact (rules R0 to R10): coherence between findings and residue, counts
210
+ Per artifact (rules R0 to R13): coherence between findings and residue, counts
211
211
  that add up, family decorrelation between generator and reviewer, mandatory
212
212
  material evidence in verifiable refutations (`text`, `link` or `hash`) against a
213
213
  verifiable target (`verification.against` other than `none`), mandatory human
214
214
  attention in interpretive refutations, a fix verified before closing a finding
215
215
  in a diff gate, rejection of generic markers (in English and in Spanish), and a
216
- minimized profile with the free text R9 covers stripped.
216
+ minimized profile with the free text R9 covers stripped. From residue/v0.4 they
217
+ also cover the coherence between declared independence and the families and
218
+ models the declaration names (R4), the minimums of the degraded mode per level
219
+ (R11) and its per-reviewer correlation residue (R12), and from this version the
220
+ integrity of local identifiers: unique, and every reference to a reviewer
221
+ resolves against the declared ones (R13).
217
222
 
218
223
  Per artifact, against the PR: level equal to the repository's declared one (G2),
219
224
  Level A blocked while governance is not validated (G3), reviewer confinement
@@ -288,6 +293,7 @@ to resolve:
288
293
  - **Organisation ruleset or required workflow**, defined outside the audited repository.
289
294
  - **Pin the Action by SHA**, not by tag: a tag is movable and is not a root of trust. `disensor init` resolves the tag of the installed version to the commit it points at and writes the workflow already frozen; without network at init time the tag stays and `disensor pin` finishes the job. The command resolves annotated tags to the commit they wrap, never to the tag object, which is the classic trap of doing it by hand. This repository's documentation still uses the tag, because it documents which version corresponds; the frozen SHA is produced by whoever deploys.
290
295
  - **Bootstrap**: the first PR that adds the config and the workflow cannot make itself the root of trust. Initial activation is an administrative step, prior to the gate meaning anything.
296
+ - **When the pin goes up**: the workflow sits in the non-relaxable floor, so raising the pin through a PR costs an adversarial round for a change whose correctness a `git rev-parse` verifies better than any model. The convention in this repository is that a new pin **travels in the next PR of real work**, with its declaration, rather than in a PR of its own. **Exception**: if the release fixes gate security or changes the schema version, the pin goes up immediately, because the window in which the repository judges itself with the previous version stops being harmless: an old gate does not know the new contract and rejects what the freshly published CLI emits. This is a convention, not a control: as long as the branch does not require pull requests with bypass disabled for administrators too, nothing prevents pushing the pin directly ([#17](https://github.com/NicolasRocchia/disensor/issues/17)).
291
297
 
292
298
  Explicit limit: reading the policy from the base turns a one-step bypass into a
293
299
  two-step one, it does not eliminate it. Whoever can merge a relaxation uses it
@@ -319,21 +325,49 @@ not help if `event.pr` carries the URL. The schema says as much about the
319
325
  extension space: an identifier-shaped key can still carry a message. Treat
320
326
  `minimized` as a reduction of surface, not as a guarantee that nothing leaves.
321
327
 
328
+ ## What each number promises
329
+
330
+ The package and the schema are numbered separately, and they promise different
331
+ things.
332
+
333
+ **Stable within a major series of the package.** The exit codes of
334
+ `disensor round` (`0` reviewed, `1` error, `3` no round required, `4` chain
335
+ exhausted, `5` tree modified during the round, `6` could not decide whether a
336
+ round was required); the subcommands and flags documented in this file; the four
337
+ inputs of the Action (`github-token`, `directory`, `config`, `python-version`);
338
+ and the keys of `disensor.config.json`, which already fail closed on anything
339
+ unknown. Breaking these needs a major.
340
+
341
+ **Versioned on their own.** The declaration schema (`residue/vX.Y`, stated inside
342
+ every artifact) and the result of `disensor round` (`result_version`). Neither
343
+ follows the package numbering, and a change in them is not a package major.
344
+
345
+ **No promise.** The reviewer registry on your machine, the recipe catalogue, and
346
+ the ingestion API of the evidence plane. The catalogue is the one surface that
347
+ depends on third-party CLIs: the Codex recipe changed the day an account stopped
348
+ offering the model it named.
349
+
350
+ **One asymmetry worth knowing before you pin.** The gate demands that a
351
+ declaration a PR adds states the current schema version, and it rejects both
352
+ superseded and *newer* ones. The Action installs the CLI from its own checkout,
353
+ so the SHA you pin fixes which schema version your gate accepts. A schema change
354
+ therefore requires updating that pin before declarations under the new version
355
+ can merge.
356
+
322
357
  ## Conformance between implementations
323
358
 
324
- `spec/vectors/` holds the conformance vectors: 31 artifacts with their expected
359
+ `spec/vectors/` holds the conformance vectors, one suite per schema version: 11 artifacts for v0.2, 35 for v0.3 and 43 for v0.4, each with its expected
325
360
  verdict (valid or not, and the rule labels that must fire). Every validator
326
361
  implementation has to pass them identically: the Python reference runs them in
327
362
  its suite (`tests/test_vectors.py`) and the TypeScript port of the evidence
328
363
  plane runs them with `npm run conformidad`. Labels are compared, not messages.
329
- The vectors are regenerated with `python -m disensor.vectors spec/vectors`.
364
+ The vectors are regenerated with `python -m disensor.vectors <directory>`. The generator produces the current schema version and refuses to write over a suite that declares another one: overwriting a historical suite would erase the only negative coverage those rules have. The runner fails if a known version has no vectors declaring it, because otherwise a version can be claimed as supported without anything checking it, which is exactly how v0.2 got here. `spec/version_ordinality.json` carries the other shared vectors: the form of a schema identifier and which rules reach which declaration, both verified by both implementations.
330
365
 
331
366
  Each vector is validated under the schema of the version it declares. The
332
- TypeScript port implements the rules of **v0.2 and v0.3**: handed a v0.4
333
- artifact it says so and refuses, rather than returning a verdict without having
334
- run the rules that version added. So the two-independent-implementations claim
335
- currently covers up to v0.3; the Python reference is the only one that validates
336
- v0.4 ([#29](https://github.com/NicolasRocchia/disensor/issues/29)).
367
+ TypeScript port implements the rules of **v0.2, v0.3 and v0.4**, so the
368
+ two-independent-implementations claim covers the version the CLI emits. Handed a
369
+ version it does not implement it says so and refuses, rather than returning a
370
+ verdict without having run the rules that version added.
337
371
 
338
372
  `plano-evidencia/` holds the ingestion Worker (Cloudflare Workers plus D1) with
339
373
  the TypeScript port of the validator and the append-only integrity receipt. See
@@ -467,7 +501,20 @@ it says.
467
501
 
468
502
  ## Status
469
503
 
470
- v0.9.2, on **residue/v0.4**. This version makes `disensor guide` hand over the
504
+ v0.9.4, on **residue/v0.4**. This version closes residue/v0.4: the TypeScript
505
+ port validates v0.2, v0.3 and v0.4, so the two-independent-implementations claim
506
+ covers the version the CLI emits; conformance runs 89 vectors across three suites
507
+ plus 28 shared cases fixing the form of a schema identifier and which rules reach
508
+ which declaration, and it fails when a known version has no vectors declaring it;
509
+ the Level A floor is enforced; referential integrity enters as R13, guarded so it
510
+ does not reach frozen versions; the frozen resources are verified by content and
511
+ not only by name; and which rules reach which declaration stopped depending on
512
+ the order the lines happen to be written in. The previous version writes down
513
+ when the gate Action's own pin goes up: it travels in the next PR of real work, except when the release
514
+ fixes gate security or changes the schema version, and it says out loud that
515
+ this is a convention rather than a control
516
+ ([#17](https://github.com/NicolasRocchia/disensor/issues/17)). The previous
517
+ version makes `disensor guide` hand over the
471
518
  event runbook as well as the artifact filling guide, so an agent that is not
472
519
  Claude Code gets from one command the same material the Claude Code skill
473
520
  carries, which is what the documentation had been promising since the round
@@ -487,10 +534,14 @@ residue/v0.3 hardens three points of the artifact, closing issues
487
534
  [#7](https://github.com/NicolasRocchia/disensor/issues/7) and
488
535
  [#8](https://github.com/NicolasRocchia/disensor/issues/8). See "Schema
489
536
  migration: residue/v0.2 to residue/v0.3". Decision closed in v0.2: schema keys
490
- and CLI in English (Spanish remains as CLI aliases). The schema may change up to
491
- v1.0; changes are declared in the schema itself. Decision open before v1.0: the
492
- definitive licence (MIT today; Apache-2.0 under consideration for its patent
493
- grant).
537
+ and CLI in English (Spanish remains as CLI aliases). The schema may change;
538
+ each version from residue/v0.2 onwards is frozen under its own identifier, and a
539
+ declaration keeps being validated under the rules that judged it when it was
540
+ emitted. The Spanish-keyed residuo/v0.1 is recognised and refused with migration
541
+ instructions, not validated. No version is
542
+ committed to as the point where the schema stabilises: when there is a contract
543
+ ratified as stable, it will be said here. Open decision: the definitive licence
544
+ (MIT today; Apache-2.0 under consideration for its patent grant).
494
545
 
495
546
  ## Licence
496
547
 
@@ -31,7 +31,7 @@ paper is in Spanish; the glossary at the end maps its terminology to the schema.
31
31
 
32
32
  - `spec/residue.schema.json`: the artifact schema (JSON Schema 2020-12), version residue/v0.4. Superseded versions keep their own frozen resource next to it.
33
33
  - `spec/examples/`: three example artifacts, including a real anonymised event and the minimized profile with no free text.
34
- - `src/disensor/`: Python package with the validator (rules R0 to R10), the CI gate (checks G1 to G9), the PR comment rendering, artifact and repository scaffolding (`init`), and the packaged filling guide (`GUIDE.md`).
34
+ - `src/disensor/`: Python package with the validator (rules R0 to R13), the CI gate (checks G1 to G9), the PR comment rendering, artifact and repository scaffolding (`init`), and the packaged filling guide (`GUIDE.md`).
35
35
  - `action.yml`: composite GitHub Action, ready to use.
36
36
  - `docs/integracion-claude-code.md` (Spanish only): how the real flow (Claude Code plus a reviewer from another family) emits the artifact at the close of each event.
37
37
  - `docs/antecedentes.md` (Spanish only): where the method sits relative to the literature (residual doubt and defeaters, design rationale and its capture bottleneck, multi-agent adversarial review, governance runtimes, supply chain provenance), with the verification status of each reference.
@@ -53,7 +53,7 @@ disensor new --gate diff --level B --round ../result.json # declaration from t
53
53
  disensor prompt --gate diff # the adversarial brief, to hand to a reviewer from another family
54
54
  disensor pack --gate diff --base main --head HEAD # the full package, if you drive the round yourself
55
55
  disensor new --gate diff --level B # template prefilled in .residue/
56
- disensor validate .residue/<id>.json # schema + rules R0 to R10
56
+ disensor validate .residue/<id>.json # schema + rules R0 to R13
57
57
  disensor gate --no-comment # what CI will run, locally
58
58
 
59
59
  disensor guide # the filling guide, for any agent or human
@@ -121,7 +121,7 @@ jobs:
121
121
  - uses: actions/checkout@v4
122
122
  with:
123
123
  fetch-depth: 0
124
- - uses: NicolasRocchia/disensor@v0.9.2
124
+ - uses: NicolasRocchia/disensor@v0.9.4
125
125
  ```
126
126
 
127
127
  The gate validates the declarations **the PR adds**, applies the policy and
@@ -140,7 +140,7 @@ models by hand.
140
140
 
141
141
  ```bash
142
142
  disensor reviewer suggest # what this machine has, offline
143
- disensor reviewer add codex --yes # register it, once per machine
143
+ disensor reviewer add codex --model <model> --yes # once per machine, with a model your account runs
144
144
 
145
145
  disensor round --gate diff --generator-family anthropic \
146
146
  --base main --head HEAD --result ../result.json
@@ -182,13 +182,18 @@ declare what happened.
182
182
 
183
183
  ## What the gate enforces
184
184
 
185
- Per artifact (rules R0 to R10): coherence between findings and residue, counts
185
+ Per artifact (rules R0 to R13): coherence between findings and residue, counts
186
186
  that add up, family decorrelation between generator and reviewer, mandatory
187
187
  material evidence in verifiable refutations (`text`, `link` or `hash`) against a
188
188
  verifiable target (`verification.against` other than `none`), mandatory human
189
189
  attention in interpretive refutations, a fix verified before closing a finding
190
190
  in a diff gate, rejection of generic markers (in English and in Spanish), and a
191
- minimized profile with the free text R9 covers stripped.
191
+ minimized profile with the free text R9 covers stripped. From residue/v0.4 they
192
+ also cover the coherence between declared independence and the families and
193
+ models the declaration names (R4), the minimums of the degraded mode per level
194
+ (R11) and its per-reviewer correlation residue (R12), and from this version the
195
+ integrity of local identifiers: unique, and every reference to a reviewer
196
+ resolves against the declared ones (R13).
192
197
 
193
198
  Per artifact, against the PR: level equal to the repository's declared one (G2),
194
199
  Level A blocked while governance is not validated (G3), reviewer confinement
@@ -263,6 +268,7 @@ to resolve:
263
268
  - **Organisation ruleset or required workflow**, defined outside the audited repository.
264
269
  - **Pin the Action by SHA**, not by tag: a tag is movable and is not a root of trust. `disensor init` resolves the tag of the installed version to the commit it points at and writes the workflow already frozen; without network at init time the tag stays and `disensor pin` finishes the job. The command resolves annotated tags to the commit they wrap, never to the tag object, which is the classic trap of doing it by hand. This repository's documentation still uses the tag, because it documents which version corresponds; the frozen SHA is produced by whoever deploys.
265
270
  - **Bootstrap**: the first PR that adds the config and the workflow cannot make itself the root of trust. Initial activation is an administrative step, prior to the gate meaning anything.
271
+ - **When the pin goes up**: the workflow sits in the non-relaxable floor, so raising the pin through a PR costs an adversarial round for a change whose correctness a `git rev-parse` verifies better than any model. The convention in this repository is that a new pin **travels in the next PR of real work**, with its declaration, rather than in a PR of its own. **Exception**: if the release fixes gate security or changes the schema version, the pin goes up immediately, because the window in which the repository judges itself with the previous version stops being harmless: an old gate does not know the new contract and rejects what the freshly published CLI emits. This is a convention, not a control: as long as the branch does not require pull requests with bypass disabled for administrators too, nothing prevents pushing the pin directly ([#17](https://github.com/NicolasRocchia/disensor/issues/17)).
266
272
 
267
273
  Explicit limit: reading the policy from the base turns a one-step bypass into a
268
274
  two-step one, it does not eliminate it. Whoever can merge a relaxation uses it
@@ -294,21 +300,49 @@ not help if `event.pr` carries the URL. The schema says as much about the
294
300
  extension space: an identifier-shaped key can still carry a message. Treat
295
301
  `minimized` as a reduction of surface, not as a guarantee that nothing leaves.
296
302
 
303
+ ## What each number promises
304
+
305
+ The package and the schema are numbered separately, and they promise different
306
+ things.
307
+
308
+ **Stable within a major series of the package.** The exit codes of
309
+ `disensor round` (`0` reviewed, `1` error, `3` no round required, `4` chain
310
+ exhausted, `5` tree modified during the round, `6` could not decide whether a
311
+ round was required); the subcommands and flags documented in this file; the four
312
+ inputs of the Action (`github-token`, `directory`, `config`, `python-version`);
313
+ and the keys of `disensor.config.json`, which already fail closed on anything
314
+ unknown. Breaking these needs a major.
315
+
316
+ **Versioned on their own.** The declaration schema (`residue/vX.Y`, stated inside
317
+ every artifact) and the result of `disensor round` (`result_version`). Neither
318
+ follows the package numbering, and a change in them is not a package major.
319
+
320
+ **No promise.** The reviewer registry on your machine, the recipe catalogue, and
321
+ the ingestion API of the evidence plane. The catalogue is the one surface that
322
+ depends on third-party CLIs: the Codex recipe changed the day an account stopped
323
+ offering the model it named.
324
+
325
+ **One asymmetry worth knowing before you pin.** The gate demands that a
326
+ declaration a PR adds states the current schema version, and it rejects both
327
+ superseded and *newer* ones. The Action installs the CLI from its own checkout,
328
+ so the SHA you pin fixes which schema version your gate accepts. A schema change
329
+ therefore requires updating that pin before declarations under the new version
330
+ can merge.
331
+
297
332
  ## Conformance between implementations
298
333
 
299
- `spec/vectors/` holds the conformance vectors: 31 artifacts with their expected
334
+ `spec/vectors/` holds the conformance vectors, one suite per schema version: 11 artifacts for v0.2, 35 for v0.3 and 43 for v0.4, each with its expected
300
335
  verdict (valid or not, and the rule labels that must fire). Every validator
301
336
  implementation has to pass them identically: the Python reference runs them in
302
337
  its suite (`tests/test_vectors.py`) and the TypeScript port of the evidence
303
338
  plane runs them with `npm run conformidad`. Labels are compared, not messages.
304
- The vectors are regenerated with `python -m disensor.vectors spec/vectors`.
339
+ The vectors are regenerated with `python -m disensor.vectors <directory>`. The generator produces the current schema version and refuses to write over a suite that declares another one: overwriting a historical suite would erase the only negative coverage those rules have. The runner fails if a known version has no vectors declaring it, because otherwise a version can be claimed as supported without anything checking it, which is exactly how v0.2 got here. `spec/version_ordinality.json` carries the other shared vectors: the form of a schema identifier and which rules reach which declaration, both verified by both implementations.
305
340
 
306
341
  Each vector is validated under the schema of the version it declares. The
307
- TypeScript port implements the rules of **v0.2 and v0.3**: handed a v0.4
308
- artifact it says so and refuses, rather than returning a verdict without having
309
- run the rules that version added. So the two-independent-implementations claim
310
- currently covers up to v0.3; the Python reference is the only one that validates
311
- v0.4 ([#29](https://github.com/NicolasRocchia/disensor/issues/29)).
342
+ TypeScript port implements the rules of **v0.2, v0.3 and v0.4**, so the
343
+ two-independent-implementations claim covers the version the CLI emits. Handed a
344
+ version it does not implement it says so and refuses, rather than returning a
345
+ verdict without having run the rules that version added.
312
346
 
313
347
  `plano-evidencia/` holds the ingestion Worker (Cloudflare Workers plus D1) with
314
348
  the TypeScript port of the validator and the append-only integrity receipt. See
@@ -442,7 +476,20 @@ it says.
442
476
 
443
477
  ## Status
444
478
 
445
- v0.9.2, on **residue/v0.4**. This version makes `disensor guide` hand over the
479
+ v0.9.4, on **residue/v0.4**. This version closes residue/v0.4: the TypeScript
480
+ port validates v0.2, v0.3 and v0.4, so the two-independent-implementations claim
481
+ covers the version the CLI emits; conformance runs 89 vectors across three suites
482
+ plus 28 shared cases fixing the form of a schema identifier and which rules reach
483
+ which declaration, and it fails when a known version has no vectors declaring it;
484
+ the Level A floor is enforced; referential integrity enters as R13, guarded so it
485
+ does not reach frozen versions; the frozen resources are verified by content and
486
+ not only by name; and which rules reach which declaration stopped depending on
487
+ the order the lines happen to be written in. The previous version writes down
488
+ when the gate Action's own pin goes up: it travels in the next PR of real work, except when the release
489
+ fixes gate security or changes the schema version, and it says out loud that
490
+ this is a convention rather than a control
491
+ ([#17](https://github.com/NicolasRocchia/disensor/issues/17)). The previous
492
+ version makes `disensor guide` hand over the
446
493
  event runbook as well as the artifact filling guide, so an agent that is not
447
494
  Claude Code gets from one command the same material the Claude Code skill
448
495
  carries, which is what the documentation had been promising since the round
@@ -462,10 +509,14 @@ residue/v0.3 hardens three points of the artifact, closing issues
462
509
  [#7](https://github.com/NicolasRocchia/disensor/issues/7) and
463
510
  [#8](https://github.com/NicolasRocchia/disensor/issues/8). See "Schema
464
511
  migration: residue/v0.2 to residue/v0.3". Decision closed in v0.2: schema keys
465
- and CLI in English (Spanish remains as CLI aliases). The schema may change up to
466
- v1.0; changes are declared in the schema itself. Decision open before v1.0: the
467
- definitive licence (MIT today; Apache-2.0 under consideration for its patent
468
- grant).
512
+ and CLI in English (Spanish remains as CLI aliases). The schema may change;
513
+ each version from residue/v0.2 onwards is frozen under its own identifier, and a
514
+ declaration keeps being validated under the rules that judged it when it was
515
+ emitted. The Spanish-keyed residuo/v0.1 is recognised and refused with migration
516
+ instructions, not validated. No version is
517
+ committed to as the point where the schema stabilises: when there is a contract
518
+ ratified as stable, it will be said here. Open decision: the definitive licence
519
+ (MIT today; Apache-2.0 under consideration for its patent grant).
469
520
 
470
521
  ## Licence
471
522
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "disensor"
7
- version = "0.9.2"
7
+ version = "0.9.4"
8
8
  description = "Adversarial plan & code review with a declared residue. Emits, validates and CI-enforces residue declarations (residue/v0.4 schema)."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -175,11 +175,14 @@ de `valid.*` y de `false_positives.*` es igual a la cantidad de hallazgos en ese
175
175
  estado, `escalated_open` lo mismo, y `total_findings` es el largo de la lista.
176
176
  Contá, no estimes.
177
177
 
178
- Las reglas atrapan menos de lo que esa frase sugiere. R6 compara solo cuando la
179
- lista de hallazgos no está vacía, y en el perfil completo R10 chequea
180
- `total_findings` contra una lista vacía pero no los baldes. Así que
181
- `findings: []` con un balde en uno pasa las dos. Que los conteos estén bien es
182
- tu responsabilidad; el validador atrapa algunas discrepancias, no todas.
178
+ Las reglas atrapan menos de lo que esa frase sugiere. R6 compara los conteos
179
+ contra la lista siempre que la lista esté, vacía incluida, y en el perfil
180
+ completo R10 además la exige y rechaza una lista vacía cuyo `total_findings`
181
+ diga otra cosa. Lo que ninguna de las dos puede chequear es si cada hallazgo
182
+ lleva el estado terminal correcto: un hallazgo anotado como incorporado cuando
183
+ en realidad se refutó deja todos los conteos coherentes. Que los estados estén
184
+ bien es tu responsabilidad; el validador atrapa discrepancias, no
185
+ clasificaciones equivocadas.
183
186
 
184
187
  ## Perfil minimizado
185
188
 
@@ -170,11 +170,13 @@ Either `items` or the express absence, never an empty field.
170
170
  that state, `escalated_open` likewise, `total_findings` equals the list
171
171
  length. Count, do not estimate.
172
172
 
173
- The rules catch less than that sentence suggests. R6 compares only when the
174
- findings list is non-empty, and in the full profile R10 checks `total_findings`
175
- against an empty list but not the buckets. So `findings: []` with a bucket set
176
- to one passes both. Getting the counts right is on you; the validator catches
177
- some mismatches, not all of them.
173
+ The rules catch less than that sentence suggests. R6 compares the counts against
174
+ the list whenever the list is there, empty included, and in the full profile R10
175
+ also demands the list and rejects an empty one whose `total_findings` says
176
+ otherwise. What neither of them can check is whether each finding carries the
177
+ right terminal state: a finding recorded as incorporated when it was really
178
+ refuted keeps every count coherent. Getting the states right is on you; the
179
+ validator catches mismatches, not misclassification.
178
180
 
179
181
  ## Minimized profile
180
182
 
@@ -1,2 +1,2 @@
1
1
  """disensor: residue declaration of adversarial review (controlled disagreement)."""
2
- __version__ = "0.9.2"
2
+ __version__ = "0.9.4"
@@ -171,6 +171,34 @@ def canonical_repo_path(raw: str, root: Path, label: str) -> str:
171
171
  return "/".join(parts)
172
172
 
173
173
 
174
+ def _level_a_without_coverage(config: dict | None) -> bool:
175
+ """La combinacion que el nivel A no admite. `None` es una config ilegible."""
176
+ if not isinstance(config, dict):
177
+ return False
178
+ return (config.get("criticality_level") == "A"
179
+ and config.get("gate", {}).get("required") is False)
180
+
181
+
182
+ def _refuse_level_a_without_coverage(config: dict, where: str) -> None:
183
+ """En nivel A, apagar la cobertura no es una preferencia: la vacia.
184
+
185
+ Con `gate.required=false`, un PR de codigo sin declaracion no crea ningun
186
+ revisor sobre el que comprobar el piso, asi que la exigencia de familia
187
+ distinta y endurecimiento probado no se aplica a nada y el gate queda verde.
188
+ El nivel A esta reservado para lo que no se puede deshacer: o significa lo
189
+ que dice o no significa nada.
190
+ """
191
+ if config.get("criticality_level") != "A":
192
+ return
193
+ if config.get("gate", {}).get("required") is False:
194
+ raise GateFailure(
195
+ f"{where}: Level A with gate.required=false. Turning coverage off leaves no "
196
+ "declaration to check the floor against, so the level that demands a cross-family "
197
+ "reviewer with tested hardening would demand it of nothing. Lower the level or "
198
+ "leave coverage on"
199
+ )
200
+
201
+
174
202
  def validate_config_shape(config: dict, where: str) -> None:
175
203
  old = V01_CONFIG_KEYS & set(config)
176
204
  if old:
@@ -423,6 +451,20 @@ def classify_requirement(ctx: GateContext) -> ReviewRequirement:
423
451
  # ortogonal a la necesidad de ronda, el gate las reporta por su cuenta y
424
452
  # sigue evaluando el resto. Quedan en el contexto para que `round` pueda
425
453
  # advertir sin mezclar las dos preguntas.
454
+ if (ctx.config.get("criticality_level") == "A"
455
+ and not ctx.config["gate"].get("required", True)):
456
+ # El gate rechaza esta politica, asi que el preflight no puede contestar
457
+ # que no hace falta ronda: son las dos caras de la misma decision, y que
458
+ # difieran es exactamente lo que esta funcion existe para impedir.
459
+ return ReviewRequirement(
460
+ status="blocked",
461
+ code="level_a_without_coverage",
462
+ reason=(
463
+ "the policy declares Level A with gate.required=false. With coverage off there "
464
+ "is no declaration to check the floor against, and the gate refuses it: fix the "
465
+ "policy before running a round against it"
466
+ ),
467
+ )
426
468
  if not ctx.config["gate"].get("required", True):
427
469
  return ReviewRequirement(
428
470
  status="not_required",
@@ -753,6 +795,18 @@ def _run_gate(directory, config_path, base, head, repo_dir: Path, post: bool) ->
753
795
  "Level A (the reviewer only reads, and that is guaranteed with permissions, not "
754
796
  "with the brief)"
755
797
  )
798
+ if ev.get("criticality_level") == "A" and r.get("hardening") != "verified":
799
+ # El campo es opcional en el esquema, asi que omitirlo evitaba
800
+ # R12 y pasaba el nivel A: la exigencia vivia solo en el camino
801
+ # feliz del runner, que es justo el que no recorre quien arma la
802
+ # declaracion a mano.
803
+ declarado = r.get("hardening", "not declared")
804
+ own.append(
805
+ f"[G4] reviewer {r['reviewer_id']}: hardening '{declarado}' in Level A. The "
806
+ "level reserved for what cannot be undone demands a reviewer whose "
807
+ "neutralisation of project instructions was tested, and that has to be "
808
+ "declared"
809
+ )
756
810
  if not conf["verified"] and gate_cfg.get("warn_unverified_confinement", True):
757
811
  warnings.append(
758
812
  f"{path}: confinement of reviewer {r['reviewer_id']} without post-run verification"
@@ -769,6 +823,19 @@ def _run_gate(directory, config_path, base, head, repo_dir: Path, post: bool) ->
769
823
  valid = [a for a in artifacts if a.valid]
770
824
  required = gate_cfg.get("required", True)
771
825
 
826
+ if config.get("criticality_level") == "A" and not required:
827
+ # La politica que gobierna este PR deja el nivel A sin nada sobre que
828
+ # aplicar el piso, y eso falla cerrado. Pero no puede trabar el PR que
829
+ # viene a arreglarlo: si el head la repara, el repositorio necesita
830
+ # poder mergearlo, o queda sin salida.
831
+ gate_errors.append(
832
+ f"[G3] {cfg_path} declares Level A with gate.required=false. With coverage off there "
833
+ "is no declaration to check the floor against, so the level that demands a "
834
+ "cross-family reviewer with tested hardening would demand it of nothing. Repairing "
835
+ "this policy is an administrative step, like the initial activation: the gate cannot "
836
+ "wave through the PR that fixes the rules it is judging by"
837
+ )
838
+
772
839
  if required:
773
840
  coverage_errors, coverage_notes, _demanding = evaluate_coverage(
774
841
  artifacts, ordinary, config, evidence_root, cfg_path
@@ -787,6 +854,18 @@ def _run_gate(directory, config_path, base, head, repo_dir: Path, post: bool) ->
787
854
  if not isinstance(head_raw, dict):
788
855
  raise GateFailure("the configuration is not an object")
789
856
  validate_config_shape(head_raw, f"{cfg_path} (head of the PR)")
857
+ # Una forma invalida en el head es un problema del futuro y avisa.
858
+ # Dejar el nivel A sin cobertura no: si esto se mergea, el proximo
859
+ # PR de codigo pasa sin declaracion y el piso no tiene sobre que
860
+ # aplicarse. El gate protege el merge, y este lo debilita.
861
+ if (head_raw.get("criticality_level") == "A"
862
+ and head_raw.get("gate", {}).get("required") is False):
863
+ gate_errors.append(
864
+ f"[G3] {cfg_path} at the head of the PR leaves Level A with "
865
+ "gate.required=false. Merging it would turn coverage off for the level "
866
+ "reserved for what cannot be undone, and the floor would have no "
867
+ "declaration to apply to"
868
+ )
790
869
  base_raw = (
791
870
  json.loads(gitctx.show_text(base_oid, cfg_path, repo_dir))
792
871
  if gitctx.path_exists(base_oid, cfg_path, repo_dir)