disensor 0.2.0__tar.gz → 0.3.0__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.2.0/src/disensor.egg-info → disensor-0.3.0}/PKG-INFO +8 -5
- {disensor-0.2.0 → disensor-0.3.0}/README.md +7 -4
- {disensor-0.2.0 → disensor-0.3.0}/pyproject.toml +2 -2
- disensor-0.3.0/src/disensor/GUIDE.md +108 -0
- {disensor-0.2.0 → disensor-0.3.0}/src/disensor/__init__.py +1 -1
- {disensor-0.2.0 → disensor-0.3.0}/src/disensor/cli.py +14 -3
- disensor-0.3.0/src/disensor/guide.py +32 -0
- {disensor-0.2.0 → disensor-0.3.0}/src/disensor/init.py +46 -28
- {disensor-0.2.0 → disensor-0.3.0/src/disensor.egg-info}/PKG-INFO +8 -5
- {disensor-0.2.0 → disensor-0.3.0}/src/disensor.egg-info/SOURCES.txt +3 -0
- disensor-0.3.0/tests/test_guide.py +36 -0
- {disensor-0.2.0 → disensor-0.3.0}/tests/test_init.py +20 -10
- {disensor-0.2.0 → disensor-0.3.0}/LICENSE +0 -0
- {disensor-0.2.0 → disensor-0.3.0}/setup.cfg +0 -0
- {disensor-0.2.0 → disensor-0.3.0}/src/disensor/__main__.py +0 -0
- {disensor-0.2.0 → disensor-0.3.0}/src/disensor/gate.py +0 -0
- {disensor-0.2.0 → disensor-0.3.0}/src/disensor/render.py +0 -0
- {disensor-0.2.0 → disensor-0.3.0}/src/disensor/residue.schema.json +0 -0
- {disensor-0.2.0 → disensor-0.3.0}/src/disensor/rules.py +0 -0
- {disensor-0.2.0 → disensor-0.3.0}/src/disensor/template.py +0 -0
- {disensor-0.2.0 → disensor-0.3.0}/src/disensor/vectors.py +0 -0
- {disensor-0.2.0 → disensor-0.3.0}/src/disensor.egg-info/dependency_links.txt +0 -0
- {disensor-0.2.0 → disensor-0.3.0}/src/disensor.egg-info/entry_points.txt +0 -0
- {disensor-0.2.0 → disensor-0.3.0}/src/disensor.egg-info/requires.txt +0 -0
- {disensor-0.2.0 → disensor-0.3.0}/src/disensor.egg-info/top_level.txt +0 -0
- {disensor-0.2.0 → disensor-0.3.0}/tests/test_rules.py +0 -0
- {disensor-0.2.0 → disensor-0.3.0}/tests/test_vectors.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: disensor
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Adversarial plan & code review with a declared residue. Emits, validates and CI-enforces residue declarations (residue/v0.2 schema).
|
|
5
5
|
Author-email: Nicolas Rocchia <nicolasrocchia@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -28,7 +28,7 @@ Paper del método: Rocchia, N. (2026), *Desacuerdo controlado: revisión adversa
|
|
|
28
28
|
|
|
29
29
|
- `spec/residue.schema.json`: el esquema del artefacto (JSON Schema 2020-12), versión residue/v0.2.
|
|
30
30
|
- `spec/examples/`: tres artefactos de ejemplo, incluido un evento real anonimizado y el perfil minimizado sin texto libre.
|
|
31
|
-
- `src/disensor/`: paquete Python con el validador (reglas R0 a R10), el gate de CI (chequeos G1 a G5), el render del comentario de PR, el scaffolding de artefactos y el de repositorios (`init`).
|
|
31
|
+
- `src/disensor/`: paquete Python con el validador (reglas R0 a R10), el gate de CI (chequeos G1 a G5), el render del comentario de PR, el scaffolding de artefactos y el de repositorios (`init`), y la guía de llenado empaquetada (`GUIDE.md`).
|
|
32
32
|
- `action.yml`: GitHub Action compuesta, lista para usar.
|
|
33
33
|
- `docs/integracion-claude-code.md`: cómo el flujo real (Claude Code más un revisor de otra familia) emite el artefacto al cierre de cada evento.
|
|
34
34
|
|
|
@@ -39,15 +39,18 @@ El paquete se instala una vez (global); cada repositorio se inicializa una vez:
|
|
|
39
39
|
```bash
|
|
40
40
|
pip install disensor # o pipx install disensor, recomendado para CLIs
|
|
41
41
|
|
|
42
|
-
disensor init # en la raíz del repo: config,
|
|
42
|
+
disensor init # en la raíz del repo: config, CLAUDE.md, skill de llenado y workflow de CI
|
|
43
43
|
disensor new --gate diff --level B # plantilla prellenada en .residue/
|
|
44
44
|
disensor validate .residue/<id>.json # schema + reglas R0 a R10
|
|
45
45
|
disensor gate --no-comment # lo que va a correr CI, en local
|
|
46
|
+
|
|
47
|
+
disensor guide # la guía de llenado, para cualquier agente o humano
|
|
48
|
+
disensor hash consigna.md # el sha256: que pide prompt_hash, sin calcularlo a mano
|
|
46
49
|
```
|
|
47
50
|
|
|
48
51
|
Los subcomandos y flags de la v0.1 en español (`nuevo`, `validar`, `--compuerta`, `--nivel`, `--directorio`, `--sin-comentario`) siguen funcionando como alias.
|
|
49
52
|
|
|
50
|
-
`disensor init` escribe, en forma idempotente, el `disensor.config.json` (el nivel viaja con el código, en un archivo versionado), la sección de cierre de evento en `CLAUDE.md
|
|
53
|
+
`disensor init` escribe, en forma idempotente, el `disensor.config.json` (el nivel viaja con el código, en un archivo versionado), la sección de cierre de evento en `CLAUDE.md`, la skill de Claude Code con la guía completa de llenado (`.claude/skills/disensor/SKILL.md`, cargada a demanda al cerrar cada ronda) y el workflow del gate; lo que ya existe se respeta y se informa. El principio es que después de `pip install disensor` y `disensor init` el usuario no toque nada a mano: Claude sabe cuándo (CLAUDE.md) y cómo (la skill), cualquier otro agente recibe lo mismo con `disensor guide`, y el CI hace cumplir el resultado. Config resultante:
|
|
51
54
|
|
|
52
55
|
```json
|
|
53
56
|
{
|
|
@@ -120,7 +123,7 @@ Migración desde v0.1: renombrar `.residuo/` a `.residue/`, las claves del confi
|
|
|
120
123
|
|
|
121
124
|
## Estado
|
|
122
125
|
|
|
123
|
-
v0.
|
|
126
|
+
v0.3, borrador en uso. El esquema sigue en residue/v0.2 (v0.3 no lo toca: agrega la skill de llenado, `disensor guide` y `disensor hash`). Decisión cerrada en v0.2: claves del esquema y CLI en inglés (el español queda como alias en la CLI y como idioma de la documentación). El esquema puede cambiar hasta v1.0; los cambios se declaran en el propio esquema. Decisión abierta antes de v1.0: licencia definitiva (hoy MIT; Apache-2.0 está en consideración por la concesión de patentes antes del release público).
|
|
124
127
|
|
|
125
128
|
## Licencia
|
|
126
129
|
|
|
@@ -14,7 +14,7 @@ Paper del método: Rocchia, N. (2026), *Desacuerdo controlado: revisión adversa
|
|
|
14
14
|
|
|
15
15
|
- `spec/residue.schema.json`: el esquema del artefacto (JSON Schema 2020-12), versión residue/v0.2.
|
|
16
16
|
- `spec/examples/`: tres artefactos de ejemplo, incluido un evento real anonimizado y el perfil minimizado sin texto libre.
|
|
17
|
-
- `src/disensor/`: paquete Python con el validador (reglas R0 a R10), el gate de CI (chequeos G1 a G5), el render del comentario de PR, el scaffolding de artefactos y el de repositorios (`init`).
|
|
17
|
+
- `src/disensor/`: paquete Python con el validador (reglas R0 a R10), el gate de CI (chequeos G1 a G5), el render del comentario de PR, el scaffolding de artefactos y el de repositorios (`init`), y la guía de llenado empaquetada (`GUIDE.md`).
|
|
18
18
|
- `action.yml`: GitHub Action compuesta, lista para usar.
|
|
19
19
|
- `docs/integracion-claude-code.md`: cómo el flujo real (Claude Code más un revisor de otra familia) emite el artefacto al cierre de cada evento.
|
|
20
20
|
|
|
@@ -25,15 +25,18 @@ El paquete se instala una vez (global); cada repositorio se inicializa una vez:
|
|
|
25
25
|
```bash
|
|
26
26
|
pip install disensor # o pipx install disensor, recomendado para CLIs
|
|
27
27
|
|
|
28
|
-
disensor init # en la raíz del repo: config,
|
|
28
|
+
disensor init # en la raíz del repo: config, CLAUDE.md, skill de llenado y workflow de CI
|
|
29
29
|
disensor new --gate diff --level B # plantilla prellenada en .residue/
|
|
30
30
|
disensor validate .residue/<id>.json # schema + reglas R0 a R10
|
|
31
31
|
disensor gate --no-comment # lo que va a correr CI, en local
|
|
32
|
+
|
|
33
|
+
disensor guide # la guía de llenado, para cualquier agente o humano
|
|
34
|
+
disensor hash consigna.md # el sha256: que pide prompt_hash, sin calcularlo a mano
|
|
32
35
|
```
|
|
33
36
|
|
|
34
37
|
Los subcomandos y flags de la v0.1 en español (`nuevo`, `validar`, `--compuerta`, `--nivel`, `--directorio`, `--sin-comentario`) siguen funcionando como alias.
|
|
35
38
|
|
|
36
|
-
`disensor init` escribe, en forma idempotente, el `disensor.config.json` (el nivel viaja con el código, en un archivo versionado), la sección de cierre de evento en `CLAUDE.md
|
|
39
|
+
`disensor init` escribe, en forma idempotente, el `disensor.config.json` (el nivel viaja con el código, en un archivo versionado), la sección de cierre de evento en `CLAUDE.md`, la skill de Claude Code con la guía completa de llenado (`.claude/skills/disensor/SKILL.md`, cargada a demanda al cerrar cada ronda) y el workflow del gate; lo que ya existe se respeta y se informa. El principio es que después de `pip install disensor` y `disensor init` el usuario no toque nada a mano: Claude sabe cuándo (CLAUDE.md) y cómo (la skill), cualquier otro agente recibe lo mismo con `disensor guide`, y el CI hace cumplir el resultado. Config resultante:
|
|
37
40
|
|
|
38
41
|
```json
|
|
39
42
|
{
|
|
@@ -106,7 +109,7 @@ Migración desde v0.1: renombrar `.residuo/` a `.residue/`, las claves del confi
|
|
|
106
109
|
|
|
107
110
|
## Estado
|
|
108
111
|
|
|
109
|
-
v0.
|
|
112
|
+
v0.3, borrador en uso. El esquema sigue en residue/v0.2 (v0.3 no lo toca: agrega la skill de llenado, `disensor guide` y `disensor hash`). Decisión cerrada en v0.2: claves del esquema y CLI en inglés (el español queda como alias en la CLI y como idioma de la documentación). El esquema puede cambiar hasta v1.0; los cambios se declaran en el propio esquema. Decisión abierta antes de v1.0: licencia definitiva (hoy MIT; Apache-2.0 está en consideración por la concesión de patentes antes del release público).
|
|
110
113
|
|
|
111
114
|
## Licencia
|
|
112
115
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "disensor"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Adversarial plan & code review with a declared residue. Emits, validates and CI-enforces residue declarations (residue/v0.2 schema)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -22,4 +22,4 @@ disensor = "disensor.cli:main"
|
|
|
22
22
|
where = ["src"]
|
|
23
23
|
|
|
24
24
|
[tool.setuptools.package-data]
|
|
25
|
-
disensor = ["residue.schema.json"]
|
|
25
|
+
disensor = ["residue.schema.json", "GUIDE.md"]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# How to fill a residue declaration (residue/v0.2)
|
|
2
|
+
|
|
3
|
+
This guide is the single source of truth for filling the artifact that
|
|
4
|
+
`disensor new` creates under `.residue/`. It ships inside the package:
|
|
5
|
+
`disensor guide` prints it for any coding agent, and `disensor init` installs
|
|
6
|
+
it as a Claude Code skill. The validator (`disensor validate`) and the CI gate
|
|
7
|
+
enforce everything described here; filling the artifact correctly the first
|
|
8
|
+
time is cheaper than iterating against their errors.
|
|
9
|
+
|
|
10
|
+
## The flow at the close of a review round
|
|
11
|
+
|
|
12
|
+
1. `disensor new --gate <plan|diff> --level <A|B|C>` creates the template,
|
|
13
|
+
prefilled with what git knows (repository, commits, timestamp, uuid).
|
|
14
|
+
2. Fill in every `FILL_IN` marker and the findings of the round. The template
|
|
15
|
+
does not validate while markers remain: that is intentional.
|
|
16
|
+
3. `disensor validate .residue/<id>.json`. Fix until it prints VALID.
|
|
17
|
+
4. Commit the artifact alone: `docs(residue): declare event <short-id>`.
|
|
18
|
+
Never mixed with code changes.
|
|
19
|
+
|
|
20
|
+
Declare what happened, not what should have happened. An event without
|
|
21
|
+
findings and with an express declaration of absence is valid data, not a
|
|
22
|
+
failure.
|
|
23
|
+
|
|
24
|
+
## Actors
|
|
25
|
+
|
|
26
|
+
- `generator`: the assistant that produced the plan or diff. `family` is its
|
|
27
|
+
model family (anthropic, openai, google, meta, mistral, other).
|
|
28
|
+
- `reviewers[]`: the attacking assistants. Each needs `reviewer_id` (r1,
|
|
29
|
+
r2...), `family`, `model`, and `confinement`. Rule R4 rejects any reviewer
|
|
30
|
+
whose family equals the generator's: decorrelation is the point of the
|
|
31
|
+
method, not an option.
|
|
32
|
+
- `reviewers[].prompt_hash`: hash of the adversarial brief given to the
|
|
33
|
+
reviewer. Compute it with `disensor hash <brief-file>`; paste the full
|
|
34
|
+
`sha256:...` output.
|
|
35
|
+
- `confinement.mode`: how it was guaranteed that the reviewer only reads
|
|
36
|
+
(permissions, sandbox, read_only_by_instruction, no_confinement). Declare
|
|
37
|
+
the real mode; the gate makes gaps visible instead of hiding them.
|
|
38
|
+
- `confinement.verified`: true ONLY if you ran `git status` after the
|
|
39
|
+
reviewer's run and the tree was clean. Otherwise leave false.
|
|
40
|
+
- `human_arbiter.present`: must be true; an event without a human arbiter
|
|
41
|
+
does not comply with the protocol (R0).
|
|
42
|
+
|
|
43
|
+
## Findings
|
|
44
|
+
|
|
45
|
+
One entry per point the reviewer raised. Fields: `id` (h1, h2...), `origin`
|
|
46
|
+
(the reviewer_id that produced it), `severity` (critical, major, minor,
|
|
47
|
+
info), `title`, `description`, `location` (full profile only), and:
|
|
48
|
+
|
|
49
|
+
- `verification.against`: what the generator checked the finding against
|
|
50
|
+
before accepting or refuting it: `repository` (code, config, contracts),
|
|
51
|
+
`execution` (running tests or the program), or `none`. Do not take the
|
|
52
|
+
reviewer's word: verify, then decide.
|
|
53
|
+
- `final_state`, the terminal outcome. Decision table:
|
|
54
|
+
- `incorporated`: the finding changed the plan or the code. In the diff
|
|
55
|
+
gate you MUST add `fix_verification` with type `diff_gate` or
|
|
56
|
+
`specific_test` (R7); `pending_in_diff_gate` is only legal in the plan
|
|
57
|
+
gate. If the reviewer's remedy was wrong and you fixed it, record
|
|
58
|
+
`remedy_adjustment`.
|
|
59
|
+
- `debt_recorded`: valid, deferred; requires `debt_id` (schema).
|
|
60
|
+
- `owner_decision`: valid, the owner changed scope, behavior or accepted
|
|
61
|
+
risk; requires `risk_record` (schema).
|
|
62
|
+
- `refuted_verifiable`: false positive with proof; requires `evidence`
|
|
63
|
+
(text quote, link, or hash).
|
|
64
|
+
- `refuted_interpretive`: false positive by judgment; it MUST also appear
|
|
65
|
+
as a residue item (R1) with `requires_human_attention: true` (R8).
|
|
66
|
+
- `escalated_open`: no decision yet; it MUST also appear as a residue
|
|
67
|
+
item (R1).
|
|
68
|
+
|
|
69
|
+
## Residue
|
|
70
|
+
|
|
71
|
+
The heart of the declaration: what the cycle could not close by itself.
|
|
72
|
+
Either `items` or the express absence, never an empty field.
|
|
73
|
+
|
|
74
|
+
- `items[]`: `id` (r1, r2...), `class`, `finding_ref` when it comes from a
|
|
75
|
+
finding, `requires_human_attention`.
|
|
76
|
+
- `escalation_without_decision`: from every `escalated_open` finding.
|
|
77
|
+
- `principal_refutation`: from every refuted finding; add
|
|
78
|
+
`refutation_type` (`verifiable` or `interpretive`; interpretive forces
|
|
79
|
+
`requires_human_attention: true`).
|
|
80
|
+
- `execution_gap`: behavior execution could not arbitrate; add
|
|
81
|
+
`gap_reason`. In Level A an execution gap blocks the merge until a
|
|
82
|
+
technical lead accepts it in writing (`lead_acceptance`, R5).
|
|
83
|
+
- Absence: `"declared_absence": true` plus `declaration`, minimum 30
|
|
84
|
+
characters of concrete text. Generic markers (none, n/a, all resolved,
|
|
85
|
+
ninguno, todo resuelto...) are rejected by R2 in any language.
|
|
86
|
+
|
|
87
|
+
## Metrics
|
|
88
|
+
|
|
89
|
+
`counts` must add up exactly against the findings list (R6): each
|
|
90
|
+
`valid.*` and `false_positives.*` bucket equals the number of findings in
|
|
91
|
+
that state, `escalated_open` likewise, `total_findings` equals the list
|
|
92
|
+
length. Count, do not estimate.
|
|
93
|
+
|
|
94
|
+
## Minimized profile
|
|
95
|
+
|
|
96
|
+
No free text anywhere (R9): no titles, descriptions or locations in
|
|
97
|
+
findings; no descriptions in items; evidence only as `hash`; `repository`
|
|
98
|
+
as a hash or opaque identifier, never a URL.
|
|
99
|
+
|
|
100
|
+
## Quick map of validator labels
|
|
101
|
+
|
|
102
|
+
R0 human arbiter absent; R1 residue/finding coherence; R2 generic or
|
|
103
|
+
template markers; R3 abbreviated path over protected cases; R4 reviewer
|
|
104
|
+
shares the generator's family; R5 Level A execution gap without lead
|
|
105
|
+
acceptance; R6 counts that do not add up; R7 incorporated without verified
|
|
106
|
+
fix in the diff gate; R8 interpretive refutation without human attention;
|
|
107
|
+
R9 text leaks in the minimized profile; R10 full profile without findings;
|
|
108
|
+
`schema` shape errors (missing required fields, wrong enums, bad patterns).
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""disensor: residue declaration of adversarial review (controlled disagreement)."""
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.3.0"
|
|
@@ -12,6 +12,7 @@ import sys
|
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
|
|
14
14
|
from .gate import main_gate
|
|
15
|
+
from .guide import main_guide, main_hash
|
|
15
16
|
from .init import main_init
|
|
16
17
|
from .rules import load_schema, validate_artifact
|
|
17
18
|
from .template import main_new
|
|
@@ -38,11 +39,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
38
39
|
)
|
|
39
40
|
sub = p.add_subparsers(dest="command", required=True)
|
|
40
41
|
|
|
41
|
-
init = sub.add_parser("init", help="Scaffold a repository: config, CLAUDE.md section and CI workflow.")
|
|
42
|
+
init = sub.add_parser("init", help="Scaffold a repository: config, CLAUDE.md section, filling skill and CI workflow.")
|
|
42
43
|
init.add_argument("--level", "--nivel", choices=["A", "B", "C"], default="B")
|
|
43
|
-
init.add_argument("--no-claude", action="store_true", help="Do not touch CLAUDE.md.")
|
|
44
|
+
init.add_argument("--no-claude", action="store_true", help="Do not touch CLAUDE.md nor the skill.")
|
|
45
|
+
init.add_argument("--no-skill", action="store_true", help="Write the CLAUDE.md section but not the skill.")
|
|
44
46
|
init.add_argument("--claude-global", action="store_true",
|
|
45
|
-
help="Write the Claude Code section to ~/.claude
|
|
47
|
+
help="Write the Claude Code section and skill to ~/.claude instead of the repo.")
|
|
46
48
|
init.add_argument("--no-workflow", action="store_true", help="Do not write the CI workflow.")
|
|
47
49
|
init.set_defaults(func=main_init)
|
|
48
50
|
|
|
@@ -67,6 +69,15 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
67
69
|
gate.add_argument("--no-comment", "--sin-comentario", action="store_true",
|
|
68
70
|
help="Do not post a comment on the PR.")
|
|
69
71
|
gate.set_defaults(func=main_gate)
|
|
72
|
+
|
|
73
|
+
guide = sub.add_parser("guide", help="Print the artifact filling guide (for any coding agent or human).")
|
|
74
|
+
guide.set_defaults(func=main_guide)
|
|
75
|
+
|
|
76
|
+
hash_ = sub.add_parser("hash", help="Compute the sha256:<hex> value for prompt_hash from a file or text.")
|
|
77
|
+
src = hash_.add_mutually_exclusive_group(required=True)
|
|
78
|
+
src.add_argument("file", nargs="?", help="File to hash (e.g. the adversarial brief).")
|
|
79
|
+
src.add_argument("--text", help="Hash this literal text instead of a file.")
|
|
80
|
+
hash_.set_defaults(func=main_hash)
|
|
70
81
|
return p
|
|
71
82
|
|
|
72
83
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Packaged filling guide and hash helper: `disensor guide` and `disensor hash`.
|
|
2
|
+
|
|
3
|
+
The guide (GUIDE.md, shipped inside the package) is the single source of
|
|
4
|
+
truth on how to fill a residue declaration. `disensor init` installs it as a
|
|
5
|
+
Claude Code skill; `disensor guide` prints it for any other coding agent or
|
|
6
|
+
for a human. `disensor hash` computes the `sha256:...` value the schema
|
|
7
|
+
expects in `prompt_hash`, so nobody hashes the adversarial brief by hand.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
from importlib import resources
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def guide_text() -> str:
|
|
17
|
+
"""The packaged guide, verbatim."""
|
|
18
|
+
return resources.files("disensor").joinpath("GUIDE.md").read_text(encoding="utf-8")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main_guide(args) -> int:
|
|
22
|
+
print(guide_text(), end="")
|
|
23
|
+
return 0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main_hash(args) -> int:
|
|
27
|
+
if args.text is not None:
|
|
28
|
+
data = args.text.encode("utf-8")
|
|
29
|
+
else:
|
|
30
|
+
data = Path(args.file).read_bytes()
|
|
31
|
+
print("sha256:" + hashlib.sha256(data).hexdigest())
|
|
32
|
+
return 0
|
|
@@ -5,10 +5,16 @@ initialized once with this command. Idempotent by design: running it again
|
|
|
5
5
|
respects what already exists and reports what it did, so nothing is ever
|
|
6
6
|
silently overwritten.
|
|
7
7
|
|
|
8
|
-
It writes
|
|
8
|
+
It writes four pieces, each optional by flag:
|
|
9
9
|
1. disensor.config.json: the criticality level, versioned with the code.
|
|
10
|
-
2. A CLAUDE.md section: the event-close
|
|
11
|
-
3.
|
|
10
|
+
2. A CLAUDE.md section: the event-close trigger for Claude Code.
|
|
11
|
+
3. A Claude Code skill with the full filling guide, loaded on demand
|
|
12
|
+
(the same text `disensor guide` prints for any other agent).
|
|
13
|
+
4. .github/workflows/disensor.yml: the CI gate, pinned to this version.
|
|
14
|
+
|
|
15
|
+
After `pip install disensor` and `disensor init`, the user should not have
|
|
16
|
+
to touch anything by hand: Claude knows when (CLAUDE.md) and how (skill),
|
|
17
|
+
and CI enforces the result.
|
|
12
18
|
"""
|
|
13
19
|
from __future__ import annotations
|
|
14
20
|
|
|
@@ -17,6 +23,7 @@ import subprocess
|
|
|
17
23
|
from pathlib import Path
|
|
18
24
|
|
|
19
25
|
from . import __version__
|
|
26
|
+
from .guide import guide_text
|
|
20
27
|
|
|
21
28
|
CLAUDE_HEADING = "## disensor: residue declaration at event close"
|
|
22
29
|
|
|
@@ -25,29 +32,16 @@ CLAUDE_SECTION = f"""{CLAUDE_HEADING}
|
|
|
25
32
|
At the end of each adversarial review round (plan or diff), BEFORE closing
|
|
26
33
|
the event:
|
|
27
34
|
|
|
28
|
-
1.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
- The verification of each finding (`against`: repository or execution).
|
|
36
|
-
- In the diff gate, the fix verification of each incorporated finding
|
|
37
|
-
(diff_gate or specific_test). Never "pending".
|
|
38
|
-
- The residue: escalations without a decision, refutations of the
|
|
39
|
-
principal, and execution gaps. If nothing remained, the express
|
|
40
|
-
declaration of absence (concrete text, not "no residue").
|
|
41
|
-
- The sha256 hash of the adversarial brief used, in `prompt_hash`.
|
|
42
|
-
- `confinement.verified: true` ONLY if you ran `git status` after the
|
|
43
|
-
round and it was clean.
|
|
44
|
-
2. Run `disensor validate` on the file. If it fails, fix it: the CI gate
|
|
35
|
+
1. `disensor new --gate <plan|diff> --level <A|B|C>` creates the template
|
|
36
|
+
under `.residue/`.
|
|
37
|
+
2. Fill it following the disensor skill (`.claude/skills/disensor/SKILL.md`;
|
|
38
|
+
the same guide is available as `disensor guide`). Do not invent findings
|
|
39
|
+
or states: the artifact declares what happened, not what should have
|
|
40
|
+
happened.
|
|
41
|
+
3. Run `disensor validate` on the file until it prints VALID; the CI gate
|
|
45
42
|
rejects exactly the same.
|
|
46
|
-
|
|
47
|
-
<short-id>`)
|
|
48
|
-
4. Do not invent findings or states: the artifact declares what happened,
|
|
49
|
-
not what should have happened. An event without findings and with an
|
|
50
|
-
express declaration of absence is valid pilot data, not a failure.
|
|
43
|
+
4. The artifact goes in its own commit (`docs(residue): declare event
|
|
44
|
+
<short-id>`), never mixed with code.
|
|
51
45
|
"""
|
|
52
46
|
|
|
53
47
|
GLOBAL_GUARD = (
|
|
@@ -55,6 +49,13 @@ GLOBAL_GUARD = (
|
|
|
55
49
|
"`disensor.config.json`:\n\n"
|
|
56
50
|
)
|
|
57
51
|
|
|
52
|
+
SKILL_FRONTMATTER = """---
|
|
53
|
+
name: disensor
|
|
54
|
+
description: Fill and validate a disensor residue declaration (.residue/*.json) at the close of an adversarial review round. Use when closing a review event, filling the template created by `disensor new`, or fixing errors reported by `disensor validate`.
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
"""
|
|
58
|
+
|
|
58
59
|
WORKFLOW = f"""# Generated by disensor init. The gate validates the .residue/ artifacts of each PR.
|
|
59
60
|
name: disensor
|
|
60
61
|
on:
|
|
@@ -119,6 +120,16 @@ def _write_claude(path: Path, section: str, label: str, report: list[str]) -> No
|
|
|
119
120
|
report.append(f"created {label}")
|
|
120
121
|
|
|
121
122
|
|
|
123
|
+
def _write_skill(base: Path, label: str, report: list[str]) -> None:
|
|
124
|
+
path = base / ".claude" / "skills" / "disensor" / "SKILL.md"
|
|
125
|
+
if path.exists():
|
|
126
|
+
report.append(f"kept {label} (already exists)")
|
|
127
|
+
return
|
|
128
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
129
|
+
path.write_text(SKILL_FRONTMATTER + guide_text(), encoding="utf-8")
|
|
130
|
+
report.append(f"created {label}")
|
|
131
|
+
|
|
132
|
+
|
|
122
133
|
def _write_workflow(root: Path, report: list[str]) -> None:
|
|
123
134
|
path = root / ".github" / "workflows" / "disensor.yml"
|
|
124
135
|
if path.exists():
|
|
@@ -139,16 +150,21 @@ def main_init(args) -> int:
|
|
|
139
150
|
_write_config(root, args.level, report)
|
|
140
151
|
|
|
141
152
|
if args.claude_global:
|
|
153
|
+
home = Path.home()
|
|
142
154
|
_write_claude(
|
|
143
|
-
|
|
155
|
+
home / ".claude" / "CLAUDE.md",
|
|
144
156
|
GLOBAL_GUARD + CLAUDE_SECTION,
|
|
145
157
|
"~/.claude/CLAUDE.md (global)",
|
|
146
158
|
report,
|
|
147
159
|
)
|
|
160
|
+
if not args.no_skill:
|
|
161
|
+
_write_skill(home, "~/.claude/skills/disensor/SKILL.md (global)", report)
|
|
148
162
|
elif not args.no_claude:
|
|
149
163
|
_write_claude(root / "CLAUDE.md", CLAUDE_SECTION, "CLAUDE.md", report)
|
|
164
|
+
if not args.no_skill:
|
|
165
|
+
_write_skill(root, ".claude/skills/disensor/SKILL.md", report)
|
|
150
166
|
else:
|
|
151
|
-
report.append("skipped CLAUDE.md (--no-claude)")
|
|
167
|
+
report.append("skipped CLAUDE.md and skill (--no-claude)")
|
|
152
168
|
|
|
153
169
|
if args.no_workflow:
|
|
154
170
|
report.append("skipped .github/workflows/disensor.yml (--no-workflow)")
|
|
@@ -160,6 +176,8 @@ def main_init(args) -> int:
|
|
|
160
176
|
print(
|
|
161
177
|
"\nNext: the adversarial loop needs a reviewer from another model family "
|
|
162
178
|
"(rule R4). Close each review round with `disensor new`, fill in the "
|
|
163
|
-
"artifact
|
|
179
|
+
"artifact (the skill or `disensor guide` explains every field, and "
|
|
180
|
+
"`disensor hash` computes prompt_hash), and `disensor validate` it "
|
|
181
|
+
"before committing."
|
|
164
182
|
)
|
|
165
183
|
return 0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: disensor
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Adversarial plan & code review with a declared residue. Emits, validates and CI-enforces residue declarations (residue/v0.2 schema).
|
|
5
5
|
Author-email: Nicolas Rocchia <nicolasrocchia@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -28,7 +28,7 @@ Paper del método: Rocchia, N. (2026), *Desacuerdo controlado: revisión adversa
|
|
|
28
28
|
|
|
29
29
|
- `spec/residue.schema.json`: el esquema del artefacto (JSON Schema 2020-12), versión residue/v0.2.
|
|
30
30
|
- `spec/examples/`: tres artefactos de ejemplo, incluido un evento real anonimizado y el perfil minimizado sin texto libre.
|
|
31
|
-
- `src/disensor/`: paquete Python con el validador (reglas R0 a R10), el gate de CI (chequeos G1 a G5), el render del comentario de PR, el scaffolding de artefactos y el de repositorios (`init`).
|
|
31
|
+
- `src/disensor/`: paquete Python con el validador (reglas R0 a R10), el gate de CI (chequeos G1 a G5), el render del comentario de PR, el scaffolding de artefactos y el de repositorios (`init`), y la guía de llenado empaquetada (`GUIDE.md`).
|
|
32
32
|
- `action.yml`: GitHub Action compuesta, lista para usar.
|
|
33
33
|
- `docs/integracion-claude-code.md`: cómo el flujo real (Claude Code más un revisor de otra familia) emite el artefacto al cierre de cada evento.
|
|
34
34
|
|
|
@@ -39,15 +39,18 @@ El paquete se instala una vez (global); cada repositorio se inicializa una vez:
|
|
|
39
39
|
```bash
|
|
40
40
|
pip install disensor # o pipx install disensor, recomendado para CLIs
|
|
41
41
|
|
|
42
|
-
disensor init # en la raíz del repo: config,
|
|
42
|
+
disensor init # en la raíz del repo: config, CLAUDE.md, skill de llenado y workflow de CI
|
|
43
43
|
disensor new --gate diff --level B # plantilla prellenada en .residue/
|
|
44
44
|
disensor validate .residue/<id>.json # schema + reglas R0 a R10
|
|
45
45
|
disensor gate --no-comment # lo que va a correr CI, en local
|
|
46
|
+
|
|
47
|
+
disensor guide # la guía de llenado, para cualquier agente o humano
|
|
48
|
+
disensor hash consigna.md # el sha256: que pide prompt_hash, sin calcularlo a mano
|
|
46
49
|
```
|
|
47
50
|
|
|
48
51
|
Los subcomandos y flags de la v0.1 en español (`nuevo`, `validar`, `--compuerta`, `--nivel`, `--directorio`, `--sin-comentario`) siguen funcionando como alias.
|
|
49
52
|
|
|
50
|
-
`disensor init` escribe, en forma idempotente, el `disensor.config.json` (el nivel viaja con el código, en un archivo versionado), la sección de cierre de evento en `CLAUDE.md
|
|
53
|
+
`disensor init` escribe, en forma idempotente, el `disensor.config.json` (el nivel viaja con el código, en un archivo versionado), la sección de cierre de evento en `CLAUDE.md`, la skill de Claude Code con la guía completa de llenado (`.claude/skills/disensor/SKILL.md`, cargada a demanda al cerrar cada ronda) y el workflow del gate; lo que ya existe se respeta y se informa. El principio es que después de `pip install disensor` y `disensor init` el usuario no toque nada a mano: Claude sabe cuándo (CLAUDE.md) y cómo (la skill), cualquier otro agente recibe lo mismo con `disensor guide`, y el CI hace cumplir el resultado. Config resultante:
|
|
51
54
|
|
|
52
55
|
```json
|
|
53
56
|
{
|
|
@@ -120,7 +123,7 @@ Migración desde v0.1: renombrar `.residuo/` a `.residue/`, las claves del confi
|
|
|
120
123
|
|
|
121
124
|
## Estado
|
|
122
125
|
|
|
123
|
-
v0.
|
|
126
|
+
v0.3, borrador en uso. El esquema sigue en residue/v0.2 (v0.3 no lo toca: agrega la skill de llenado, `disensor guide` y `disensor hash`). Decisión cerrada en v0.2: claves del esquema y CLI en inglés (el español queda como alias en la CLI y como idioma de la documentación). El esquema puede cambiar hasta v1.0; los cambios se declaran en el propio esquema. Decisión abierta antes de v1.0: licencia definitiva (hoy MIT; Apache-2.0 está en consideración por la concesión de patentes antes del release público).
|
|
124
127
|
|
|
125
128
|
## Licencia
|
|
126
129
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
LICENSE
|
|
2
2
|
README.md
|
|
3
3
|
pyproject.toml
|
|
4
|
+
src/disensor/GUIDE.md
|
|
4
5
|
src/disensor/__init__.py
|
|
5
6
|
src/disensor/__main__.py
|
|
6
7
|
src/disensor/cli.py
|
|
7
8
|
src/disensor/gate.py
|
|
9
|
+
src/disensor/guide.py
|
|
8
10
|
src/disensor/init.py
|
|
9
11
|
src/disensor/render.py
|
|
10
12
|
src/disensor/residue.schema.json
|
|
@@ -17,6 +19,7 @@ src/disensor.egg-info/dependency_links.txt
|
|
|
17
19
|
src/disensor.egg-info/entry_points.txt
|
|
18
20
|
src/disensor.egg-info/requires.txt
|
|
19
21
|
src/disensor.egg-info/top_level.txt
|
|
22
|
+
tests/test_guide.py
|
|
20
23
|
tests/test_init.py
|
|
21
24
|
tests/test_rules.py
|
|
22
25
|
tests/test_vectors.py
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Tests of `disensor guide` and `disensor hash`: the no-hands helpers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from disensor.cli import build_parser
|
|
8
|
+
from disensor.guide import guide_text
|
|
9
|
+
|
|
10
|
+
PROMPT_HASH_PATTERN = re.compile(r"^sha256:[0-9a-f]{64}$")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run(capsys, *argv: str) -> str:
|
|
14
|
+
args = build_parser().parse_args(list(argv))
|
|
15
|
+
assert args.func(args) == 0
|
|
16
|
+
return capsys.readouterr().out
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_guide_prints_packaged_text(capsys):
|
|
20
|
+
out = run(capsys, "guide")
|
|
21
|
+
assert out == guide_text()
|
|
22
|
+
assert "residue/v0.2" in out and "R4" in out and "disensor hash" in out
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_hash_of_file_matches_hashlib_and_schema_pattern(tmp_path, capsys):
|
|
26
|
+
f = tmp_path / "consigna.md"
|
|
27
|
+
f.write_bytes(b'{"prueba":"recibo"}')
|
|
28
|
+
out = run(capsys, "hash", str(f)).strip()
|
|
29
|
+
assert out == "sha256:" + hashlib.sha256(b'{"prueba":"recibo"}').hexdigest()
|
|
30
|
+
assert PROMPT_HASH_PATTERN.match(out)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_hash_of_text(capsys):
|
|
34
|
+
out = run(capsys, "hash", "--text", "consigna adversarial v3").strip()
|
|
35
|
+
expected = hashlib.sha256("consigna adversarial v3".encode("utf-8")).hexdigest()
|
|
36
|
+
assert out == f"sha256:{expected}"
|
|
@@ -30,24 +30,25 @@ def test_init_scaffolds_everything(repo, monkeypatch):
|
|
|
30
30
|
assert config == {"criticality_level": "B", "level_A_enabled": False}
|
|
31
31
|
claude = (repo / "CLAUDE.md").read_text(encoding="utf-8")
|
|
32
32
|
assert CLAUDE_HEADING in claude and "disensor validate" in claude
|
|
33
|
+
skill = (repo / ".claude" / "skills" / "disensor" / "SKILL.md").read_text(encoding="utf-8")
|
|
34
|
+
assert skill.startswith("---\nname: disensor\n")
|
|
35
|
+
assert "final_state" in skill and "R4" in skill # the full guide travels in the skill
|
|
33
36
|
workflow = (repo / ".github" / "workflows" / "disensor.yml").read_text(encoding="utf-8")
|
|
34
37
|
assert f"NicolasRocchia/disensor@v{__version__}" in workflow
|
|
35
38
|
assert "fetch-depth: 0" in workflow
|
|
36
39
|
|
|
37
40
|
|
|
38
41
|
def test_init_is_idempotent(repo, monkeypatch):
|
|
42
|
+
pieces = [
|
|
43
|
+
repo / "disensor.config.json",
|
|
44
|
+
repo / "CLAUDE.md",
|
|
45
|
+
repo / ".claude" / "skills" / "disensor" / "SKILL.md",
|
|
46
|
+
repo / ".github" / "workflows" / "disensor.yml",
|
|
47
|
+
]
|
|
39
48
|
run_init(repo, monkeypatch)
|
|
40
|
-
before = {
|
|
41
|
-
p.name: p.read_text(encoding="utf-8")
|
|
42
|
-
for p in [repo / "disensor.config.json", repo / "CLAUDE.md",
|
|
43
|
-
repo / ".github" / "workflows" / "disensor.yml"]
|
|
44
|
-
}
|
|
49
|
+
before = {str(p): p.read_text(encoding="utf-8") for p in pieces}
|
|
45
50
|
run_init(repo, monkeypatch)
|
|
46
|
-
after = {
|
|
47
|
-
p.name: p.read_text(encoding="utf-8")
|
|
48
|
-
for p in [repo / "disensor.config.json", repo / "CLAUDE.md",
|
|
49
|
-
repo / ".github" / "workflows" / "disensor.yml"]
|
|
50
|
-
}
|
|
51
|
+
after = {str(p): p.read_text(encoding="utf-8") for p in pieces}
|
|
51
52
|
assert before == after
|
|
52
53
|
|
|
53
54
|
|
|
@@ -69,9 +70,16 @@ def test_init_flags_skip_pieces(repo, monkeypatch):
|
|
|
69
70
|
assert json.loads((repo / "disensor.config.json").read_text(encoding="utf-8"))[
|
|
70
71
|
"criticality_level"] == "C"
|
|
71
72
|
assert not (repo / "CLAUDE.md").exists()
|
|
73
|
+
assert not (repo / ".claude").exists() # --no-claude also skips the skill
|
|
72
74
|
assert not (repo / ".github").exists()
|
|
73
75
|
|
|
74
76
|
|
|
77
|
+
def test_init_no_skill_keeps_claude_section(repo, monkeypatch):
|
|
78
|
+
run_init(repo, monkeypatch, "--no-skill")
|
|
79
|
+
assert CLAUDE_HEADING in (repo / "CLAUDE.md").read_text(encoding="utf-8")
|
|
80
|
+
assert not (repo / ".claude").exists()
|
|
81
|
+
|
|
82
|
+
|
|
75
83
|
def test_init_claude_global_is_guarded(repo, monkeypatch, tmp_path_factory):
|
|
76
84
|
home = tmp_path_factory.mktemp("home")
|
|
77
85
|
monkeypatch.setenv("HOME", str(home))
|
|
@@ -81,7 +89,9 @@ def test_init_claude_global_is_guarded(repo, monkeypatch, tmp_path_factory):
|
|
|
81
89
|
content = global_md.read_text(encoding="utf-8")
|
|
82
90
|
assert "disensor.config.json" in content.splitlines()[0] or "ONLY inside repositories" in content
|
|
83
91
|
assert CLAUDE_HEADING in content
|
|
92
|
+
assert (home / ".claude" / "skills" / "disensor" / "SKILL.md").exists()
|
|
84
93
|
assert not (repo / "CLAUDE.md").exists()
|
|
94
|
+
assert not (repo / ".claude").exists()
|
|
85
95
|
|
|
86
96
|
|
|
87
97
|
def test_init_warns_on_v01_config(repo, monkeypatch, capsys):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|