This is an automated email from the ASF dual-hosted git repository. spmallette pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit ee38db59492fc7e79da6efbbf7ab7ad5e25dccb9 Author: Stephen Mallette <[email protected]> AuthorDate: Sat Oct 10 15:14:31 2026 -0400 Record bead reasoning in description instead of design bd can search a bead's description but not its design, so PRIME.md, tinker-review, the beads helper scripts and the committer docs now use description for reasoning. Assisted-by: Claude Code:claude-opus-5-5 --- .beads/PRIME.md | 20 ++++++++++++-------- .skills/tinker-review/playbooks/general.md | 8 ++++---- .skills/tinker-review/references/enrichment-cli.md | 2 +- .skills/tinker-review/references/schema.md | 4 ++-- .skills/tinker-review/scripts/discovery/beads.js | 6 ++---- .skills/tinker-review/scripts/enrichment/api.js | 4 ++-- .../tinker-review/scripts/graph/populate-beads.js | 1 - .skills/tinker-review/scripts/renderer/render.js | 8 ++++---- .skills/tinker-review/scripts/renderer/template.html | 2 +- .skills/tinker-review/test/beads.test.js | 5 +++-- bin/beads-commits.py | 4 ++-- bin/beads-report.py | 16 ++++++---------- docs/src/dev/developer/for-committers.asciidoc | 5 ++--- 13 files changed, 41 insertions(+), 44 deletions(-) diff --git a/.beads/PRIME.md b/.beads/PRIME.md index 68fd335cd5..3571cecb51 100644 --- a/.beads/PRIME.md +++ b/.beads/PRIME.md @@ -56,20 +56,24 @@ OptionsStrategy" describes code and stays a comment. ```bash # Only when something was actually ruled out. No fork = implementation; the code documents that. bd create --type=decision --parent=<root> --title="Chose X" --metadata '{"rejected":false}' \ - --design="why, and what X rules out" + --description="why, and what X rules out" # An approach you tried and abandoned is the strongest sibling — someone already walked it. bd create --type=decision --parent=<root> --title="Y" --metadata '{"rejected":true}' \ - --design="what Y concretely was, why it lost, what settled it" + --description="what Y concretely was, why it lost, what settled it" bd dep add <decision> <alternative> -t related # never put either of these on a task bead bd close <decision> <alternative> # both: a decision is resolved the moment you write it ``` +**Reasoning goes in `--description`, never `--design`.** `bd search --desc-contains` and +`bd query description=` find text in a description; nothing in bd searches design, so +reasoning written there can be read but never found. + **Close a decision as you create it.** Left `open` it lands in `bd ready`, advertising reasoning as startable work, and its `closed_at` ends up stamped with the merge date — dating the decision to a day it was not made. It gets pinned with the rest of the subtree at merge (section 4). -**A rejected alternative's `--design` names three things: what the option concretely was, why it +**A rejected alternative's `--description` names three things: what the option concretely was, why it lost, and what settled it.** The verdict is already in `rejected`, so the reason is the whole value — give it as a mechanism ("it leaves the shared database with a vocabulary no single PRIME.md describes"), never a judgment ("rejected as worse"). A mechanism can be checked again @@ -80,8 +84,8 @@ judgment cannot, so the option stays dead by default. The chosen decision needs reason is indistinguishable from a recorded one, so a guess does not read as a guess three years later — it reads as fact and gets trusted. "The operator preferred the other one" is the verdict again, not a reason. Ask what the chosen option buys and what the rejected one would have cost, -and write that answer. Ask while the conversation is live: section 5 forbids editing a design in -place, so a reason invented now is permanent. Doubt is the trigger — if you are reconstructing +and write that answer. Ask while the conversation is live: section 5 forbids editing a decision's +description in place, so a reason invented now is permanent. Doubt is the trigger — if you are reconstructing rather than recalling, you are guessing. **Cite code as `file[sha]`** — `GryoPool.java[bece4a34c7]`, with enough path to be unambiguous @@ -223,9 +227,9 @@ Pinning is what makes a bead permanent — every destructive operation keys on ## 5. Never - **Never `bd flatten`, `bd compact`, or `bd admin compact`.** They rewrite or discard - history irreversibly. `admin compact` destroys `--design` text specifically. `bd gc` only + history irreversibly. `bd gc` only with `--skip-decay`. -- **Never edit an existing bead's `--design` in place.** Add a comment, or create a new +- **Never edit an existing decision's `--description` in place.** Add a comment, or create a new decision bead with a `supersedes` edge. Field rewrites are invisible to history and lose the reasoning that was there. - `bd prune` / `bd purge` / `bd gc` are release-time maintainer operations. Don't run them. @@ -342,7 +346,7 @@ bd show <id> # one bead with dependencies bd query "status=open AND type=decision" # should be empty; an open decision was left unclosed bd query "metadata.rejected=true" --all # roads not taken; --all or closed beads are hidden bd comment <id> "..." # a fact with no fork in it (never on a task bead) -bd create --type=... --parent=<root> --design=... --labels=... +bd create --type=... --parent=<root> --description=... --labels=... bd dep add <task> <blocker> # default type is blocks: <task> waits for <blocker> bd dep add <a> <b> -t caused-by|related|discovered-from|supersedes bd dep cycles # a plan with a cycle cannot execute diff --git a/.skills/tinker-review/playbooks/general.md b/.skills/tinker-review/playbooks/general.md index 2387c8a081..60042490a1 100644 --- a/.skills/tinker-review/playbooks/general.md +++ b/.skills/tinker-review/playbooks/general.md @@ -24,7 +24,7 @@ confidence pass so the `governs` edges they write go through it: - `listBeads` — read the roots, decisions (each chosen one with the alternatives it beat) and tasks. The `primary` root is the PR's own work; `owner` and `shared` roots are connected work that explains it. -- `linkBead` — link each decision whose design is about changed code to that +- `linkBead` — link each decision whose reasoning is about changed code to that Function, Type or File, chosen and rejected alike. ## Inspect @@ -46,9 +46,9 @@ confidence pass so the `governs` edges they write go through it: - Test helpers without guard clauses (missing else/throw for invalid input) **Project memory** (only when beads were found; read decisions by `listBeads`): -- Chosen decisions are claims to verify, not reasoning to accept. A design - states facts — a mechanism, an invariant, a measurement, what the choice rules - out. Check each against the code, and against the build or the functional test +- Chosen decisions are claims to verify, not reasoning to accept. A decision's + description states facts — a mechanism, an invariant, a measurement, what the + choice rules out. Check each against the code, and against the build or the functional test where the claim is about behavior. Does the diff do something the decision rules out? Judge against the newest decision in a `supersededBy` chain. - Rejected alternatives — each answers "why not X?" only if the reason X lost diff --git a/.skills/tinker-review/references/enrichment-cli.md b/.skills/tinker-review/references/enrichment-cli.md index c23a4eaa42..9eabe83a1d 100644 --- a/.skills/tinker-review/references/enrichment-cli.md +++ b/.skills/tinker-review/references/enrichment-cli.md @@ -153,7 +153,7 @@ proposal`. ### linkBead Records that a bead — usually a decision — governs a piece of the changed code, as a `governs` edge (Bead → Function, Type or File). Draw it when a decision's -design is about that code, so Inspect checks the code against what the decision +reasoning is about that code, so Inspect checks the code against what the decision chose and ruled out, and the report can cite the decision beside it. Key a Function/Type by `--name` (add `--file` when the name repeats) and a File by `--file`. Defaults to `INFERRED`: the link is your judgment, not a bd fact. diff --git a/.skills/tinker-review/references/schema.md b/.skills/tinker-review/references/schema.md index 26fd9de4a1..026df0e9a7 100644 --- a/.skills/tinker-review/references/schema.md +++ b/.skills/tinker-review/references/schema.md @@ -111,13 +111,13 @@ A comment on a Discussion. ### Project memory -**Bead** `{ beadId, type, status, title, description, design, labels, root, rejected?, parent?, externalRef?, closedAt?, rootRole? }` +**Bead** `{ beadId, type, status, title, description, labels, root, rejected?, parent?, externalRef?, closedAt?, rootRole? }` A bead from the reviewer's local beads database (`bd`), loaded because a record bead's external ref names one of the PR's artifacts (see `discovery/beads.js`). Every bead in the subtree of each root reached that way is loaded. `type` is bd's issue type (`epic`, `feature`, `task`, `bug`, `decision`, `record`, …); `root` is the id of the root the bead belongs to. `rejected` is set only on -decisions — `true` marks the road not taken, and `design` then says what the +decisions — `true` marks the road not taken, and `description` then says what the option was, why it lost and what settled it. `externalRef` is set only on records. `labels` is the comma-joined label list. diff --git a/.skills/tinker-review/scripts/discovery/beads.js b/.skills/tinker-review/scripts/discovery/beads.js index 9896a1bd52..0e7eee2a1d 100644 --- a/.skills/tinker-review/scripts/discovery/beads.js +++ b/.skills/tinker-review/scripts/discovery/beads.js @@ -46,9 +46,8 @@ export const BEADS_REQUIRED_MESSAGE = * expected to be open (it closes at merge), and decisions are closed as they are written, so * status means most on tasks: `in_progress` is claimed, unfinished work. * @property {string} title - * @property {string} description - * @property {string} design - For a decision, the reasoning: why it was chosen, or what the rejected - * option was, why it lost and what settled it. + * @property {string} description - For a decision, the reasoning: why it was chosen, or what the + * rejected option was, why it lost and what settled it. * @property {boolean|null} rejected - On a decision, true marks the road not taken; null on other types. * @property {string[]} labels * @property {string|null} parent - Parent bead id; null on a root. @@ -167,7 +166,6 @@ function toBead(raw) { status: raw.status, title: raw.title || "", description: raw.description || "", - design: raw.design || "", rejected: raw.issue_type === "decision" ? raw.metadata?.rejected === true : null, labels: raw.labels || [], parent: raw.parent || null, diff --git a/.skills/tinker-review/scripts/enrichment/api.js b/.skills/tinker-review/scripts/enrichment/api.js index 2952ac251c..fc0dc14630 100644 --- a/.skills/tinker-review/scripts/enrichment/api.js +++ b/.skills/tinker-review/scripts/enrichment/api.js @@ -220,7 +220,7 @@ export async function listBeads(g, filter = {}) { type: one(m, "type"), status: one(m, "status"), title: one(m, "title"), - design: one(m, "design") || "", + description: one(m, "description") || "", root: one(m, "root"), rootRole: one(m, "rootRole") ?? null, rejected: one(m, "rejected") ?? null, @@ -397,7 +397,7 @@ export async function linkDiscussion(g, url, source, title, body, confidence) { /** * Record that a bead — usually a decision — governs a piece of the changed * code, as a `governs` edge (Bead → Function, Type or File). Draw it when a - * decision's design is about that code, so Inspect can check the code against + * decision's reasoning is about that code, so Inspect can check the code against * what the decision chose and ruled out, and the report can cite the decision * beside it. Key a Function or Type by `--name` (plus `--file` when the name * repeats) and a File by `--file`. diff --git a/.skills/tinker-review/scripts/graph/populate-beads.js b/.skills/tinker-review/scripts/graph/populate-beads.js index a772c3b972..650603f650 100644 --- a/.skills/tinker-review/scripts/graph/populate-beads.js +++ b/.skills/tinker-review/scripts/graph/populate-beads.js @@ -72,7 +72,6 @@ export async function populateBeads(g, beads) { .property("status", b.status) .property("title", b.title) .property("description", (b.description || "").slice(0, 4000)) - .property("design", (b.design || "").slice(0, 4000)) .property("labels", (b.labels || []).join(", ")) .property("root", b.root); if (b.rejected !== null) t = t.property("rejected", b.rejected); diff --git a/.skills/tinker-review/scripts/renderer/render.js b/.skills/tinker-review/scripts/renderer/render.js index aa5dc2440f..012af4ef87 100644 --- a/.skills/tinker-review/scripts/renderer/render.js +++ b/.skills/tinker-review/scripts/renderer/render.js @@ -241,7 +241,7 @@ function beadStatus(b) { } function beadText(text) { - return text ? `<div class="bead-design">${esc(text)}</div>` : ""; + return text ? `<div class="bead-reasoning">${esc(text)}</div>` : ""; } function renderDecisions(decisions, beadsById) { @@ -271,11 +271,11 @@ function renderDecisions(decisions, beadsById) { const items = decisions.filter((d) => d.rejected === false).map((d) => { const alts = (rejectedOf.get(d.id) || []).map((a) => - `<li>${beadAnchor(a, `<span class="bead-rejected">Instead of</span> ${esc(a.title)} <code>${esc(a.id)}</code>${superseded(a)}`)}${beadText(a.design)}</li>`).join(""); - return `<li>${beadAnchor(d, `<strong>Chose</strong> ${esc(d.title)} <code>${esc(d.id)}</code>${superseded(d)}`)}${beadText(d.design)}${alts ? `<ul class="bead-alternatives">${alts}</ul>` : ""}</li>`; + `<li>${beadAnchor(a, `<span class="bead-rejected">Instead of</span> ${esc(a.title)} <code>${esc(a.id)}</code>${superseded(a)}`)}${beadText(a.description)}</li>`).join(""); + return `<li>${beadAnchor(d, `<strong>Chose</strong> ${esc(d.title)} <code>${esc(d.id)}</code>${superseded(d)}`)}${beadText(d.description)}${alts ? `<ul class="bead-alternatives">${alts}</ul>` : ""}</li>`; }); const unpaired = decisions.filter((d) => d.rejected === true && !paired.has(d.id)).map((d) => - `<li>${beadAnchor(d, `<span class="bead-rejected">Not taken</span> ${esc(d.title)} <code>${esc(d.id)}</code>${superseded(d)}`)}${beadText(d.design)}</li>`); + `<li>${beadAnchor(d, `<span class="bead-rejected">Not taken</span> ${esc(d.title)} <code>${esc(d.id)}</code>${superseded(d)}`)}${beadText(d.description)}</li>`); const chosen = decisions.filter((d) => d.rejected === false).length; return `<details class="bead-decisions"> <summary>Decisions: ${chosen} chosen, ${decisions.length - chosen} not taken</summary> diff --git a/.skills/tinker-review/scripts/renderer/template.html b/.skills/tinker-review/scripts/renderer/template.html index 8cdfe37c75..9b7ad60a60 100644 --- a/.skills/tinker-review/scripts/renderer/template.html +++ b/.skills/tinker-review/scripts/renderer/template.html @@ -60,7 +60,7 @@ pre code { background: none; padding: 0; } .bead-ref { border-color: #6f42c1; color: #6f42c1; } .bead-ref::after { content: " \2193"; } .bead-ref:hover { background: #6f42c1; color: #fff; } .bead-list { margin: 0.4rem 0 0.4rem 1.2rem; padding: 0; font-size: 0.875rem; } .bead-list li { margin-bottom: 0.45rem; } .bead-alternatives { margin: 0.3rem 0 0 1.2rem; padding: 0; list-style: none; border-left: 2px solid var(--border); padding-left: 0.6rem; } -.bead-design { font-size: 0.8rem; color: var(--text-muted); white-space: pre-wrap; margin-top: 0.15rem; } +.bead-reasoning { font-size: 0.8rem; color: var(--text-muted); white-space: pre-wrap; margin-top: 0.15rem; } .bead-rejected { color: var(--danger); font-weight: 600; } .bead-superseded { font-size: 0.75rem; color: var(--text-muted); font-style: italic; } .bead-status { font-size: 0.7rem; text-transform: uppercase; color: var(--text-muted); } .bead-decisions summary, .card details summary { cursor: pointer; font-weight: 600; font-size: 0.9rem; margin-top: 0.4rem; } diff --git a/.skills/tinker-review/test/beads.test.js b/.skills/tinker-review/test/beads.test.js index 7e0dbae873..6e3094c7ac 100644 --- a/.skills/tinker-review/test/beads.test.js +++ b/.skills/tinker-review/test/beads.test.js @@ -34,7 +34,7 @@ import { render } from "../scripts/renderer/render.js"; function bead(id, type, { parent = null, ref = null, rejected, deps = [], comments = 0, title = id } = {}) { return { id, issue_type: type, status: type === "decision" ? "closed" : "open", title, - description: "", design: `design of ${id}`, labels: ["gremlin-core"], parent, + description: `reasoning of ${id}`, labels: ["gremlin-core"], parent, external_ref: ref, closed_at: null, comment_count: comments, metadata: rejected === undefined ? null : { rejected }, dependencies: [ @@ -216,7 +216,7 @@ const meta = { pr: 99, headSha: "abc", title: "t", domains: [], timestamp: "now" function shaped(id, type, extra = {}) { return { - id, type, status: "closed", title: `title ${id}`, description: "", design: `why ${id}`, + id, type, status: "closed", title: `title ${id}`, description: `why ${id}`, rejected: type === "decision" ? false : null, labels: [], parent: null, externalRef: null, closedAt: null, root: "tp-a", edges: [], ...extra, }; @@ -249,6 +249,7 @@ test("Project Memory shows the root, its records, plan and decisions with altern assert.match(html, /Unanswered questions:[\s\S]*tp-a\.7/); assert.match(html, /Decisions: 2 chosen, 1 not taken/); assert.match(html, /Chose<\/strong> title tp-a\.2[\s\S]*superseded by[\s\S]*bead-alternatives[\s\S]*Instead of<\/span> title tp-a\.3/); + assert.match(html, /<div class="bead-reasoning">why tp-a\.2<\/div>[\s\S]*<div class="bead-reasoning">why tp-a\.3<\/div>/, "a decision's description is its reasoning"); assert.match(html, /a fact on the root/); assert.match(html, /<h3>Beads<\/h3>/, "the appendix lists the loaded beads"); for (const b of found.beads) { diff --git a/bin/beads-commits.py b/bin/beads-commits.py index b226480b3d..d74684714e 100755 --- a/bin/beads-commits.py +++ b/bin/beads-commits.py @@ -39,9 +39,9 @@ there to write its record. Recording the landing is the one step every root passes through, and by then GitHub knows which PR carried the commits. A PR recorded under another root is related to this one rather than duplicated. -The shas go in `notes` because that is the one free-text field bd can query. +The shas go in `notes` because that is a free-text field bd can query. `bd search` matches title, id and some external refs but never notes, and -`bd query` has no field for comments or design, so a sha recorded anywhere else +`bd query` has no field for comments, so a sha recorded in one is unreachable except by grepping an export. `bd query 'notes=...'` is a substring match, which is why the full 40-character sha is stored: a short sha pasted from `git log --oneline` is a prefix of it, so both forms of the query find the bead. Quote the sha inside the diff --git a/bin/beads-report.py b/bin/beads-report.py index 343ee2e010..720e790193 100755 --- a/bin/beads-report.py +++ b/bin/beads-report.py @@ -45,7 +45,8 @@ Checks divide into two kinds, and the distinction matters: "decisions were not captured", so they are questions for a human, never verdicts. -Neither kind can judge whether design text is real reasoning or fluent filler. +Neither kind can judge whether a decision's description is real reasoning or +fluent filler. Structure is checkable; substance is not. """ @@ -79,7 +80,7 @@ def as_list(payload): def load_export(): - """Whole database, one record per line. Carries design and comments.""" + """Whole database, one record per line. Carries comments.""" proc = subprocess.run(["bd", "export"], capture_output=True, text=True) if proc.returncode != 0: sys.exit(f"bd export failed: {proc.stderr.strip()}") @@ -99,10 +100,7 @@ def load_subtree(root): `bd children` recurses in its text output but NOT under `--json`, which returns direct children only -- so a single call silently truncates the - subtree to depth 1. Verified against a four-level tree at bd 1.1.2. - - The records returned carry no `design` field, only comment_count, so - rationale-at-risk detection is weaker in this scope than at release scope.""" + subtree to depth 1. Verified against a four-level tree at bd 1.1.2.""" beads = {} for record in as_list(bd("show", root)): beads.setdefault(record["id"], record) @@ -165,13 +163,11 @@ def main(): if bead.get("comment_count") or bead.get("comments"): n = bead.get("comment_count") or len(bead.get("comments") or []) carried.append(f"{n} comment(s)") - if (bead.get("design") or "").strip(): - carried.append("design text") + if bead.get("issue_type") == "decision" and (bead.get("description") or "").strip(): + carried.append("reasoning") if carried: at_risk.append(f"{bead['id']:18} {', '.join(carried):22} {bead['title'][:44]}") report("Rationale on closed beads (lost at next purge -- pin to keep)", at_risk) - if scoped: - print(" note: subtree scope cannot see `design` text; comment counts only") # --- HEURISTIC --------------------------------------------------------- diff --git a/docs/src/dev/developer/for-committers.asciidoc b/docs/src/dev/developer/for-committers.asciidoc index a56b5040f3..c0f813762f 100644 --- a/docs/src/dev/developer/for-committers.asciidoc +++ b/docs/src/dev/developer/for-committers.asciidoc @@ -290,9 +290,8 @@ landed there is refused rather than recorded, because it is precisely the hash t squash would invalidate. Running the command a second time finds the existing record and creates nothing. -The notes field is what makes the link work in reverse. It is the only free-text field that the -query language searches, so a hash held in a comment or in design text can be read but never -found. +The notes field is what makes the link work in reverse. The query language searches it, so a +hash held there can be found, while one held in a comment can be read but never found. [source,text] ----
