From 62e157f313dac96aa7128e98d30c42875f00e581 Mon Sep 17 00:00:00 2001 From: Tin Dang Date: Thu, 3 Sep 2026 14:56:53 +0700 Subject: [PATCH 1/2] docs(bundle): author v3-final-collateral from the work it actually shipped The milestone was scaffolded 2026-08-11 by add/3.0.0 and driven to six closed tasks without its CARD or EXIT ever being filled in. Nothing asked: `freeze` did not refuse a template Milestone until the `authoring-beat-named` task shipped that guard, and `placeholders_in` reads only RULES/ASSUMPTIONS/CHECKS, which a Milestone body does not have. The new `unauthored_node` finding surfaced it, and `status` had been pointing at it as live work for three weeks. Deleting it was considered and rejected on inspection: six tasks declare `milestone: v3-final-collateral` and all six are done, so removing the node would have traded one warning for six dangling edges. Authored retrospectively from what those six tasks shipped, closed 6/6, archived. The `why:` says out loud that it was written after the fact. author: Tin Dang --- .add/milestones/v3-final-collateral.md | 31 +++++++++++++++++--------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/.add/milestones/v3-final-collateral.md b/.add/milestones/v3-final-collateral.md index f470d5d8..55657f14 100644 --- a/.add/milestones/v3-final-collateral.md +++ b/.add/milestones/v3-final-collateral.md @@ -1,26 +1,37 @@ --- type: Milestone title: v3.0.0 final collateral -status: direction +status: archived generated: { by: add/3.0.0, at: 2026-08-11 } verified: [] --- ## CARD -goal: -why: -next: add new task +goal: everything 3.0.0 needed that was not the engine itself — the docs that teach it, the post that announces it, and the four defects a real first run trips over. +why: authored retrospectively 2026-09-03. The milestone was scaffolded 2026-08-11 and driven to six closed tasks without its CARD or EXIT ever being filled in — `freeze` did not refuse a template Milestone until the `authoring-beat-named` task shipped that guard, so nothing ever asked. Its work is done and released; this records what it was. ## SCOPE -In: -Out: +In: the 3.0.0 release collateral — docs site, launch post — and the engine defects a first run hits: digest root, `--scope` append, `upgrade` leaving a working bundle, Persona scaffold keys +Out: the 3.0 graft itself, and every method change after it — those are their own milestones ## GROUND -touches: +touches: add-method/docs/, blog/, add-method/tooling/add.py, add-method/tooling/templates/ risks: - - + - collateral is the part a release forgets; a shipped engine nobody can start is not shipped ## EXIT -- [ ] (← ) +- [x] the receipt digest and the gate's freshness check resolve `scope:` from the same root, and a degrade is said out loud (← run-digest-root) +- [x] `add new Persona` scaffolds every contract-recommended routing key plus OKF description/sources (← okf-persona-template) +- [x] the mkdocs book teaches the engine that ships (← docs-beta2-refresh) +- [x] repeated `--scope` flags append in order, and the comma form keeps working (← scope-flag-append) +- [x] a launch post a normal user can act on, with the honest numbers (← launch-blog) +- [x] after `add upgrade` the very next `add status` runs (← upgrade-working-bundle) ## CLOSE -evidence: +| task | verdict | receipt | +|---|---|---| +| run-digest-root | PASS | closed 2026-08 — one digest root, degrade stated | +| okf-persona-template | PASS | closed 2026-08 — routing keys + OKF fields scaffolded | +| docs-beta2-refresh | PASS | closed 2026-08 — book teaches the shipped engine | +| scope-flag-append | PASS | closed 2026-08 — repeated flags append, comma form kept | +| launch-blog | PASS | closed 2026-08 — post written against the honest numbers | +| upgrade-working-bundle | PASS | closed 2026-08 — upgrade leaves a runnable bundle | From caf5b103738940d14929cbe345c5f6f7dc267054 Mon Sep 17 00:00:00 2001 From: Tin Dang Date: Thu, 3 Sep 2026 14:56:53 +0700 Subject: [PATCH 2/2] fix(engine): an advisory nudge names a node you can still advise MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Measured on this repo's own bundle: 23 of doctor's 25 findings were `unadvised_sensitive`, and every one named a task already `done`. The advice each carries — attach a lens — is unreachable on a closed, gated node, so the report read as a wall of noise with the two actionable findings sitting inside it. A report is read as a worklist whether or not it was written as one. `unadvised_sensitive` now skips a node whose status is `done`. Everything else is untouched: type, sensitivity floor, lens presence, and the severity split where security warns and the softer floors nudge. Three calls inside the contract, each with a probe: - `done` is the ONLY exclusion. A node in `verify` can still take a lens before its gate, so excluding it would hide a finding while it is still actionable. - An ABSENT status is not closed (R:BLINDCLOSE). Hiding a finding on a malformed node is the failure this rule guards against, not an instance of it. - The closed findings are DROPPED, not tallied. A summary line is still a line the reader has to decide to ignore. On this bundle: 25 findings -> 1, and the survivor is the real `edge_unresolved` defect that the noise had been burying. Task: doctor-nudges-what-you-can-act-on, gated PASS on receipt runs/1.md. ENGINE_MD5 re-aimed 7932ec06 -> the doctor scoping. author: Tin Dang --- .../doctor-nudges-what-you-can-act-on.md | 62 +++++++++++++ .../src/add_method/_bundled/tooling/add.py | 9 ++ .../test_doctor_nudges_what_you_can_act_on.py | 93 +++++++++++++++++++ add-method/tooling/add.py | 9 ++ add-method/tooling/engine_pin.py | 2 +- 5 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 .add/tasks/doctor-nudges-what-you-can-act-on.md create mode 100644 add-method/tests/engine/test_doctor_nudges_what_you_can_act_on.py diff --git a/.add/tasks/doctor-nudges-what-you-can-act-on.md b/.add/tasks/doctor-nudges-what-you-can-act-on.md new file mode 100644 index 00000000..69d57f22 --- /dev/null +++ b/.add/tasks/doctor-nudges-what-you-can-act-on.md @@ -0,0 +1,62 @@ +--- +type: Task +title: an advisory nudge names a node you can still advise +status: done +depth: quick +kind: feature +scope: + - add-method/tooling/add.py + - add-method/tests/engine/ +gives: + - S1 +generated: { by: add/3.4.0, at: 2026-09-03 } +verified: + - { by: "Tin Dang", at: 2026-09-03, act: freeze, authority: plan, direction: "sha256:9fdf0952e0e1351f", binding: "sha256:66eb975a05423ae8" } + - { by: "cli", at: 2026-09-03, act: brief, authority: process, brief: "sha256:37c7474b66d9eac4" } + - { by: "process:run", at: 2026-09-03, act: run, authority: process, outcome: PASS, receipt: /tasks/doctor-nudges-what-you-can-act-on.d/runs/1.md } + - { by: "Tin Dang", at: 2026-09-03, act: gate, authority: process, outcome: PASS, receipt: /tasks/doctor-nudges-what-you-can-act-on.d/runs/1.md, brief: "sha256:13e8cfbf6dc13b5b" } +--- +## CARD +goal: `unadvised_sensitive` reports only nodes that can still take a lens, so `doctor`'s output is the set of things you can act on. +why: measured 2026-09-03 on this bundle — 23 of doctor's 25 findings are `unadvised_sensitive`, and every one names a task already `done`. The advice each gives ("advise it") is unreachable: the node is closed, its gate is stamped, and re-opening it to attach a retrospective lens is not something anyone will do. A reader learns to skim the whole report, which is how the two findings that ARE actionable get missed. + +## RULES + +- M1 `unadvised_sensitive` is not reported for a node whose status is `done` +- M2 an OPEN sensitive node with no lens is still reported, at the severity it has today +- M3 the severity split is untouched — `security` stays `warn`, the softer floors stay `info` +- M4 no other finding changes which nodes it reports + + +- R:BLINDCLOSE a closed node hides a finding that was never about the lens at all -> "BLINDCLOSE" + + +## ASSUMPTIONS +- A1 [who] covers: S1 · n/a · the report is the same for every reader; no authority sees a different finding set +- A2 [which] covers: S1 · the request says "open nodes" and does not say which statuses count as open; taking `done` as the ONLY exclusion -> a node in `verify` can still be advised before its gate, and excluding it would hide a finding while it is still actionable · probe: a `verify` node with no lens is still reported +- A3 [when] covers: S1 · the request does not say whether to drop the closed ones or tally them; taking DROP -> a tally line is still a line the reader must decide to ignore, and the archived record is the node itself, which keeps its own frontmatter · probe: no summary line replaces the dropped findings +- A4 [absent] covers: S1 · the request does not say what an ABSENT status means; taking absent-as-open -> a node with no status has not been closed, and hiding a finding on a malformed node is R:BLINDCLOSE · probe: a node with no `status:` is still reported +- A5 [order] covers: S1 · n/a · the finding is emitted inside one sorted pass and the exclusion does not reorder it +- A6 [experience] covers: S1 · the request is about what a reader can ACT on; taking the report as a worklist, not an audit log -> if the archived count matters later it is recoverable by reading the bundle, which is where it already lives · probe: the finding count on this bundle drops to the actionable set + +## PLAN +contract: `doctor`'s `unadvised_sensitive` loop skips a node whose `status:` is `done`. Every other condition — type, sensitivity floor, lens presence, severity split — is unchanged, and no other finding is touched. + +## EDGES +- E1 a node with no `status:` at all — absent is not closed +- E2 a `security` node that is done — the HARD floor does not survive closure either, or M1 is not a rule + +## CHECKS +- test_a_done_node_is_not_nudged_for_a_lens · covers: M1, A3, A6 · the 23 measured findings +- test_an_open_node_is_still_nudged · covers: M2, A2 · the finding must not go silent +- test_a_node_with_no_status_is_still_nudged · covers: A4, E1, R:BLINDCLOSE · absent is not closed +- test_the_severity_split_survives · covers: M3, E2 · security warns while open, silent when done +- test_no_other_finding_changed_its_reach · covers: M4 · the exclusion is scoped to one finding +red-first: every check MUST fail first. + +## EVIDENCE +receipt: .md> +gate: + +## LESSONS +- a report is read as a worklist whether or not it was written as one -> add learn method diff --git a/add-method/src/add_method/_bundled/tooling/add.py b/add-method/src/add_method/_bundled/tooling/add.py index 77427664..651a1c66 100644 --- a/add-method/src/add_method/_bundled/tooling/add.py +++ b/add-method/src/add_method/_bundled/tooling/add.py @@ -4559,6 +4559,15 @@ def find(severity, code, detail, node=None): fm = node["fm"] or {} if fm.get("type") != "Task" or SENSITIVITY_FLOOR.get(fm.get("sensitivity"), "process") == "process": continue + # M1: only a node that can still TAKE a lens. On a closed, gated node the advice this + # finding carries is unreachable, and 23 of 25 findings on this repo's own bundle were + # exactly that — a wall of unactionable lines the two real ones sat inside of. A report + # is read as a worklist whether or not it was written as one. + # `done` is the ONLY exclusion (A2): a node in `verify` is still advisable before its + # gate. An ABSENT status is NOT closed (A4, R:BLINDCLOSE) — hiding a finding on a + # malformed node is the failure this rule is meant to prevent, not an instance of it. + if fm.get("status") == "done": + continue if not fm.get("persona") and not fm.get("advised_by"): # Severity agrees with the gate floor (A2): security is a HARD gate refusal (R:NOCOVERAGE), # so doctor says `warn`; the softer data/architecture floors stay `info` nudges. diff --git a/add-method/tests/engine/test_doctor_nudges_what_you_can_act_on.py b/add-method/tests/engine/test_doctor_nudges_what_you_can_act_on.py new file mode 100644 index 00000000..2128c1e3 --- /dev/null +++ b/add-method/tests/engine/test_doctor_nudges_what_you_can_act_on.py @@ -0,0 +1,93 @@ +"""An advisory nudge names a node you can still advise. + +Measured on this repo's own bundle 2026-09-03: 23 of `doctor`'s 25 findings were +`unadvised_sensitive`, and every one named a task already `done`. The advice each carries — +attach a lens — is unreachable on a closed, gated node, so the whole report reads as noise and +the two findings that WERE actionable sat in the middle of it. + +The finding is true either way; the question is whether a report is a worklist or an audit log. +It is read as a worklist whether or not it was written as one. +""" +import sys +from pathlib import Path + +REPO = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(REPO / "tooling")) +import add # noqa: E402 + + +def _sensitive(tmp_path, slug, sensitivity="architecture", status=None): + """A sensitive Task carrying no lens, at whatever status the caller names.""" + if not (tmp_path / ".add").exists(): + add.init(tmp_path, "code", "T") # returns a TUPLE; nothing here needs it + cid, _ = add.new(tmp_path, "Task", slug, title=slug, sensitivity=sensitivity) + node = Path(add.scan(tmp_path)[cid]["path"]) + lines = node.read_text(encoding="utf-8").splitlines() + # E1 strips the field entirely; otherwise rewrite whatever status `new` chose — it seeds + # `direction`, not `created`, so a literal replace of the wrong word silently does nothing. + lines = [l for l in lines if not l.startswith("status:")] + if status is not None: + lines.insert(next(i for i, l in enumerate(lines) if l.startswith("type:")) + 1, + f"status: {status}") + node.write_text("\n".join(lines) + "\n", encoding="utf-8") + return cid + + +def _codes(tmp_path, cid): + """Findings doctor reports against one node. + + `doctor` takes the PROJECT root — the parent of `.add` — and returns a list of dicts. + Handing it the bundle root returns `[]`, which is a green anchor that proves nothing. + """ + return [f for f in add.doctor(tmp_path) if f["node"] == cid] + + +def test_a_done_node_is_not_nudged_for_a_lens(tmp_path): + """covers: M1, A3, A6 — the 23 measured findings.""" + cid = _sensitive(tmp_path, "closed", status="done") + hits = [f for f in _codes(tmp_path, cid) if f["code"] == "unadvised_sensitive"] + assert not hits, f"a closed node was told to attach a lens it can no longer attach: {hits}" + + +def test_an_open_node_is_still_nudged(tmp_path): + """covers: M2, A2 — the finding must not go silent; `verify` is still advisable.""" + for status in ("created", "direction", "build", "verify"): + p = tmp_path / status + p.mkdir() + cid = _sensitive(p, f"open-{status}", status=status) + hits = [f for f in _codes(p, cid) if f["code"] == "unadvised_sensitive"] + assert hits, f"an OPEN ({status}) sensitive node with no lens went unreported" + + +def test_a_node_with_no_status_is_still_nudged(tmp_path): + """covers: A4, E1, R:BLINDCLOSE — absent is not closed.""" + cid = _sensitive(tmp_path, "statusless", status=None) + hits = [f for f in _codes(tmp_path, cid) if f["code"] == "unadvised_sensitive"] + assert hits, "a node with no `status:` was treated as closed and its finding hidden" + + +def test_the_severity_split_survives(tmp_path): + """covers: M3, E2 — security warns while open, and is silent once done like any other.""" + a = tmp_path / "open"; a.mkdir() + cid = _sensitive(a, "sec-open", sensitivity="security", status="verify") + hits = [f for f in _codes(a, cid) if f["code"] == "unadvised_sensitive"] + assert hits and any(f["severity"] == "warn" for f in hits), f"security lost its `warn` severity: {hits}" + + b = tmp_path / "done"; b.mkdir() + cid = _sensitive(b, "sec-done", sensitivity="security", status="done") + assert not [f for f in _codes(b, cid) if f["code"] == "unadvised_sensitive"], \ + "M1 does not hold for the HARD floor — a done security node is still unadvisable" + + +def test_no_other_finding_changed_its_reach(tmp_path): + """covers: M4 — the exclusion is scoped to ONE finding. + + A done node still carries every other property doctor reports on. Proved by giving the same + closed node a defect a different finding owns and asserting that one still fires. + """ + cid = _sensitive(tmp_path, "closed-too", status="done") + node = Path(add.scan(tmp_path)[cid]["path"]) + node.write_text(node.read_text(encoding="utf-8").replace("---\n", "", 1), encoding="utf-8") + codes = {f["code"] for f in add.doctor(tmp_path)} + assert "missing_frontmatter" in codes, ( + "closing a node silenced a finding that was never about the lens: " + str(codes)) diff --git a/add-method/tooling/add.py b/add-method/tooling/add.py index 77427664..651a1c66 100644 --- a/add-method/tooling/add.py +++ b/add-method/tooling/add.py @@ -4559,6 +4559,15 @@ def find(severity, code, detail, node=None): fm = node["fm"] or {} if fm.get("type") != "Task" or SENSITIVITY_FLOOR.get(fm.get("sensitivity"), "process") == "process": continue + # M1: only a node that can still TAKE a lens. On a closed, gated node the advice this + # finding carries is unreachable, and 23 of 25 findings on this repo's own bundle were + # exactly that — a wall of unactionable lines the two real ones sat inside of. A report + # is read as a worklist whether or not it was written as one. + # `done` is the ONLY exclusion (A2): a node in `verify` is still advisable before its + # gate. An ABSENT status is NOT closed (A4, R:BLINDCLOSE) — hiding a finding on a + # malformed node is the failure this rule is meant to prevent, not an instance of it. + if fm.get("status") == "done": + continue if not fm.get("persona") and not fm.get("advised_by"): # Severity agrees with the gate floor (A2): security is a HARD gate refusal (R:NOCOVERAGE), # so doctor says `warn`; the softer data/architecture floors stay `info` nudges. diff --git a/add-method/tooling/engine_pin.py b/add-method/tooling/engine_pin.py index 12aa4e2b..57e568c8 100644 --- a/add-method/tooling/engine_pin.py +++ b/add-method/tooling/engine_pin.py @@ -17,7 +17,7 @@ this file only ever holds the newest pointer. """ -ENGINE_MD5 = "7932ec061ddf19310c138766a691bfdf" # re-aimed @ 3.4.0 release: the ENGINE version stamp. prior: 9a800083… @ scaffold-truth +ENGINE_MD5 = "28cb0c115ec30f58c2d498f3cfc0eb68" # re-aimed @ doctor-nudges-what-you-can-act-on: `unadvised_sensitive` reports only a node that can still take a lens. prior: 7932ec06… @ 3.4.0 # ADD 3.0 (ABF-1): the engine is a flat two-file pair (add.py + cli.py), no add_engine/ package. # ENGINE_PKG_MD5 is repurposed to pin the dispatch entry cli.py (the second engine file). ENGINE_PKG_MD5 = "b41bc148795c695b7f04ebd10c1098c1" # re-aimed @ scaffold-truth (`join` propagates its refusal to a non-zero exit). prior: ad04b73a… @ method-truth-sweep