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.
- {disensor-0.9.2/src/disensor.egg-info → disensor-0.9.4}/PKG-INFO +70 -19
- {disensor-0.9.2 → disensor-0.9.4}/README.md +69 -18
- {disensor-0.9.2 → disensor-0.9.4}/pyproject.toml +1 -1
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor/GUIDE.es.md +8 -5
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor/GUIDE.md +7 -5
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor/__init__.py +1 -1
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor/gate.py +79 -0
- disensor-0.9.4/src/disensor/residue.schema.v0.4.json +958 -0
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor/reviewers.py +21 -4
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor/round.py +60 -5
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor/rules.py +128 -5
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor/template.py +16 -1
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor/vectors.py +159 -6
- {disensor-0.9.2 → disensor-0.9.4/src/disensor.egg-info}/PKG-INFO +70 -19
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor.egg-info/SOURCES.txt +1 -0
- {disensor-0.9.2 → disensor-0.9.4}/tests/test_docs.py +22 -0
- {disensor-0.9.2 → disensor-0.9.4}/tests/test_gate.py +91 -0
- {disensor-0.9.2 → disensor-0.9.4}/tests/test_new_round.py +33 -0
- {disensor-0.9.2 → disensor-0.9.4}/tests/test_requirement.py +20 -0
- {disensor-0.9.2 → disensor-0.9.4}/tests/test_reviewers.py +43 -1
- {disensor-0.9.2 → disensor-0.9.4}/tests/test_round.py +49 -0
- disensor-0.9.4/tests/test_v04.py +673 -0
- disensor-0.9.4/tests/test_vectors.py +115 -0
- disensor-0.9.2/tests/test_v04.py +0 -211
- disensor-0.9.2/tests/test_vectors.py +0 -41
- {disensor-0.9.2 → disensor-0.9.4}/LICENSE +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/setup.cfg +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor/__main__.py +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor/brief.py +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor/cli.py +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor/gitctx.py +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor/guide.py +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor/init.py +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor/pack.py +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor/pin.py +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor/prompts/_common.md +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor/prompts/architecture.md +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor/prompts/diff.md +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor/prompts/plan.md +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor/render.py +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor/residue.schema.json +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor/residue.schema.v0.2.json +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor/residue.schema.v0.3.json +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor/scope.py +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor.egg-info/dependency_links.txt +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor.egg-info/entry_points.txt +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor.egg-info/requires.txt +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/src/disensor.egg-info/top_level.txt +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/tests/test_brief.py +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/tests/test_cli_errors.py +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/tests/test_docs_sync.py +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/tests/test_guide.py +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/tests/test_hardening.py +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/tests/test_init.py +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/tests/test_pack.py +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/tests/test_pin.py +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/tests/test_rules.py +0 -0
- {disensor-0.9.2 → disensor-0.9.4}/tests/test_scope.py +0 -0
- {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.
|
|
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
|
|
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
|
|
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.
|
|
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 #
|
|
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
|
|
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:
|
|
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
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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.
|
|
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
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
|
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
|
|
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.
|
|
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 #
|
|
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
|
|
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:
|
|
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
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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.
|
|
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
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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.
|
|
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
|
|
179
|
-
lista
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
+
__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)
|