feat(coding): quote the body's trigger paragraph in label findings (#331) - #332
Conversation
) When a check's edge label is empty or isn't a condition, the text that should be in the label is usually a few lines away in the node body. The finding now quotes it, so whoever fixes it has the source material without opening each node. On hadronmemory.com::hadron-server (hadron-server#845): review:entity-ref-args error label-is-condition edge label is the bare stem "Applies when" with no condition after it — the body states its scope, condense it into the label: "Adding or renaming a GraphQL argument (query OR mutation) that identifies an existing entity, or widening an existing arg to accept a URN. Skip for arguments…" Two body conventions are recognised, measured across all 90 review checks in the four memories that have them: 55 use `> **Scope.** …`, 2 a line-initial `**Applies when** …`, 33 neither. So a trigger is findable for ~63% of checks; the rest gain nothing rather than carrying a "none found" line, which would be noise on a third of every run. Quoted, NOT promoted into the label. Those paragraphs run a median of 238 characters (max 567) against a median healthy edge label of 84 (p90 142), so feeding one to --fix would mint a label 3x too long and, since the edge loc is slugified from the name, an enormous derived loc with it. --fix keeps reading only the description, which must already read as a trigger. The quote is truncated to 160 chars by default; --suggest prints it whole. Presentation only: a test asserts the hint changes neither which rules fire, their severities, nor the --fix plan. Content costs no extra round trip — the shared NodeBatch operation already selects it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
There was a problem hiding this comment.
Pull request overview
This PR enhances the hadron coding review lint UX by appending a quote from a check node’s body when label-related findings fire, so the likely intended trigger text is visible directly in the lint output (without changing rule behavior, severities, --fix behavior, or the --json shape).
Changes:
- Add body parsing to extract a “trigger/scope paragraph” from node content (
> **Scope.** …or line-initial**Applies when** …) and append it tolabel-present/label-is-conditionfinding messages (truncated by default, full with--suggest). - Extend the lint model to carry node
ContentfromNodeBatch, and add a--suggestflag to control truncation. - Add unit tests covering body-trigger extraction, “stays quiet” behavior when no trigger is present, and determinism/
--fixnon-interference.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/cmd/coding/review_lint.go | Adds --suggest plumbing and appends body-derived trigger hints to label findings. |
| internal/cmd/coding/review_lint_test.go | Adds tests ensuring label findings quote body trigger text and remain behaviorally deterministic. |
| internal/cmd/coding/coding.go | Extends checkNode to include Content and populates it from NodeBatch results. |
| internal/cmd/coding/bodytrigger.go | New helper for extracting and truncating/quoting trigger paragraphs from node body content. |
| internal/cmd/coding/bodytrigger_test.go | New unit tests for body-trigger parsing and truncation behavior. |
| internal/cmd/agentic/agentic-usage.md | Updates CLI usage synopsis to include --suggest for coding review lint. |
| docs/plans/coding-command-group.md | Updates the plan doc with the new behavior and correct rule naming reference. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for i, ln := range lines { | ||
| if m := reScopeMarker.FindStringSubmatch(ln); m != nil { | ||
| return joinParagraph(m[1], lines[i+1:], true) | ||
| } | ||
| if m := reAppliesMarker.FindStringSubmatch(ln); m != nil { |
There was a problem hiding this comment.
Correct — fixed in 21f1b04. The regex makes the > optional but bodyTrigger hardcoded quoted=true, so a plain **Scope.** paragraph lost everything after its first line.
Continuation now follows the line actually matched:
quoted := strings.HasPrefix(strings.TrimSpace(ln), ">")
return joinParagraph(m[1], lines[i+1:], quoted)Worth noting it's latent rather than biting: I checked all four memories and every one of the 57 Scope markers is blockquoted, 0 plain. So nothing changes on today's data — but the regex and the continuation logic genuinely disagreed, and that's the kind of thing that surfaces the first time someone writes the marker without the >. TestBodyTriggerPlainScopeWraps covers both forms, including that the blockquoted one still stops at the end of its blockquote.
| if n.Content != nil { | ||
| cn.Content = *n.Content | ||
| } |
There was a problem hiding this comment.
Fair — fixed in 21f1b04. fetchNodes now takes wantContent bool: review lint passes true, preflight lint false.
To be precise about the cost, since it shaped how I framed it in the PR: the bytes were already on the wire either way — the shared NodeBatch operation selects content unconditionally, so this was never extra I/O. What it did do was retain a body per route target in the checkNode map for the run instead of letting it go with the response, which on mmdata's 71 routes is real if not dramatic. Opting out is free and makes the intent explicit, so it's worth doing regardless.
TestCodingFetchNodesContentIsOptIn asserts both directions: review lint quotes the body scope, preflight lint never surfaces it given the identical payload.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ddc5a47742
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if i := strings.LastIndexAny(cut, " \t"); i > n*3/4 { | ||
| cut = cut[:i] |
There was a problem hiding this comment.
Compare the word boundary in rune units
For non-ASCII scope text, LastIndexAny returns a byte offset while n*3/4 is a rune count. A 160-rune CJK trigger whose last space occurs after 50 CJK characters therefore treats byte offset 150 as being near the 160-rune limit and truncates the suggestion to roughly 50 runes. Convert the boundary offset to a rune count before applying the 75% threshold so multilingual hints retain the intended amount of context.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmed and fixed in 21f1b04. I reproduced it before changing anything:
input runes=251 output runes=51 // truncateRunes(s, 160)
with s = strings.Repeat("経", 50) + " " + strings.Repeat("路", 200) — exactly your scenario. The space sits at rune 50, byte offset ~150, which passes 150 > 120 and cuts a 160-rune request down to 51.
Both the search and the threshold are now in runes:
cut := r[:n]
for i := len(cut) - 1; i > n*3/4; i-- {
if cut[i] == ' ' || cut[i] == '\t' { cut = cut[:i]; break }
}TestTruncateRunesNonASCIIKeepsContext covers it and asserts ASCII word-boundary behaviour is unchanged. My existing test missed this because it used strings.Repeat("é", 50) with no space in it, so the backoff branch never ran.
…t-in content Review follow-ups on #332. - truncateRunes compared a BYTE offset against a rune count: LastIndexAny returns bytes while the 3/4 threshold is in runes, so multi-byte text was over-truncated. Verified — a 160-rune CJK trigger whose only space sits at rune 50 (byte ~150) came back as 51 runes. Both the search and the threshold are now in runes. - reScopeMarker makes the blockquote `>` optional, but bodyTrigger always asked joinParagraph for blockquote continuation, so a plain `**Scope.**` paragraph lost everything after its first line. Continuation now follows the line actually matched. Latent rather than observed: all 57 Scope markers in the live memories are blockquoted. - fetchNodes populated Content for every caller. Only `review lint` reads it; `preflight lint` was retaining a body per route target for nothing. Content is now opt-in per call, with a command test asserting review lint surfaces the body scope and preflight lint never does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…) (#339) * feat(cmdutil): accept a bare node id wherever a node ref is taken (#336) The CLI printed node ids on every --json surface (`id`, and `otherNodeId` on each edge) and then refused to take one back, so reading a node by an id it had just given you meant dropping to `hadron api` with hand-written GraphQL. That gap produced a wrong conclusion in this repo, not just friction: probing tasks:build-review-coverage by loc failed, I recorded it as a live instance of the list-vs-read visibility gap in docs/plans/coding-command-group.md, and had to retract it in #332 once reading by id returned the node. The same root cause was a real bug in `coding preflight lint` — rebuilding a URN per route target from the router's memory broke cross-memory routes — also fixed in #332. Both ref paths now accept one: - ResolveNodeURN short-circuits and returns the id. It does NOT round-trip through resolveUrn, which returns null for a bare id (verified against the live server); node(ref:) and nodeBatch(refs:) both accept ids directly. - BatchNodeRef passes it through verbatim to nodeBatch(refs:). Matched by SHAPE (32 lowercase hex), not by "contains no colon". Colon-free alone would have swallowed a bare loc typed without -m: `node get start-here` would have degraded from a usage error naming -m into a bare "not found". A test pins that message. Kind isn't verified for a bare id — that would need the round-trip resolveUrn can't serve — so a wrong-kind id fails at the caller's own read. Same trade resolveMemoryID already makes for raw memory ids. Verified live: `node get <id>`, `edge ls <id>`, and the batch path `node get <id> <urn>` all resolve; a bare loc without -m still gets its -m guidance. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(cmdutil): let a bare id win before -m composition Review follow-up on #339. BatchNodeRef and ResolveNodeRef applied -m/--memory composition before the id check, so `node get <id> -m <memory>` treated the id as a loc, composed it into a node URN, and looked up a node that cannot exist. An id is unambiguous by shape, so it now short-circuits ahead of -m in both; -m is simply redundant alongside one. Verified live: `node get <id> -m …::specs` resolves. Also records, in IsNodeID's doc and a test, why the gate is NOT widened to the CUID the schema names alongside 32-char hex. A CUID-shaped rule (letter-led lowercase alphanumeric) cannot be told apart from an ordinary loc: across the 907 nodes sampled in four memories it would capture 16 real ones — `preflight`, `instructions`, `conventions`, `findings` among them — so `node get preflight` would stop saying "pass -m" and start saying "not found". No CUID-shaped id exists on this server either (907 nodes, 45 memories, 10 revisions: all 32-hex), and a CUID-backed node stays addressable by URN, so the narrow gate removes nothing while widening would break refs that work today. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Holger Selover-Stephan <holger@baragaun.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Closes #331.
When a check's edge label is empty or isn't a condition, the text that should be in the label is usually sitting a few lines away in the node body. The finding now quotes it.
Live, against the memory that motivated the issue (hadron-memory/hadron-server#845):
Quoted, not promoted
This is the part #331 exists to get right. Two body conventions are recognised, measured across all 90 review checks in the four memories that have them:
> **Scope.** …**Applies when** …So a trigger is findable for ~63% of checks. The other third gain nothing — a "none found" line on every one of them would be noise.
And the text is not fed to
--fix:> **Scope.**paragraphs**Applies when**paragraphsPromoting one verbatim would mint a label 3× too long and — since the edge loc is slugified from the name — an enormous derived loc with it.
--fixstill reads only the description, which must already read as a trigger. The quote truncates to 160 chars (just above the p90 healthy label, so what you see is roughly the size of the label being asked for);--suggestprints it whole.Presentation only
A test asserts the hint changes neither which rules fire, their severities, nor the
--fixplan — the same input with and without a body scope produces identical findings apart from the message text, and an identical fix plan.No new rule, no severity change, no writes, and the
--jsoncontract is unchanged (the text goes in the existingmessage). Content costs no extra round trip either: the sharedNodeBatchoperation already selects it, so the bytes were on the wire before this change.Verification
go test ./...— 16 packages green.make lint— 0 issues.gofmt -l .— clean (the new gate from #330 is live).Read-only live runs:
One thing worth noting: mmdata now reports 4 errors rather than the 3 recorded in the plan doc. That is new data, not a regression —
review:exception-lists-not-patterns-in-ratchetswas created 2026-07-31T19:01Z with achild-oflabel. I confirmed by running the pre-change binary built from mergedmain, which reports the same 4.🤖 Generated with Claude Code