task-pipeline-skill 1.73.0 → 1.74.0
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.
- package/CHANGELOG.md +113 -0
- package/README.md +1 -0
- package/SKILL-CARD.md +1 -1
- package/package.json +4 -3
- package/plugins/task-pipeline/.claude-plugin/plugin.json +1 -1
- package/plugins/task-pipeline/agents/verifier-product.md +96 -0
- package/plugins/task-pipeline/agents/verifier-seam.md +98 -0
- package/plugins/task-pipeline/agents/verifier-unit.md +86 -0
- package/plugins/task-pipeline/agents/verifier.md +9 -0
- package/plugins/task-pipeline/skills/task-pipeline/SKILL.md +1 -0
- package/plugins/task-pipeline/skills/task-pipeline/graph.schema.json +84 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/certification.md +146 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/portability.md +1 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/stages.md +6 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/work-graph.md +3 -0
- package/plugins/task-pipeline/skills/task-pipeline/scripts/graph.py +311 -2
- package/plugins/task-pipeline/skills/task-pipeline/templates/run.md +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,118 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.74.0 — 2026-08-20 — a node is closed by three readings, not one
|
|
4
|
+
|
|
5
|
+
**A verifier reads the diff it was handed, and that is the definition of its context, not a
|
|
6
|
+
shortcoming.** It also means a class of defect is invisible to it by construction: the change is
|
|
7
|
+
correct where it was made and a caller's contract moved under it; a second implementation of the
|
|
8
|
+
same rule did not get the fix; a documented behaviour is now false and the document still reads
|
|
9
|
+
as true; another feature reaches the same path and nobody considered the interaction. None of
|
|
10
|
+
those is a bug in the changed lines. All of them ship.
|
|
11
|
+
|
|
12
|
+
A node is now closed by **three independent readings at escalating visibility**, and the run may
|
|
13
|
+
not advance until all three pass.
|
|
14
|
+
|
|
15
|
+
| Tier | Subject | Characteristic finding |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `unit` | the changed functions, classes and branches, plus the node's own `check` | a branch nothing exercises; a boundary that moved |
|
|
18
|
+
| `seam` | everything that can reach the change — callers, callees, implementors, shared state, the neighbours' tests | a contract that moved under a dependent; the duplicate that did not get the fix |
|
|
19
|
+
| `product` | documentation, scenarios, user-visible strings, the neighbouring features sharing this path | a documented behaviour that is now false; an interaction nobody listed |
|
|
20
|
+
|
|
21
|
+
**Blind is the design, not a detail.** The three dispatch in parallel and no tier reads another's
|
|
22
|
+
report, because three readings that inform each other are one opinion with three signatures — and
|
|
23
|
+
the failure is specific: an agent that has just read a convincing account of the implementation
|
|
24
|
+
will paraphrase it back as product truth. `certify` refuses a report whose prose cites another
|
|
25
|
+
tier's verdict.
|
|
26
|
+
|
|
27
|
+
**`certify` is a gate in front of `close`, and `close`'s contract is unchanged.** It takes one
|
|
28
|
+
report per tier, requires all three to pass, and assembles the same seven-key verdict `close`
|
|
29
|
+
already consumed — then runs that verdict through `close`'s own `verdict_violations` before
|
|
30
|
+
writing it, so a certification cannot hand the run a verdict its consumer refuses. No field is
|
|
31
|
+
used for something it does not mean: `confirms` → `done`, `not_examined` → `not_verified`, a
|
|
32
|
+
`risk` finding → a blocker with `can_continue_around: true`.
|
|
33
|
+
|
|
34
|
+
**Two rules give a pass its meaning.** A tier cannot pass on an empty `scope` — a report naming
|
|
35
|
+
nothing it read is a rubber stamp, and three rubber stamps cost three times one verifier while
|
|
36
|
+
reading as three times the assurance, which is strictly worse than what they replaced. And a tier
|
|
37
|
+
cannot pass while carrying a `breaks` finding. There are two severities and no third, because a
|
|
38
|
+
certification that admits a maybe admits everything.
|
|
39
|
+
|
|
40
|
+
**The fix cycle records itself.** A failing round leaves the node open and writes its round number,
|
|
41
|
+
this round's three verdicts and the whole history onto the node — on failure too, because a failing
|
|
42
|
+
round that wrote nothing would erase the only evidence that a node is churning. Every `breaks`
|
|
43
|
+
finding carries the `check` that will prove its fix, so it becomes a node the next round can close.
|
|
44
|
+
At the ceiling (`--ceiling`, default 3) the gate **measures rather than stops**: it names the tier
|
|
45
|
+
that has failed *every* round, because the same tier failing three times is a planning defect
|
|
46
|
+
wearing a verification failure's clothes, and different tiers each round is churn across levels.
|
|
47
|
+
|
|
48
|
+
### The verification of this change, and what it caught in itself
|
|
49
|
+
|
|
50
|
+
`test/certify_mutations.py` disables each of the gate's sixteen rules in a copy of the tree and
|
|
51
|
+
requires a fixture whose name begins `certify:` to notice. **16 of 16 noticed.** It is wired into
|
|
52
|
+
`test:all`, and it exists because the first two attempts at this pass were both wrong:
|
|
53
|
+
|
|
54
|
+
* The first reported **11 of 11 killed** and proved nothing — every mutant had died of the same
|
|
55
|
+
unrelated fixture, because the copy has no `.git` and one pre-existing case checks the commit
|
|
56
|
+
stamp. A mutant is killed only by a `certify:` fixture now.
|
|
57
|
+
* Fixing that reported **0 of 16** and explained why: the twenty new fixtures had been inserted
|
|
58
|
+
**below** `graph_test.py`'s `if failures: sys.exit(1)`, so any earlier failure skipped all of
|
|
59
|
+
them. Locally everything was green and they ran; in CI, on the run where they mattered, they
|
|
60
|
+
would not have. The block moved above the summary, and the mutation pass now asserts its own
|
|
61
|
+
control — that exactly twenty ran and none was red — before reporting a single result.
|
|
62
|
+
|
|
63
|
+
149 graph fixtures, 20 of them new. `references/certification.md` is the doctrine; the three
|
|
64
|
+
agents ship beside `verifier.md`, which now points at them.
|
|
65
|
+
|
|
66
|
+
### The first live dispatch, and what it caught
|
|
67
|
+
|
|
68
|
+
The fixtures prove the **gate**. Whether the shipped agent prose yields a usable report is a live
|
|
69
|
+
dispatch, so one was run: all three tiers against `sheleg-design@f88c14b`, a strict reduced-motion
|
|
70
|
+
check that had shipped green through a single verdict hours earlier.
|
|
71
|
+
|
|
72
|
+
All three returned a well-formed eight-key report. **Two returned `fail`, with five `breaks`
|
|
73
|
+
findings between them.** The unit tier ran the node's check in an isolated worktree at the
|
|
74
|
+
certified commit, ran its own negative control, and put a cross-level observation in
|
|
75
|
+
`not_examined` rather than claiming it — which is the instruction that keeps the levels apart.
|
|
76
|
+
|
|
77
|
+
What the independence bought, stated as measurements rather than as a claim about the design:
|
|
78
|
+
|
|
79
|
+
* the **seam** tier instrumented the gate's `check()` calls and found the ratchet prices the
|
|
80
|
+
*exception* rather than the rule — collapsing the four durations the requirement names as its
|
|
81
|
+
first remediation dropped the count by 4 against its floor and turned the suite red on the
|
|
82
|
+
stricter answer
|
|
83
|
+
* the **product** tier planted a defect and watched it pass: a duration whose token name carries
|
|
84
|
+
no duration word was outside the walk entirely, and thirteen such tokens across ten layers were
|
|
85
|
+
clean by habit rather than by the check
|
|
86
|
+
* **both outer tiers independently** found a propagation-matrix row still stating the obligation
|
|
87
|
+
the change had replaced — two blind readings converging is the signal that they are reading
|
|
88
|
+
different things
|
|
89
|
+
|
|
90
|
+
None of the five is a bug in the changed lines, which is the class this gate was built for. They
|
|
91
|
+
are fixed in `sheleg-design@874ba17`; four further findings are filed there as decisions rather
|
|
92
|
+
than repairs. The one about the author is filed too: every ratchet floor set that day sits below
|
|
93
|
+
its true count, because the sequence was measure, keep editing, restate.
|
|
94
|
+
|
|
95
|
+
Guards: 412 → **412**, and the flatness is the honest number — written in that exact shape
|
|
96
|
+
because the guard reads it: this release adds no validator
|
|
97
|
+
negative, because every rule it adds lives in `scripts/graph.py` rather than in `validate.py`, and
|
|
98
|
+
`.github/workflows/validate.yml` plants defects for the second. The certification's own negative
|
|
99
|
+
control is `npm run test:certify` — **16 mutations, 16 noticed** — wired into `test:all` beside
|
|
100
|
+
the 14 property checks. A guard count that rose here would be a number borrowed from a suite that
|
|
101
|
+
never ran.
|
|
102
|
+
|
|
103
|
+
**The shape of that line is load-bearing, and this is the second time it has bitten.** The
|
|
104
|
+
v1.39.0 entry wrote the count with no colon, the pattern missed, and `npm test` was green over
|
|
105
|
+
a number it had never read. This release first wrote it with the bold around the whole phrase
|
|
106
|
+
instead of around the second number, and the same guard went silent again. The readable shape is
|
|
107
|
+
`Guards: N → **M**`, and the negative test that plants a stale count is the only reason either
|
|
108
|
+
miss was caught.
|
|
109
|
+
|
|
110
|
+
**Neither wrong form is reproduced here, and that is deliberate.** Writing the bad example with
|
|
111
|
+
real digits makes it a second readable count in this section — which is exactly what happened on
|
|
112
|
+
the first attempt: the plant removed the real count, the narrative about the mistake still
|
|
113
|
+
matched, and the guard stayed silent over a section that no longer stated anything. A document
|
|
114
|
+
that quotes a form as an example is indistinguishable from the form itself.
|
|
115
|
+
|
|
3
116
|
## v1.73.0 — 2026-08-20 — the registry could not see the templates, the scripts, or its own registers
|
|
4
117
|
|
|
5
118
|
|
package/README.md
CHANGED
|
@@ -154,6 +154,7 @@ until it is installed.
|
|
|
154
154
|
| 3 Spec | [`spec.md`](plugins/task-pipeline/skills/task-pipeline/references/spec.md) — UX-track order, locked contracts, global constraints, self-review |
|
|
155
155
|
| 4 Plan | [`planning.md`](plugins/task-pipeline/skills/task-pipeline/references/planning.md) — zero-context tasks, parallel groups, no placeholders |
|
|
156
156
|
| the queue | [`work-graph.md`](plugins/task-pipeline/skills/task-pipeline/references/work-graph.md) — a script walks the graph so the model never reads it: 400 nodes and 4 print the same 27-byte frontier |
|
|
157
|
+
| closing a node | [`certification.md`](plugins/task-pipeline/skills/task-pipeline/references/certification.md) — three blind readings at escalating visibility (the changed code, what reaches it, the product around it), all three required to pass; a failing round records itself and the node stays open |
|
|
157
158
|
| 5 Build | [`build.md`](plugins/task-pipeline/skills/task-pipeline/references/build.md) + [`review.md`](plugins/task-pipeline/skills/task-pipeline/references/review.md) — isolation, ledger, subagent loop, review rubric, fix loop |
|
|
158
159
|
| 5–6 TDD | [`tdd.md`](plugins/task-pipeline/skills/task-pipeline/references/tdd.md) — the iron law, red/green/refactor, the suite gate |
|
|
159
160
|
| 5, 6, 8 The browser | [`browser.md`](plugins/task-pipeline/skills/task-pipeline/references/browser.md) — the ref model both channels share, the four commands the look is made of, sessions, and the three different things *"tested in a browser"* means |
|
package/SKILL-CARD.md
CHANGED
|
@@ -12,7 +12,7 @@ harmless.
|
|
|
12
12
|
|---|---|
|
|
13
13
|
| **Purpose** | Runs a substantial task through ten gated delivery stages — intake grill, docs study, brainstorm, spec, plan, subagent build, tests, lint/deploy, post-deploy, docs+registers, acceptance — refusing to advance until each gate passes |
|
|
14
14
|
| **Owner** | ssheleg ([github.com/ssheleg/task-pipeline](https://github.com/ssheleg/task-pipeline)) |
|
|
15
|
-
| **Version** | 1.
|
|
15
|
+
| **Version** | 1.74.0 |
|
|
16
16
|
| **Surface** | Claude Code (filesystem skill + plugin) and the vercel `skills` CLI. **Not** uploaded to the Skills API; custom Skills do not sync across surfaces |
|
|
17
17
|
| **Dependencies** | None required. Optional: `context7` (MCP), `figma` (MCP), super-ux, agent-sync, graphify, obsidian-wiki, and **one of two browser channels** — `playwright` (CLI or MCP) or `chrome-devtools` (MCP); either satisfies the browser step and neither is required. Every stage's doctrine ships in-repo; the one conditional requirement is super-ux for the stage-3 UX track on a user-facing task |
|
|
18
18
|
| **Evaluation status** | Suite authored, 5 categories. One recorded run, **self-observed by the author**; **zero blind runs on zero of three models** — the split, and the numbers, live in [`evals/RESULTS.md`](evals/RESULTS.md) and are computed by `evals/run.py` |
|
package/package.json
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "task-pipeline-skill",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.74.0",
|
|
4
4
|
"description": "Full-cycle delivery pipeline for coding agents: a mandatory built-in intake grill, then 10 gated stages (docs, brainstorm+decompose, spec, plan, build, tests, lint/deploy, post-deploy, docs/wiki, acceptance). Every stage's doctrine ships inside the skill — no companion plugin required. This package is the installer CLI.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"task-pipeline": "bin/task-pipeline.js"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
9
|
"test": "python3 test/validate.py && python3 test/graph_test.py",
|
|
10
|
-
"test:all": "python3 test/validate.py && python3 test/graph_test.py && python3 test/negatives.py && npm run test:exposure && npm run test:probe && npm run test:hooks && npm run test:artifacts && npm run test:docs",
|
|
10
|
+
"test:all": "python3 test/validate.py && python3 test/graph_test.py && python3 test/negatives.py && npm run test:certify && npm run test:exposure && npm run test:probe && npm run test:hooks && npm run test:artifacts && npm run test:docs",
|
|
11
11
|
"test:negatives": "python3 test/negatives.py",
|
|
12
12
|
"test:exposure": "python3 test/exposure_test.py",
|
|
13
13
|
"test:probe": "python3 test/probe.py --self-test",
|
|
14
14
|
"test:hooks": "python3 test/release_gate_test.py",
|
|
15
15
|
"test:artifacts": "python3 test/artifact_root_test.py && python3 test/migrate_artifacts_test.py",
|
|
16
|
-
"test:docs": "bash plugins/task-pipeline/skills/task-pipeline/templates/docgate.sh"
|
|
16
|
+
"test:docs": "bash plugins/task-pipeline/skills/task-pipeline/templates/docgate.sh",
|
|
17
|
+
"test:certify": "python3 test/certify_mutations.py"
|
|
17
18
|
},
|
|
18
19
|
"files": [
|
|
19
20
|
"bin",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "task-pipeline",
|
|
3
3
|
"displayName": "Task Pipeline",
|
|
4
4
|
"description": "Runs a substantial task through a mandatory built-in intake grill, then 10 gated stages (docs, brainstorm+decompose, spec, plan, subagent build, tests, lint/deploy, post-deploy, docs/wiki, acceptance). Every stage's doctrine is built into the skill — no companion plugin required — with typed auto/manual gates, a frozen requirement spine that closes with evidence, a work board and a verification ledger that outlive a run, an exposure line naming what shipped unconfirmed, a progress rail computed from the project's own config, a loop guard whose review ceiling measures rather than stops, and stage-3 tracks for what a product does, how it sounds and how it looks. Two modes need no task: `checkup` (what is unverified) and `setup` (audit existing docs). Retro insights can publish upstream as issues, opt-in and redacted.",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.74.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "ssheleg",
|
|
8
8
|
"url": "https://x.com/sshlg93"
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: verifier-product
|
|
3
|
+
description: Tier 3 of a three-tier certification. Reads the documentation, the scenarios and the neighbouring features, and reports whether the change leaves any documented or user-facing behaviour false, and whether it collides with another feature that shares the path. Returns an eight-key tier report. Use as one of three blind readings when a task-pipeline node claims to be finished. Not for line-level correctness, which is the unit tier, and not for call graphs, which is the seam tier.
|
|
4
|
+
model: inherit
|
|
5
|
+
tools: Read, Grep, Glob, Bash
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Product tier — what the product now says, and whether it is still true
|
|
9
|
+
|
|
10
|
+
You are the **furthest** of three readings. One agent has the diff and one has the
|
|
11
|
+
call graph; you will see neither report and neither will see yours. Blind is the
|
|
12
|
+
point, and at this distance it matters most: an agent that has just read a
|
|
13
|
+
convincing account of the implementation will paraphrase it back as product truth.
|
|
14
|
+
|
|
15
|
+
Your subject is **behaviour and what claims it.** Documentation, scenarios, the
|
|
16
|
+
changelog, ADRs, the runbook, the strings a user reads, and the other features that
|
|
17
|
+
share this path. You may open code to confirm a behaviour — but code is your
|
|
18
|
+
evidence, never your scope. If your report describes functions, you have written a
|
|
19
|
+
third unit-tier report and the level the user built this gate for went unread.
|
|
20
|
+
|
|
21
|
+
## Read the claims before you judge the change
|
|
22
|
+
|
|
23
|
+
1. **The requirement, in the words the product uses.** The node's `serves` is the
|
|
24
|
+
standard; the product's own wording of it is the thing that must still hold.
|
|
25
|
+
2. **The documents that describe this behaviour.** `docs/`, `README`, the
|
|
26
|
+
changelog, `CONTEXT.md`, ADRs, the runbook. Where `docs/ux/scenarios.md` exists
|
|
27
|
+
it is the source of truth for user-facing behaviour — `super-ux` owns it, and a
|
|
28
|
+
change to user-facing behaviour is supposed to update it in the same change.
|
|
29
|
+
3. **The strings a person sees.** An error message, an empty state, a label, an
|
|
30
|
+
email. A behaviour change that leaves the old wording in place is a lie shipped
|
|
31
|
+
in the product's own voice.
|
|
32
|
+
4. **The neighbouring features that share this path.** Not the callers — the
|
|
33
|
+
*features*. Two flows that both end at this behaviour, a report that counts
|
|
34
|
+
these events, an export, a webhook, an admin screen, a metric on a dashboard.
|
|
35
|
+
5. **The interaction the change creates.** What is now possible that was not, and
|
|
36
|
+
what does the rest of the product do when it happens? A new state usually has to
|
|
37
|
+
be handled in three places nobody listed: a list view, a filter, and a total.
|
|
38
|
+
|
|
39
|
+
## The six things that are true at this level and invisible below it
|
|
40
|
+
|
|
41
|
+
- **A documented behaviour is now false.** The commonest, and the cheapest to fix
|
|
42
|
+
in the same change.
|
|
43
|
+
- **A scenario no longer holds** — the steps still describe the old path.
|
|
44
|
+
- **A user-visible change nobody wrote down.** It shipped, and support will find it.
|
|
45
|
+
- **A second feature reaches the same behaviour** and was not considered.
|
|
46
|
+
- **A number the product reports moves** — a count, a total, a metric — because the
|
|
47
|
+
set it counts changed.
|
|
48
|
+
- **A migration or a mixed state.** Data written before the change, in-flight
|
|
49
|
+
requests, a cached response, a client on the old version.
|
|
50
|
+
|
|
51
|
+
## `breaks` or `risk`
|
|
52
|
+
|
|
53
|
+
- **`breaks`** — the product now claims something untrue, or a documented behaviour
|
|
54
|
+
or scenario is contradicted, or another feature is broken by the interaction.
|
|
55
|
+
Carries a `check` that will prove the fix. *"Documentation ships in the same
|
|
56
|
+
change as the code; in the next ticket it never ships at all."*
|
|
57
|
+
- **`risk`** — a claim you cannot resolve without the operator, a metric that
|
|
58
|
+
probably moves, a mixed-state window that probably closes on its own. It ships,
|
|
59
|
+
named, and reaches the closing verdict as a blocker the run can continue around.
|
|
60
|
+
|
|
61
|
+
## The report — all eight keys, and `[]` is an answer
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"node": "N-007",
|
|
66
|
+
"tier": "product",
|
|
67
|
+
"verdict": "fail",
|
|
68
|
+
"scope": ["docs/ux/scenarios.md:S-04 — the checkout decline flow",
|
|
69
|
+
"README.md:120-140 — 'a declined card raises PaymentError'",
|
|
70
|
+
"the two features that reach this behaviour: checkout, and the retry job",
|
|
71
|
+
"the admin Payments list, which filters on state"],
|
|
72
|
+
"confirms": ["scenario S-04's steps still describe what the product does"],
|
|
73
|
+
"findings": [{ "what": "the README documents an exception the product no longer raises",
|
|
74
|
+
"where": "README.md:131", "severity": "breaks",
|
|
75
|
+
"fix": "state the return value, and note the version it changed in",
|
|
76
|
+
"check": "judgement — the README's payment section read against the new return path" }],
|
|
77
|
+
"evidence": ["read docs/ux/scenarios.md:S-04 — steps unchanged by this behaviour",
|
|
78
|
+
"grep -rn 'PaymentError' docs README.md → 2 hits, both in the payment section"],
|
|
79
|
+
"not_examined": ["whether the finance export counts declines, which needs the operator"]
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**A `check` may be a judgement here, and it is written as one** — named, not dressed
|
|
84
|
+
as an exit code. `references/gates.md` says which is which; recording a judgement as
|
|
85
|
+
a command is how a document acquires proof it never had.
|
|
86
|
+
|
|
87
|
+
## Three ways this goes wrong
|
|
88
|
+
|
|
89
|
+
| Temptation | Why it is wrong |
|
|
90
|
+
|---|---|
|
|
91
|
+
| «The implementation is sound» | Not your level, and you are the agent least equipped to say it. Two readings already covered the code with context you do not have |
|
|
92
|
+
| «No docs mention this» | Then your `scope` names the searches that found none, and *nothing documents a user-facing behaviour* is itself a finding |
|
|
93
|
+
| «Docs can follow in the next ticket» | In the next ticket they never ship. A behaviour change with no document is `breaks` at this tier, and the fix is one paragraph |
|
|
94
|
+
|
|
95
|
+
Doctrine: `references/certification.md`. What proof a document owes:
|
|
96
|
+
`references/documentation.md`. Scenarios: `super-ux`, `docs/ux/scenarios.md`.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: verifier-seam
|
|
3
|
+
description: Tier 2 of a three-tier certification. Reads everything that can REACH the change — callers, callees, implementors, shared state, config, and the neighbours' tests — and reports whether a contract moved under something that depends on it. Returns an eight-key tier report. Use as one of three blind readings when a task-pipeline node claims to be finished. Not for re-reading the diff, which is the unit tier, and not for documentation, which is the product tier.
|
|
4
|
+
model: inherit
|
|
5
|
+
tools: Read, Grep, Glob, Bash
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Seam tier — what depends on the change, and did not change with it
|
|
9
|
+
|
|
10
|
+
You are the **middle** of three readings. One agent is reading the diff itself and
|
|
11
|
+
another is reading the product around it; you will never see either report, and
|
|
12
|
+
they will never see yours. Blind is the point — three readings that inform each
|
|
13
|
+
other are one opinion with three signatures.
|
|
14
|
+
|
|
15
|
+
Your subject is **the seam**: everything that can reach the changed code, and
|
|
16
|
+
everything the changed code can reach. Not the hunks. If your `scope` is mostly
|
|
17
|
+
the same lines the diff shows, you have re-run the unit tier and the middle level
|
|
18
|
+
went unread — which is the level the user's whole reason for this gate lives at.
|
|
19
|
+
|
|
20
|
+
## Find the neighbours before you judge anything
|
|
21
|
+
|
|
22
|
+
You cannot report on dependents you never enumerated, so enumerate first and put
|
|
23
|
+
the enumeration in `scope`:
|
|
24
|
+
|
|
25
|
+
1. **Callers.** Grep the changed symbols across the repository, including tests,
|
|
26
|
+
scripts and configuration. A call site in a fixture is a call site.
|
|
27
|
+
2. **Callees.** What the change now calls that it did not before, and what it
|
|
28
|
+
stopped calling. A dropped call is invisible in a diff read forwards.
|
|
29
|
+
3. **Implementors and subclasses.** If the change touched an interface, a base
|
|
30
|
+
class, a protocol or a duck-typed contract, every implementation is a dependent.
|
|
31
|
+
4. **Shared state.** A module-level constant, a cache, a global, a database column,
|
|
32
|
+
a file on disk, an environment variable, a lock. Two readers of one mutable
|
|
33
|
+
thing are a seam even with no call between them.
|
|
34
|
+
5. **The neighbours' tests.** A test that exercises a caller is your evidence that
|
|
35
|
+
the caller still works — or your finding that nothing covers it.
|
|
36
|
+
6. **A second implementation of the same rule.** The duplicate that did *not* get
|
|
37
|
+
the fix is the single most common thing this tier exists to catch.
|
|
38
|
+
|
|
39
|
+
Where a code graph exists (`graphify-out/graph.json`), it answers *reach* directly
|
|
40
|
+
and grep cannot — `references/knowledge-graph.md`.
|
|
41
|
+
|
|
42
|
+
## The seven contracts that move without anybody noticing
|
|
43
|
+
|
|
44
|
+
- **Signature** — an argument added, reordered, renamed, or made required
|
|
45
|
+
- **Return shape** — a field added or dropped, a list becoming a generator, `None`
|
|
46
|
+
becoming an empty list, a dict becoming an object
|
|
47
|
+
- **Errors** — a new exception a caller does not catch, or an exception replaced by
|
|
48
|
+
a return value the caller reads as success
|
|
49
|
+
- **Nullability** — something that could not be absent now can
|
|
50
|
+
- **Ordering and timing** — a sort dropped, a call moved before or after another,
|
|
51
|
+
an operation that used to be atomic
|
|
52
|
+
- **Units and encoding** — seconds to milliseconds, cents to units, bytes to a
|
|
53
|
+
string, a naive datetime to an aware one
|
|
54
|
+
- **Idempotence and side effects** — a function that could be called twice and now
|
|
55
|
+
cannot, or a write that used to happen once
|
|
56
|
+
|
|
57
|
+
For each one you find, name the **dependent** in `where`, not the change. The
|
|
58
|
+
change is the unit tier's subject; the thing that will break is yours.
|
|
59
|
+
|
|
60
|
+
## `breaks` or `risk`
|
|
61
|
+
|
|
62
|
+
- **`breaks`** — a dependent is now wrong, or a contract moved under one and
|
|
63
|
+
nothing updated it. Carries a `check` that will prove the fix, because the
|
|
64
|
+
finding becomes a node the next round has to close.
|
|
65
|
+
- **`risk`** — a dependent that is *probably* fine and nothing proves it: a caller
|
|
66
|
+
with no test, a duplicate implementation that happens to agree today. It ships,
|
|
67
|
+
and it reaches the closing verdict as a blocker the run can continue around.
|
|
68
|
+
|
|
69
|
+
## The report — all eight keys, and `[]` is an answer
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"node": "N-007",
|
|
74
|
+
"tier": "seam",
|
|
75
|
+
"verdict": "fail",
|
|
76
|
+
"scope": ["3 callers of charge(): api/checkout.py:44, jobs/retry.py:19, tests/test_api.py:120",
|
|
77
|
+
"1 other implementation of the decline rule: legacy/billing.py:301",
|
|
78
|
+
"shared: PAYMENT_STATES in src/pay/states.py, read by both"],
|
|
79
|
+
"confirms": ["both live callers already treat Declined as a value, so the new return path is handled"],
|
|
80
|
+
"findings": [{ "what": "legacy/billing.py still raises on a decline, so the two paths disagree",
|
|
81
|
+
"where": "legacy/billing.py:301", "severity": "breaks",
|
|
82
|
+
"fix": "route legacy through charge() or apply the same rule",
|
|
83
|
+
"check": "pytest tests/test_legacy_decline.py -q" }],
|
|
84
|
+
"evidence": ["grep -rn 'charge(' → 3 call sites, listed in scope",
|
|
85
|
+
"pytest tests/test_api.py -q → 12 passed"],
|
|
86
|
+
"not_examined": ["the retry job's integration test, which needs a broker this box has no access to"]
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Three ways this goes wrong
|
|
91
|
+
|
|
92
|
+
| Temptation | Why it is wrong |
|
|
93
|
+
|---|---|
|
|
94
|
+
| «The diff looks correct» | You were not asked about the diff. Another agent has it, with more context than you, and your saying so leaves the seam unread |
|
|
95
|
+
| «Nothing calls it» | Then say so in `scope` with the search you ran, including tests and config. *Nothing calls it* is a finding when something should |
|
|
96
|
+
| «The callers' tests pass» | Which callers, and did any test exercise the path that moved? A suite that never reached the seam proves the suite ran |
|
|
97
|
+
|
|
98
|
+
Doctrine: `references/certification.md`. Reach: `references/knowledge-graph.md`.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: verifier-unit
|
|
3
|
+
description: Tier 1 of a three-tier certification. Reads only the code that changed — the functions, classes and branches in the diff — runs the node's own check, and reports whether the change does what was asked and breaks nothing inside its own unit. Returns an eight-key tier report. Use as one of three blind readings when a task-pipeline node claims to be finished. Not for reviewing style, and not for looking at callers — that is the seam tier.
|
|
4
|
+
model: inherit
|
|
5
|
+
tools: Read, Grep, Glob, Bash
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Unit tier — the code that changed, and nothing else
|
|
9
|
+
|
|
10
|
+
You are the **closest** of three readings. Two other agents are reading the same
|
|
11
|
+
node one and two levels out, and you will never see their reports. That is
|
|
12
|
+
deliberate: three readings that inform each other are one opinion with three
|
|
13
|
+
signatures, and the disagreement between blind readings is the instrument.
|
|
14
|
+
|
|
15
|
+
Your subject is **the diff and the unit it lands in.** The functions, methods,
|
|
16
|
+
classes and branches that changed, plus whatever in the same file or module the
|
|
17
|
+
change reads and writes. If you find yourself opening a caller in another module
|
|
18
|
+
to decide something, stop — that finding belongs to the seam tier and reporting it
|
|
19
|
+
as yours costs the certification a level.
|
|
20
|
+
|
|
21
|
+
## What you actually do
|
|
22
|
+
|
|
23
|
+
1. **Read the node's `serves`.** The REQ or goal clause is the standard. Not what
|
|
24
|
+
the diff does — what was asked.
|
|
25
|
+
2. **Run the node's `check`.** One command, or the named judgement where no
|
|
26
|
+
command decides it. Its output is your first `evidence` row. Not `npm test`
|
|
27
|
+
when the node named something narrower.
|
|
28
|
+
3. **Read every changed hunk against the requirement**, and then read the parts of
|
|
29
|
+
the unit the hunk touches but the diff does not show — the other branches of the
|
|
30
|
+
same function, the sibling method that shares the field, the error path.
|
|
31
|
+
4. **Look for the five things that break a unit from inside:**
|
|
32
|
+
- a branch the change added that nothing exercises
|
|
33
|
+
- an error or empty path the change routes into differently than before
|
|
34
|
+
- a boundary the change moved (off-by-one, inclusive/exclusive, first/last)
|
|
35
|
+
- state the change mutates that another method in the same class assumes
|
|
36
|
+
- a value the change computes twice, so the two can disagree
|
|
37
|
+
5. **Say what you read.** `scope` is a list of what you actually opened, with
|
|
38
|
+
`file:line` ranges. A pass on an empty `scope` is refused by
|
|
39
|
+
`graph.py certify` by name, as a rubber stamp — and it is right to.
|
|
40
|
+
|
|
41
|
+
## `breaks` or `risk`, and the line is not taste
|
|
42
|
+
|
|
43
|
+
- **`breaks`** — the node is not done. The requirement is unmet, or the change is
|
|
44
|
+
wrong on a path a caller can reach. Every `breaks` finding carries a `check`:
|
|
45
|
+
the command or judgement that will prove the fix, because the finding becomes a
|
|
46
|
+
node the next round has to close.
|
|
47
|
+
- **`risk`** — found, judged survivable, and named. It ships, and it appears in the
|
|
48
|
+
closing verdict as a blocker the run can continue around. Use it for the thing
|
|
49
|
+
you would say in review and would not block on.
|
|
50
|
+
|
|
51
|
+
There is no third value, because a certification that admits a maybe admits
|
|
52
|
+
everything.
|
|
53
|
+
|
|
54
|
+
## The report — all eight keys, and `[]` is an answer
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"node": "N-007",
|
|
59
|
+
"tier": "unit",
|
|
60
|
+
"verdict": "pass",
|
|
61
|
+
"scope": ["src/pay/charge.py:88-140 — charge() and its three branches",
|
|
62
|
+
"src/pay/charge.py:210-232 — _retry, which charge() now calls"],
|
|
63
|
+
"confirms": ["a declined card returns Declined instead of raising, which REQ-004 asked for"],
|
|
64
|
+
"findings": [{ "what": "…", "where": "src/pay/charge.py:131",
|
|
65
|
+
"severity": "breaks", "fix": "…", "check": "…" }],
|
|
66
|
+
"evidence": ["pytest tests/test_charge.py::test_declined -q → 1 passed",
|
|
67
|
+
"read charge() at src/pay/charge.py:88-140"],
|
|
68
|
+
"not_examined": ["the retry backoff constants, which no test in this unit covers"]
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`not_examined` is the field that keeps a pass honest: it is what you saw and could
|
|
73
|
+
not check. It flows into the closing verdict as `not_verified`, which is what
|
|
74
|
+
*present and unchecked* means one level up. An empty list is a valid answer and
|
|
75
|
+
silence is not.
|
|
76
|
+
|
|
77
|
+
## Three ways this goes wrong
|
|
78
|
+
|
|
79
|
+
| Temptation | Why it is wrong |
|
|
80
|
+
|---|---|
|
|
81
|
+
| «The suite is green, so it is done» | The node serves a requirement, not a suite. A green run that never exercised the requirement proves the run happened |
|
|
82
|
+
| «This caller looks wrong too» | Not your level. Say it in `not_examined` if you must, and let the seam tier find it with the context to judge it |
|
|
83
|
+
| «Nothing to report, pass» | Then `scope` says what you read and `confirms` says what is now true. A pass that names neither is the one thing this tier cannot return |
|
|
84
|
+
|
|
85
|
+
Doctrine: `references/certification.md`. The node's own contract:
|
|
86
|
+
`references/work-graph.md`.
|
|
@@ -7,6 +7,15 @@ tools: Read, Grep, Glob, Bash
|
|
|
7
7
|
|
|
8
8
|
# Verifier — accept the work, or say precisely what is missing
|
|
9
9
|
|
|
10
|
+
> **A node is normally closed by three readings, not by this one.**
|
|
11
|
+
> `verifier-unit`, `verifier-seam` and `verifier-product` each report at a
|
|
12
|
+
> different distance, `graph.py certify` requires all three to pass and assembles
|
|
13
|
+
> the verdict below from them — because a change can be correct where it was made
|
|
14
|
+
> and wrong one level out, and a single context cannot see both. Doctrine:
|
|
15
|
+
> `references/certification.md`. This agent remains for the case `certify` does not
|
|
16
|
+
> cover: a verdict already in flight, or a hand-written one fed to
|
|
17
|
+
> `close --verdict` directly. Reach for the three tiers first.
|
|
18
|
+
|
|
10
19
|
You close **one node**. You do not fix, you do not implement, and you do not
|
|
11
20
|
review style. You answer four questions about work that claims to be finished, and
|
|
12
21
|
your answer moves the graph.
|
|
@@ -59,6 +59,7 @@ gate stops until it is installed.
|
|
|
59
59
|
| 3 Spec | `references/spec.md` |
|
|
60
60
|
| 4 Plan | `references/planning.md` |
|
|
61
61
|
| the queue the loop walks | `references/work-graph.md` |
|
|
62
|
+
| 5–8 · how a node is CLOSED — three blind readings at three distances, all three required | `references/certification.md` |
|
|
62
63
|
| 5 Build (worktree, subagents, fix loop) | `references/build.md` + `references/review.md` |
|
|
63
64
|
| 5–6 TDD + suite gate | `references/tdd.md` |
|
|
64
65
|
| 5, 6, 8 The browser — the look, the spec suite, and the difference | `references/browser.md` |
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
"not": {
|
|
118
118
|
"pattern": "[\\n\\r]"
|
|
119
119
|
},
|
|
120
|
-
"description": "**How this node will be closed**
|
|
120
|
+
"description": "**How this node will be closed** — the command a verifier runs, or the named judgement that stands in where no command can (`references/gates.md`'s third gate type: name the judge, record the verdict AS judgement). Its output becomes the node's `evidence` row.\n\nRequired on every node except a `parked` one, and that is the whole point of the field: `agents/verifier.md` tells the verifier to run *the check this node names*, so a node with none leaves it the two options that same paragraph forbids — invent a check, or run everything. Shipped doctrine read a field the schema did not have (B-080); the field the doctrine reads now exists.\n\nOne string, never a list, because the requirement it serves is singular throughout — one input, one job, one output, one owner, **one** completion test. A node needing two unrelated checks is a node doing two jobs, and the answer is to split it rather than to widen this field. One gate made of two commands is `a && b`, which is still one gate.\n\nA `parked` node is exempt because it is the one node nobody will close, and a placeholder there — *n/a, parked* — is the confidence-without-correctness this schema refuses everywhere else. The field survives a park: `park` sets the status and never removes what the node said it would run."
|
|
121
121
|
},
|
|
122
122
|
"evidence": {
|
|
123
123
|
"type": [
|
|
@@ -145,6 +145,89 @@
|
|
|
145
145
|
"pattern": "\\S"
|
|
146
146
|
},
|
|
147
147
|
"description": "What this node MUTATES — paths, register names, remote resource ids. `references/planning.md` states the rule the frontier needs: *distinct is not the same as independent, and the check is what they touch, never what they are called.* That rule lived in the markdown plan, and the graph replaced the plan as the thing deciding what runs next — so `next` could hand two agents two runnable nodes that write the same file, with nothing able to report it.\\n\\nOptional, and its absence is DISCLOSED rather than treated as «touches nothing»: `next` prints how many frontier nodes declared no targets, because a quiet run and a checked one must not look alike."
|
|
148
|
+
},
|
|
149
|
+
"certification": {
|
|
150
|
+
"type": "object",
|
|
151
|
+
"description": "What the three-tier certification recorded for this node. Written by `graph.py certify` on every round, pass or fail: a failing round that wrote nothing would erase the only evidence that a node is churning, which is the number the loop-guard ceiling reads.",
|
|
152
|
+
"additionalProperties": false,
|
|
153
|
+
"required": [
|
|
154
|
+
"round",
|
|
155
|
+
"tiers",
|
|
156
|
+
"at",
|
|
157
|
+
"history"
|
|
158
|
+
],
|
|
159
|
+
"properties": {
|
|
160
|
+
"round": {
|
|
161
|
+
"type": "integer",
|
|
162
|
+
"minimum": 1
|
|
163
|
+
},
|
|
164
|
+
"tiers": {
|
|
165
|
+
"type": "object",
|
|
166
|
+
"additionalProperties": false,
|
|
167
|
+
"required": [
|
|
168
|
+
"unit",
|
|
169
|
+
"seam",
|
|
170
|
+
"product"
|
|
171
|
+
],
|
|
172
|
+
"properties": {
|
|
173
|
+
"unit": {
|
|
174
|
+
"enum": [
|
|
175
|
+
"pass",
|
|
176
|
+
"fail"
|
|
177
|
+
]
|
|
178
|
+
},
|
|
179
|
+
"seam": {
|
|
180
|
+
"enum": [
|
|
181
|
+
"pass",
|
|
182
|
+
"fail"
|
|
183
|
+
]
|
|
184
|
+
},
|
|
185
|
+
"product": {
|
|
186
|
+
"enum": [
|
|
187
|
+
"pass",
|
|
188
|
+
"fail"
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
"at": {
|
|
194
|
+
"type": "string",
|
|
195
|
+
"minLength": 1
|
|
196
|
+
},
|
|
197
|
+
"history": {
|
|
198
|
+
"type": "array",
|
|
199
|
+
"minItems": 1,
|
|
200
|
+
"items": {
|
|
201
|
+
"type": "object",
|
|
202
|
+
"additionalProperties": false,
|
|
203
|
+
"required": [
|
|
204
|
+
"unit",
|
|
205
|
+
"seam",
|
|
206
|
+
"product"
|
|
207
|
+
],
|
|
208
|
+
"properties": {
|
|
209
|
+
"unit": {
|
|
210
|
+
"enum": [
|
|
211
|
+
"pass",
|
|
212
|
+
"fail"
|
|
213
|
+
]
|
|
214
|
+
},
|
|
215
|
+
"seam": {
|
|
216
|
+
"enum": [
|
|
217
|
+
"pass",
|
|
218
|
+
"fail"
|
|
219
|
+
]
|
|
220
|
+
},
|
|
221
|
+
"product": {
|
|
222
|
+
"enum": [
|
|
223
|
+
"pass",
|
|
224
|
+
"fail"
|
|
225
|
+
]
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
148
231
|
}
|
|
149
232
|
},
|
|
150
233
|
"allOf": [
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Certification — three readings at three distances, and all three must pass
|
|
2
|
+
|
|
3
|
+
A node is not closed by one agent's opinion. It is closed by **three independent
|
|
4
|
+
readings at escalating visibility**, and the run may not advance until all three
|
|
5
|
+
pass.
|
|
6
|
+
|
|
7
|
+
## Contents
|
|
8
|
+
|
|
9
|
+
- Why one verifier is not enough, stated as the failure it produces
|
|
10
|
+
- The three tiers
|
|
11
|
+
- Blind, and it is the whole design
|
|
12
|
+
- A pass has to mean something, so two rules have teeth
|
|
13
|
+
- The report, and where each field lands
|
|
14
|
+
- The commands
|
|
15
|
+
- The fix cycle, and its ceiling
|
|
16
|
+
- What this costs, said out loud
|
|
17
|
+
- Rationalizations
|
|
18
|
+
|
|
19
|
+
## Why one verifier is not enough, stated as the failure it produces
|
|
20
|
+
|
|
21
|
+
A verifier reads the diff it was handed. That is not a shortcoming of the agent; it
|
|
22
|
+
is the definition of its context. And it means a whole class of defect is invisible
|
|
23
|
+
to it by construction:
|
|
24
|
+
|
|
25
|
+
- the change is correct where it was made, and a **caller's contract moved** under it
|
|
26
|
+
- a **second implementation of the same rule** did not get the fix
|
|
27
|
+
- a **documented behaviour** is now false, and the document still reads as true
|
|
28
|
+
- **another feature** reaches the same path and nobody considered the interaction
|
|
29
|
+
|
|
30
|
+
None of these is a bug in the changed lines. All of them ship. Each is found by
|
|
31
|
+
looking one level further out than the change — which is a different reading, not a
|
|
32
|
+
longer one, because the context that finds it is the context that excludes the diff.
|
|
33
|
+
|
|
34
|
+
## The three tiers
|
|
35
|
+
|
|
36
|
+
| Tier | Subject | Characteristic finding |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `unit` | the changed functions, classes and branches, plus the node's own `check` | a branch nothing exercises; a boundary that moved |
|
|
39
|
+
| `seam` | everything that can reach the change — callers, callees, implementors, shared state, the neighbours' tests | a contract that moved under a dependent; the duplicate that did not get the fix |
|
|
40
|
+
| `product` | documentation, scenarios, user-visible strings, and the neighbouring features that share this path | a documented behaviour that is now false; an interaction nobody listed |
|
|
41
|
+
|
|
42
|
+
Agents: [`../../../agents/verifier-unit.md`](../../../agents/verifier-unit.md),
|
|
43
|
+
[`verifier-seam.md`](../../../agents/verifier-seam.md),
|
|
44
|
+
[`verifier-product.md`](../../../agents/verifier-product.md).
|
|
45
|
+
|
|
46
|
+
## Blind, and it is the whole design
|
|
47
|
+
|
|
48
|
+
**The three run in parallel and no tier reads another's report.** Three readings
|
|
49
|
+
that inform each other are one opinion with three signatures — and the failure mode
|
|
50
|
+
is specific: an agent that has just read a convincing account of the implementation
|
|
51
|
+
will paraphrase it back as product truth. The disagreement between blind readings is
|
|
52
|
+
the instrument, so `graph.py certify` refuses a report whose prose cites another
|
|
53
|
+
tier's verdict.
|
|
54
|
+
|
|
55
|
+
Dispatch all three in one message so they run concurrently. Give each the node id,
|
|
56
|
+
its `serves`, and the diff — nothing else, and never another tier's output.
|
|
57
|
+
|
|
58
|
+
## A pass has to mean something, so two rules have teeth
|
|
59
|
+
|
|
60
|
+
**A tier cannot pass on an empty `scope`.** `scope` is what the tier actually
|
|
61
|
+
opened, with `file:line`. A report that names nothing it read is a rubber stamp, and
|
|
62
|
+
three rubber stamps cost three times one verifier while reading as three times the
|
|
63
|
+
assurance — strictly worse than the single verdict it replaced. `certify` refuses it
|
|
64
|
+
by name.
|
|
65
|
+
|
|
66
|
+
**A tier cannot pass while carrying a `breaks` finding.** There are two severities
|
|
67
|
+
and no third, because a certification that admits a maybe admits everything:
|
|
68
|
+
|
|
69
|
+
- **`breaks`** — the node is not done. Carries a `check`: the command or judgement
|
|
70
|
+
that will prove the fix, for the same reason `replan.add` does. The finding
|
|
71
|
+
becomes a node the next round has to close, and handing that node the absence is
|
|
72
|
+
how the defect returns one round later.
|
|
73
|
+
- **`risk`** — found, judged survivable, and named. It ships, and it reaches the
|
|
74
|
+
closing verdict as a blocker with `can_continue_around: true`, which is exactly
|
|
75
|
+
what a named survivable finding is.
|
|
76
|
+
|
|
77
|
+
## The report, and where each field lands
|
|
78
|
+
|
|
79
|
+
Eight keys, all required, `[]` a valid answer and silence not one. On a pass
|
|
80
|
+
`certify` assembles the canonical seven-key verdict that
|
|
81
|
+
[`work-graph.md`](work-graph.md) already specifies, and no field is used for
|
|
82
|
+
something it does not mean:
|
|
83
|
+
|
|
84
|
+
| Tier field | Becomes | Because |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| `confirms` | `done` | asked for, and now true |
|
|
87
|
+
| `not_examined` | `not_verified` | present, and no check touched it |
|
|
88
|
+
| `findings` at `risk` | `blockers`, `can_continue_around: true` | found, judged survivable, named |
|
|
89
|
+
| `evidence` | `evidence`, prefixed with the tier | the command and what it printed |
|
|
90
|
+
|
|
91
|
+
`certify` runs the assembled verdict through the same `verdict_violations` gate
|
|
92
|
+
`close` will apply, so a certification cannot hand the run a verdict its own
|
|
93
|
+
consumer refuses.
|
|
94
|
+
|
|
95
|
+
## The commands
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# three reports in, one verdict out — exits 1 if any tier failed
|
|
99
|
+
graph.py certify --node N-007 \
|
|
100
|
+
--tier unit.json --tier seam.json --tier product.json
|
|
101
|
+
|
|
102
|
+
# unchanged, and still the only thing that moves the graph
|
|
103
|
+
graph.py close --verdict .task-pipeline/verdict-N-007.json
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## The fix cycle, and its ceiling
|
|
107
|
+
|
|
108
|
+
A failing round **records itself and leaves the node open.** `certification` on the
|
|
109
|
+
node carries the round number, this round's three verdicts and the history of every
|
|
110
|
+
round — written on failure too, because a failing round that wrote nothing would
|
|
111
|
+
erase the only evidence that a node is churning.
|
|
112
|
+
|
|
113
|
+
The cycle is: `certify` → fail → the `breaks` findings become nodes (each already
|
|
114
|
+
carrying its `check`) → fix → `certify` again, round `N+1`. Same three tiers, same
|
|
115
|
+
blind dispatch. A tier that passed in an earlier round is **re-run**, because the
|
|
116
|
+
fix is a new change and the level it passed on is not the level it now faces.
|
|
117
|
+
|
|
118
|
+
**At the ceiling the gate measures rather than stops** —
|
|
119
|
+
[`loop-guard.md`](loop-guard.md). `--ceiling` defaults to 3. At or over it, `certify`
|
|
120
|
+
still runs and still reports the tiers; what it adds is the name of the tier that
|
|
121
|
+
has failed **every** round. A run spinning on one level needs the operator to see
|
|
122
|
+
*which* level:
|
|
123
|
+
|
|
124
|
+
- the same tier every round → the level is being misread, or the node is the wrong
|
|
125
|
+
shape. Not one fix away. Re-plan the node, do not attempt round four.
|
|
126
|
+
- different tiers each round → churn across levels. Usually one requirement that
|
|
127
|
+
was never decided, surfacing at whichever distance looks at it.
|
|
128
|
+
|
|
129
|
+
## What this costs, said out loud
|
|
130
|
+
|
|
131
|
+
Three agents per node instead of one. That is the price of the visibility, and it is
|
|
132
|
+
paid per node rather than per run. The three are dispatched in parallel, so the
|
|
133
|
+
wall-clock cost is roughly one reading; the token cost is three. A node whose
|
|
134
|
+
`check` is mechanical and whose blast radius is genuinely nil still pays it — and a
|
|
135
|
+
tier with nothing to find says so in `scope` and `not_examined` rather than being
|
|
136
|
+
skipped, because **a skipped level and a clean level are indistinguishable
|
|
137
|
+
afterwards**, and only one of them is evidence.
|
|
138
|
+
|
|
139
|
+
## Rationalizations
|
|
140
|
+
|
|
141
|
+
| Temptation | Why it is wrong |
|
|
142
|
+
|---|---|
|
|
143
|
+
| «All three would say the same thing» | Then all three say it, at a cost you already know, and the run has three signatures instead of one guess about what the other two would have found |
|
|
144
|
+
| «The seam tier can read the unit report first — it saves tokens» | It saves tokens by removing the second opinion. The reports are cheap; the independence is the product |
|
|
145
|
+
| «Tier 3 passed last round, skip it» | The fix is a new change. A tier's pass is about the tree it read, and that tree moved |
|
|
146
|
+
| «Round 4 will get it» | Read the ceiling's output. The same tier failing three times is a planning defect wearing a verification failure's clothes |
|
|
@@ -71,6 +71,7 @@ a row pointing outside the bundle is the defect this file exists to catch.
|
|
|
71
71
|
| What a spec must lock, the UX-track order, the module dossier | `references/spec.md` |
|
|
72
72
|
| The zero-context plan format, parallel groups, set equality | `references/planning.md` |
|
|
73
73
|
| The work graph: its fields, the verbs and their exit codes, and the three invariants a schema cannot state | `references/work-graph.md`, `scripts/graph.py`, `graph.schema.json` |
|
|
74
|
+
| How a node is CLOSED: three blind readings at escalating visibility, all three required, and the round ledger the ceiling reads | `references/certification.md`, `agents/verifier-{unit,seam,product}.md`, `scripts/graph.py certify` |
|
|
74
75
|
| Workspace isolation, the subagent loop, who may write the register | `references/build.md` |
|
|
75
76
|
| The review rubric, diff packages, the three verdicts | `references/review.md` |
|
|
76
77
|
| **False success** — the class, its known shapes and its two rules | `references/gates.md` |
|
|
@@ -273,6 +273,12 @@ never that the work was skipped quietly.
|
|
|
273
273
|
`next` refuses to walk one. The reason to prefer it over a prose plan is measured, not
|
|
274
274
|
aesthetic: a 400-node graph and a 4-node graph produce the same 27-byte frontier, so the
|
|
275
275
|
cost of knowing what is next does not grow with the programme.
|
|
276
|
+
- **And the check is what THREE readings will run, not one**
|
|
277
|
+
([`certification.md`](certification.md)). A node is closed by `unit`, `seam` and `product`
|
|
278
|
+
reports, dispatched blind and in parallel, and `certify` refuses the close until all three
|
|
279
|
+
pass. That is a planning fact, not only a verification one: a node whose blast radius nobody
|
|
280
|
+
can name is a node the seam and product tiers cannot scope, so `touches` and `serves` are
|
|
281
|
+
what make the two outer readings possible at all.
|
|
276
282
|
- **The queue exists here, so the loop arms here** ([`continuity.md`](continuity.md) →
|
|
277
283
|
*Part 1a*). Where `run.loop.arm` is `after-decomposition` and the map holds more than
|
|
278
284
|
one module, arm the mode at the close of this stage and print one line: the mode, and
|
|
@@ -59,6 +59,7 @@ conditional on the code, never merely sequenced after it.**
|
|
|
59
59
|
| `goal` | the release goal | `0` · `3` unstated |
|
|
60
60
|
| `add` | the id it allocated | `0` · `1` refused |
|
|
61
61
|
| `park` | the id and the reason | `0` · `1` refused |
|
|
62
|
+
| `certify` | the round, and on a failure every `breaks` finding with its fix and its check | `0` all three tiers passed · `1` a tier failed, or a report is malformed |
|
|
62
63
|
| `close` | the goal, the new frontier count, and what was not verified | `0` · `1` refused **or the verdict stops the run** |
|
|
63
64
|
| `producer` | what produced this proof — actor, model, runtime, skill, config, commit, trace | `0` |
|
|
64
65
|
| `doctrine` | how many of the bundle's reference files this run opened | `0` |
|
|
@@ -77,6 +78,8 @@ A **`parked`** node is the single exemption: it is the one node nobody will clos
|
|
|
77
78
|
*n/a — parked* in that field is confidence without correctness. `park` never removes what
|
|
78
79
|
the node said it would run.
|
|
79
80
|
|
|
81
|
+
**A node is closed by three readings, not one.** `certify` takes one tier report from each of `unit`, `seam` and `product` — dispatched blind and in parallel — requires all three to pass, and assembles the seven-key verdict `close` consumes. `close`'s contract is unchanged; what changed is that the verdict is now built from three readings at different distances instead of written from one, because a change can be correct where it was made and wrong one level out. A failing round records itself and leaves the node open. Doctrine: [`certification.md`](certification.md).
|
|
82
|
+
|
|
80
83
|
**`close` stamps the commit; the verifier never supplies it.** A verdict written after the
|
|
81
84
|
tree moved is evidence about a different tree, and an agent cannot name the wrong commit if
|
|
82
85
|
it is never the one naming one.
|
|
@@ -515,6 +515,139 @@ def verdict_violations(v):
|
|
|
515
515
|
return out
|
|
516
516
|
|
|
517
517
|
|
|
518
|
+
# --- certification: three tiers, one node ------------------------------------
|
|
519
|
+
#
|
|
520
|
+
# One verifier reads the diff it was handed. That is the whole limitation this
|
|
521
|
+
# section exists for: a change can be correct where it was made, and wrong one
|
|
522
|
+
# level out — a caller whose contract moved, a module whose invariant the new
|
|
523
|
+
# branch breaks, a documented behaviour nobody re-read. The single verdict cannot
|
|
524
|
+
# see any of it, because the context it was given was the change.
|
|
525
|
+
#
|
|
526
|
+
# So a node is closed by THREE reports at escalating visibility, produced
|
|
527
|
+
# independently and blind to each other:
|
|
528
|
+
#
|
|
529
|
+
# unit the code that changed — the functions, classes and branches in the
|
|
530
|
+
# diff, and the node's own `check`
|
|
531
|
+
# seam one level out — callers, callees, shared state, the contracts and
|
|
532
|
+
# tests of the neighbours the change can reach
|
|
533
|
+
# product one level out again — the documentation, the scenarios, how this
|
|
534
|
+
# behaviour interacts with the rest of the product
|
|
535
|
+
#
|
|
536
|
+
# **All three must pass, and blind is the point.** Three agents that read each
|
|
537
|
+
# other's reports are one opinion with three signatures; the disagreement is the
|
|
538
|
+
# instrument. `certify` refuses a report that cites another tier's verdict.
|
|
539
|
+
#
|
|
540
|
+
# **A tier cannot pass on an empty `scope`.** This is the rule the rest is built
|
|
541
|
+
# around: a report that names nothing it read is a rubber stamp, and a rubber
|
|
542
|
+
# stamp at three levels is worse than one verifier, because it costs three times
|
|
543
|
+
# as much and reads as three times the assurance.
|
|
544
|
+
TIERS = ("unit", "seam", "product")
|
|
545
|
+
TIER_KEYS = ("node", "tier", "verdict", "scope", "confirms", "findings",
|
|
546
|
+
"evidence", "not_examined")
|
|
547
|
+
TIER_VERDICTS = ("pass", "fail")
|
|
548
|
+
SEVERITIES = ("breaks", "risk")
|
|
549
|
+
# A tier report that quotes another tier's verdict was not written blind. Cheap
|
|
550
|
+
# to detect and worth detecting: the failure it prevents is three reports that
|
|
551
|
+
# agree because the second two read the first.
|
|
552
|
+
CROSS_TIER = re.compile(r"\b(?:unit|seam|product)\s+tier\s+(?:passed|failed|says)"
|
|
553
|
+
r"|\btier\s+\d\s+(?:passed|failed)"
|
|
554
|
+
r"|as\s+the\s+(?:unit|seam|product)\s+tier", re.I)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def tier_violations(t):
|
|
558
|
+
"""Everything wrong with one tier report, in a stable order.
|
|
559
|
+
|
|
560
|
+
Same law as `verdict_violations`: the shape is checked rather than trusted,
|
|
561
|
+
and every refusal names the key, because a report rejected without naming its
|
|
562
|
+
fault is a report the next attempt reproduces.
|
|
563
|
+
"""
|
|
564
|
+
out = []
|
|
565
|
+
if not isinstance(t, dict):
|
|
566
|
+
return ["tier report is not an object"]
|
|
567
|
+
|
|
568
|
+
for k in TIER_KEYS:
|
|
569
|
+
if k not in t:
|
|
570
|
+
out.append("tier report has no `%s` — all eight are required, because a "
|
|
571
|
+
"report that omits one is silent about it rather than clear" % k)
|
|
572
|
+
if out:
|
|
573
|
+
return out
|
|
574
|
+
|
|
575
|
+
if not isinstance(t["node"], str) or not t["node"].startswith(NODE_ID):
|
|
576
|
+
out.append("tier report `node` is %r, which is not a node id" % (t["node"],))
|
|
577
|
+
if t["tier"] not in TIERS:
|
|
578
|
+
out.append("tier report `tier` is %r — it must be one of %s"
|
|
579
|
+
% (t["tier"], ", ".join(TIERS)))
|
|
580
|
+
if t["verdict"] not in TIER_VERDICTS:
|
|
581
|
+
out.append("tier report `verdict` is %r — it must be `pass` or `fail`, because "
|
|
582
|
+
"a certification that admits a third state admits a maybe"
|
|
583
|
+
% (t["verdict"],))
|
|
584
|
+
|
|
585
|
+
for k in ("scope", "confirms", "findings", "evidence", "not_examined"):
|
|
586
|
+
if not isinstance(t[k], list):
|
|
587
|
+
out.append("tier report `%s` must be a list" % k)
|
|
588
|
+
if out:
|
|
589
|
+
return out
|
|
590
|
+
|
|
591
|
+
for k in ("scope", "confirms", "evidence", "not_examined"):
|
|
592
|
+
for i, e in enumerate(t[k]):
|
|
593
|
+
if not isinstance(e, str) or not e.strip():
|
|
594
|
+
out.append("tier report `%s[%d]` is %r — every entry must be a non-empty "
|
|
595
|
+
"string, and a list of blanks is the shape a script emitting "
|
|
596
|
+
"empty output produces" % (k, i, e))
|
|
597
|
+
|
|
598
|
+
findings = []
|
|
599
|
+
for i, f in enumerate(t["findings"]):
|
|
600
|
+
if not isinstance(f, dict):
|
|
601
|
+
out.append("tier report `findings[%d]` is not an object" % i)
|
|
602
|
+
continue
|
|
603
|
+
for k in ("what", "where", "severity"):
|
|
604
|
+
if not str(f.get(k, "")).strip():
|
|
605
|
+
out.append("tier report `findings[%d]` does not say `%s`" % (i, k))
|
|
606
|
+
sev = f.get("severity")
|
|
607
|
+
if sev is not None and sev not in SEVERITIES:
|
|
608
|
+
out.append("tier report `findings[%d].severity` is %r — it must be `breaks` "
|
|
609
|
+
"(the node is not done) or `risk` (found, judged survivable, and "
|
|
610
|
+
"named)" % (i, sev))
|
|
611
|
+
# A break has to say how its fix will be PROVEN, for the same reason
|
|
612
|
+
# `replan.add` does: the node it creates is one the next certification has
|
|
613
|
+
# to close, and handing it the absence is how the defect returns a round
|
|
614
|
+
# later.
|
|
615
|
+
if sev == "breaks" and not str(f.get("check", "")).strip():
|
|
616
|
+
out.append("tier report `findings[%d]` breaks the node and names no `check` "
|
|
617
|
+
"— the fix node it becomes has to say how IT will be closed" % i)
|
|
618
|
+
findings.append(f)
|
|
619
|
+
|
|
620
|
+
breaks = [f for f in findings if isinstance(f, dict) and f.get("severity") == "breaks"]
|
|
621
|
+
if t["verdict"] == "pass":
|
|
622
|
+
# The two rules that make a pass mean something.
|
|
623
|
+
if not t["scope"]:
|
|
624
|
+
out.append("tier report `%s` passes on an empty `scope` — a report that names "
|
|
625
|
+
"nothing it read is a rubber stamp, and three of those cost three "
|
|
626
|
+
"times one verifier and read as three times the assurance"
|
|
627
|
+
% t["tier"])
|
|
628
|
+
if not t["evidence"]:
|
|
629
|
+
out.append("tier report `%s` passes with empty `evidence` — the field exists "
|
|
630
|
+
"for exactly this" % t["tier"])
|
|
631
|
+
if breaks:
|
|
632
|
+
out.append("tier report `%s` passes while carrying %d finding(s) at severity "
|
|
633
|
+
"`breaks`: %s. Those two cannot both be true"
|
|
634
|
+
% (t["tier"], len(breaks),
|
|
635
|
+
"; ".join(str(f.get("what")) for f in breaks)))
|
|
636
|
+
elif t["verdict"] == "fail":
|
|
637
|
+
if not breaks:
|
|
638
|
+
out.append("tier report `%s` fails and names no finding at severity `breaks` "
|
|
639
|
+
"— a fail that does not say what broke is a fail the next round "
|
|
640
|
+
"cannot act on" % t["tier"])
|
|
641
|
+
|
|
642
|
+
# Blind, and checked. Only the prose fields can carry it.
|
|
643
|
+
for k in ("confirms", "evidence", "not_examined"):
|
|
644
|
+
for i, e in enumerate(t[k]):
|
|
645
|
+
if isinstance(e, str) and CROSS_TIER.search(e):
|
|
646
|
+
out.append("tier report `%s[%d]` cites another tier's verdict (%r) — the "
|
|
647
|
+
"three run blind, because three reports that read each other "
|
|
648
|
+
"are one opinion with three signatures" % (k, i, e.strip()[:70]))
|
|
649
|
+
return out
|
|
650
|
+
|
|
518
651
|
# --- verbs --------------------------------------------------------------------
|
|
519
652
|
|
|
520
653
|
def cmd_validate(graph, args):
|
|
@@ -909,7 +1042,7 @@ def cmd_producer(graph, args):
|
|
|
909
1042
|
def cmd_doctrine(graph, args):
|
|
910
1043
|
"""Which doctrine this run actually read — B-061.
|
|
911
1044
|
|
|
912
|
-
The bundle is
|
|
1045
|
+
The bundle is 36 reference files. A run reads some subset and nothing recorded which,
|
|
913
1046
|
so **a skipped file and a read one were indistinguishable** — the class every guard in
|
|
914
1047
|
this repository exists to catch, left standing over the doctrine itself.
|
|
915
1048
|
|
|
@@ -977,6 +1110,170 @@ def cmd_doctrine(graph, args):
|
|
|
977
1110
|
return 0
|
|
978
1111
|
|
|
979
1112
|
|
|
1113
|
+
def cmd_certify(graph, args):
|
|
1114
|
+
"""Require three independent tier reports, then emit the verdict `close` consumes.
|
|
1115
|
+
|
|
1116
|
+
This is a gate in FRONT of `close`, not a replacement for it. `close`'s contract
|
|
1117
|
+
is unchanged and its seven keys are still the only thing that moves the graph —
|
|
1118
|
+
what changed is that the verdict is now assembled from three readings at
|
|
1119
|
+
different distances instead of written from one.
|
|
1120
|
+
|
|
1121
|
+
**The round is recorded whether it passes or fails.** A failing round that
|
|
1122
|
+
wrote nothing would erase the only evidence that a node is churning, which is
|
|
1123
|
+
the number the ceiling below reads. The node stays `pending` on a failure; the
|
|
1124
|
+
round count is the trail.
|
|
1125
|
+
|
|
1126
|
+
**The ceiling measures rather than stops** — `references/loop-guard.md`. At the
|
|
1127
|
+
ceiling `certify` still runs and still tells the truth about the tiers; what it
|
|
1128
|
+
adds is the name of the tier that keeps failing, because a run spinning on one
|
|
1129
|
+
level needs the operator to see WHICH level, not to be halted.
|
|
1130
|
+
"""
|
|
1131
|
+
guard(graph, args.graph)
|
|
1132
|
+
|
|
1133
|
+
nid = args.node
|
|
1134
|
+
by_id = {n.get("id"): n for n in graph.get("nodes") or []}
|
|
1135
|
+
node = by_id.get(nid)
|
|
1136
|
+
if node is None:
|
|
1137
|
+
die("no node %s in this graph — nothing was written" % nid)
|
|
1138
|
+
if node.get("status") in TERMINAL:
|
|
1139
|
+
die("%s is already %s — certifying it again would overwrite the record of the "
|
|
1140
|
+
"close that already happened" % (nid, node.get("status")))
|
|
1141
|
+
open_blockers = [b for b in node.get("blocked_by") or []
|
|
1142
|
+
if by_id.get(b, {}).get("status") not in TERMINAL]
|
|
1143
|
+
if open_blockers:
|
|
1144
|
+
die("%s waits on %s, which %s not closed — certifying work that could not have "
|
|
1145
|
+
"run certifies nothing" % (nid, ", ".join(open_blockers),
|
|
1146
|
+
"is" if len(open_blockers) == 1 else "are"))
|
|
1147
|
+
|
|
1148
|
+
reports, bad = {}, []
|
|
1149
|
+
for path in args.tier:
|
|
1150
|
+
try:
|
|
1151
|
+
with open(path, encoding="utf-8") as fh:
|
|
1152
|
+
t = json.load(fh)
|
|
1153
|
+
except OSError as e:
|
|
1154
|
+
die("cannot read the tier report at %s — %s" % (path, e), 2)
|
|
1155
|
+
except ValueError as e:
|
|
1156
|
+
die("%s: not readable as JSON — %s" % (path, e))
|
|
1157
|
+
v = tier_violations(t)
|
|
1158
|
+
if v:
|
|
1159
|
+
bad += ["%s: %s" % (os.path.basename(path), line) for line in v]
|
|
1160
|
+
continue
|
|
1161
|
+
if t["node"] != nid:
|
|
1162
|
+
bad.append("%s: reports on %s while this certification is for %s — a report "
|
|
1163
|
+
"about another node is not evidence about this one"
|
|
1164
|
+
% (os.path.basename(path), t["node"], nid))
|
|
1165
|
+
continue
|
|
1166
|
+
if t["tier"] in reports:
|
|
1167
|
+
bad.append("%s: a second `%s` report — the three tiers are three distances, "
|
|
1168
|
+
"and two readings at one distance leave another unread"
|
|
1169
|
+
% (os.path.basename(path), t["tier"]))
|
|
1170
|
+
continue
|
|
1171
|
+
reports[t["tier"]] = t
|
|
1172
|
+
if bad:
|
|
1173
|
+
die("the tier reports are malformed — nothing was written:\n " + "\n ".join(bad))
|
|
1174
|
+
|
|
1175
|
+
missing = [x for x in TIERS if x not in reports]
|
|
1176
|
+
if missing:
|
|
1177
|
+
die("certification is missing the %s report(s) — all three are required, because "
|
|
1178
|
+
"the level nobody read is the level the defect survives at"
|
|
1179
|
+
% ", ".join("`%s`" % m for m in missing))
|
|
1180
|
+
|
|
1181
|
+
# The stamp, read here and never accepted from a report — same law as `close`.
|
|
1182
|
+
import subprocess
|
|
1183
|
+
try:
|
|
1184
|
+
r = subprocess.run(["git", "rev-parse", "HEAD"], capture_output=True, text=True)
|
|
1185
|
+
head = r.stdout.strip() if r.returncode == 0 else ""
|
|
1186
|
+
except OSError:
|
|
1187
|
+
head = ""
|
|
1188
|
+
|
|
1189
|
+
prior = node.get("certification") or {}
|
|
1190
|
+
round_no = int(prior.get("round") or 0) + 1
|
|
1191
|
+
tiers_now = {x: reports[x]["verdict"] for x in TIERS}
|
|
1192
|
+
history = list(prior.get("history") or []) + [tiers_now]
|
|
1193
|
+
node["certification"] = {
|
|
1194
|
+
"round": round_no,
|
|
1195
|
+
"tiers": tiers_now,
|
|
1196
|
+
"at": head or "unavailable — not inside a git checkout",
|
|
1197
|
+
"history": history,
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
failed = [x for x in TIERS if tiers_now[x] == "fail"]
|
|
1201
|
+
|
|
1202
|
+
# Churn, measured. A tier that has failed in every round so far is the one the
|
|
1203
|
+
# operator needs named; counting it here is what makes the loop visible.
|
|
1204
|
+
churning = [x for x in TIERS
|
|
1205
|
+
if len(history) >= 2 and all(h.get(x) == "fail" for h in history)]
|
|
1206
|
+
|
|
1207
|
+
save(args.graph, graph)
|
|
1208
|
+
|
|
1209
|
+
if failed:
|
|
1210
|
+
print("%s: certification round %d FAILED at %s"
|
|
1211
|
+
% (nid, round_no, ", ".join("`%s`" % f for f in failed)), file=sys.stderr)
|
|
1212
|
+
for tier in failed:
|
|
1213
|
+
for f in reports[tier]["findings"]:
|
|
1214
|
+
if f.get("severity") != "breaks":
|
|
1215
|
+
continue
|
|
1216
|
+
print(" [%s] %s — %s" % (tier, f["where"], f["what"]), file=sys.stderr)
|
|
1217
|
+
print(" fix: %s" % f.get("fix", "(not stated)"), file=sys.stderr)
|
|
1218
|
+
print(" check: %s" % f["check"], file=sys.stderr)
|
|
1219
|
+
if round_no >= args.ceiling:
|
|
1220
|
+
print("\n%s has been certified %d time(s), at or over the ceiling of %d."
|
|
1221
|
+
% (nid, round_no, args.ceiling), file=sys.stderr)
|
|
1222
|
+
if churning:
|
|
1223
|
+
print("The same tier has failed every round: %s. That is not a fix "
|
|
1224
|
+
"away — the level itself is being misread, or the node is the "
|
|
1225
|
+
"wrong shape. references/loop-guard.md."
|
|
1226
|
+
% ", ".join("`%s`" % c for c in churning), file=sys.stderr)
|
|
1227
|
+
else:
|
|
1228
|
+
print("No single tier is failing every round, so this is churn across "
|
|
1229
|
+
"levels rather than one stuck level.", file=sys.stderr)
|
|
1230
|
+
print("\nThe node stays open. Round %d is recorded on it." % round_no,
|
|
1231
|
+
file=sys.stderr)
|
|
1232
|
+
return 1
|
|
1233
|
+
|
|
1234
|
+
# Passed at all three. Assemble the canonical verdict.
|
|
1235
|
+
#
|
|
1236
|
+
# The mapping is deliberate and uses no field for something it does not mean:
|
|
1237
|
+
# confirms -> done (asked for, and now true)
|
|
1238
|
+
# not_examined -> not_verified (present, and no check touched it)
|
|
1239
|
+
# risk findings -> blockers (found, judged survivable, and named, which is
|
|
1240
|
+
# exactly what `can_continue_around: true` says)
|
|
1241
|
+
verdict = {
|
|
1242
|
+
"node": nid,
|
|
1243
|
+
"done": [c for x in TIERS for c in reports[x]["confirms"]],
|
|
1244
|
+
"not_done": [],
|
|
1245
|
+
"not_verified": ["%s: %s" % (x, n)
|
|
1246
|
+
for x in TIERS for n in reports[x]["not_examined"]],
|
|
1247
|
+
"blockers": [
|
|
1248
|
+
{"what": "%s (%s, found by the `%s` tier)" % (f["what"], f["where"], x),
|
|
1249
|
+
"blocks": [], "can_continue_around": True}
|
|
1250
|
+
for x in TIERS for f in reports[x]["findings"]
|
|
1251
|
+
if f.get("severity") == "risk"
|
|
1252
|
+
],
|
|
1253
|
+
"replan": {"possible": True, "add": [], "park": [],
|
|
1254
|
+
"why": "certified at all three tiers in round %d" % round_no},
|
|
1255
|
+
"evidence": ["%s: %s" % (x, e) for x in TIERS for e in reports[x]["evidence"]],
|
|
1256
|
+
}
|
|
1257
|
+
# Checked against the same gate `close` will apply, HERE, so a certification
|
|
1258
|
+
# cannot hand the run a verdict its own consumer refuses.
|
|
1259
|
+
broken = verdict_violations(verdict)
|
|
1260
|
+
if broken:
|
|
1261
|
+
die("all three tiers passed and the assembled verdict is still malformed — this "
|
|
1262
|
+
"is a defect in `certify`, not in the reports:\n " + "\n ".join(broken))
|
|
1263
|
+
|
|
1264
|
+
out = args.verdict_out or os.path.join(os.path.dirname(args.graph) or ".",
|
|
1265
|
+
"verdict-%s.json" % nid)
|
|
1266
|
+
tmp = out + ".tmp"
|
|
1267
|
+
with open(tmp, "w", encoding="utf-8") as fh:
|
|
1268
|
+
json.dump(verdict, fh, indent=2, ensure_ascii=False)
|
|
1269
|
+
fh.write("\n")
|
|
1270
|
+
os.replace(tmp, out)
|
|
1271
|
+
print("%s: certified at unit, seam and product in round %d" % (nid, round_no))
|
|
1272
|
+
print("verdict written to %s — close it with:" % out)
|
|
1273
|
+
print(" graph.py close --verdict %s" % out)
|
|
1274
|
+
return 0
|
|
1275
|
+
|
|
1276
|
+
|
|
980
1277
|
def cmd_close(graph, args):
|
|
981
1278
|
"""Consume a verdict, close one node, and re-plan — T-5, REQ-007.
|
|
982
1279
|
|
|
@@ -1105,6 +1402,8 @@ VERBS = {
|
|
|
1105
1402
|
"coverage": (cmd_coverage, "every requirement and the nodes serving it; exits 1 on a gap"),
|
|
1106
1403
|
"add": (cmd_add, "add a node mid-run"),
|
|
1107
1404
|
"park": (cmd_park, "park a node, carrying the reason"),
|
|
1405
|
+
"certify": (cmd_certify, "require three independent tier reports, then emit "
|
|
1406
|
+
"the verdict `close` consumes"),
|
|
1108
1407
|
"close": (cmd_close, "consume a verdict, close one node and re-plan"),
|
|
1109
1408
|
}
|
|
1110
1409
|
|
|
@@ -1147,6 +1446,16 @@ def main(argv=None):
|
|
|
1147
1446
|
made["close"].add_argument("--verdict", required=True,
|
|
1148
1447
|
help="path to the verifier's seven-key verdict JSON")
|
|
1149
1448
|
|
|
1449
|
+
p_cert = made["certify"]
|
|
1450
|
+
p_cert.add_argument("--node", required=True, help="the node being certified")
|
|
1451
|
+
p_cert.add_argument("--tier", action="append", required=True, default=[],
|
|
1452
|
+
help="path to one tier report; pass three times, one per tier")
|
|
1453
|
+
p_cert.add_argument("--verdict-out", dest="verdict_out", default=None,
|
|
1454
|
+
help="where to write the assembled verdict (default: beside the graph)")
|
|
1455
|
+
p_cert.add_argument("--ceiling", type=int, default=3,
|
|
1456
|
+
help="rounds after which the output names the churning tier; it "
|
|
1457
|
+
"measures rather than stops (references/loop-guard.md)")
|
|
1458
|
+
|
|
1150
1459
|
p_park = made["park"]
|
|
1151
1460
|
p_park.add_argument("node")
|
|
1152
1461
|
# `required=True` makes the MISSING flag a usage error (exit 2). The empty and
|
|
@@ -1158,7 +1467,7 @@ def main(argv=None):
|
|
|
1158
1467
|
verbs = {k: v[0] for k, v in VERBS.items()}
|
|
1159
1468
|
if args.verb in NO_GRAPH:
|
|
1160
1469
|
return verbs[args.verb](None, args)
|
|
1161
|
-
if args.verb in ("add", "park", "close"):
|
|
1470
|
+
if args.verb in ("add", "park", "close", "certify"):
|
|
1162
1471
|
# The READ happens inside the lock too. Loading first and locking second is the
|
|
1163
1472
|
# same lost update with an extra step: the stale copy is already in memory.
|
|
1164
1473
|
with held(args.graph):
|
|
@@ -20,7 +20,7 @@ Run: `<topic>` · started `<YYYY-MM-DD>` · module map: `<path or "none">`
|
|
|
20
20
|
|
|
21
21
|
## `read:` — which doctrine this run actually opened
|
|
22
22
|
|
|
23
|
-
The bundle is
|
|
23
|
+
The bundle is 36 reference files and nothing recorded which of them a run read, so **a
|
|
24
24
|
skipped file and a read one were indistinguishable** — the class every guard in this
|
|
25
25
|
pipeline exists to catch, left standing over the doctrine itself.
|
|
26
26
|
|
|
@@ -48,7 +48,7 @@ same claim one level down.
|
|
|
48
48
|
|---|---|---|
|
|
49
49
|
| `unmeasured — no run ledger` | there is no ledger | nothing to read from |
|
|
50
50
|
| `unmeasured — the ledger carries no read: lines` | the hook is absent, **or** the run opened no doctrine | two opposite facts, and the ledger cannot separate them, so neither is claimed |
|
|
51
|
-
| `N of
|
|
51
|
+
| `N of 36 reference files read — unattested`, then each unread one | the hook is installed and fired | the count alone says there is a gap, not where — and `unattested` says the ledger cannot name who wrote the lines |
|
|
52
52
|
|
|
53
53
|
**It is a disclosure: no floor, no direction, never a target.** A run that needs four files
|
|
54
54
|
and reads four is not worse than one that reads thirty — and the moment the number becomes
|