Skip to content

feat(coding): quote the body's trigger paragraph in label findings (#331) - #332

Merged
shadowbrush merged 2 commits into
mainfrom
claude/coding-lint-body-trigger
Aug 1, 2026
Merged

feat(coding): quote the body's trigger paragraph in label findings (#331)#332
shadowbrush merged 2 commits into
mainfrom
claude/coding-lint-body-trigger

Conversation

@shadowbrush

Copy link
Copy Markdown
Member

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):

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…"

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:

count
> **Scope.** … 55
line-initial **Applies when** … 2
neither 33

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:

min median p90 max
healthy edge labels (84) 32 84 142 179
> **Scope.** paragraphs 61 238 567
**Applies when** paragraphs 179 389 389

Promoting 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. --fix still 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); --suggest prints it whole.

Presentation only

A test asserts the hint changes neither which rules fire, their severities, nor the --fix plan — 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 --json contract is unchanged (the text goes in the existing message). Content costs no extra round trip either: the shared NodeBatch operation 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:

hadron coding review lint -m hadronmemory.com::hadron-server   → 3 label errors, each now quoting its scope
hadron coding review lint -m hadronmemory.com::hadron-server --suggest  → full paragraphs
hadron coding review lint -m hadronmemory.com::hadron-cli      → ✓ 6 check(s) OK
hadron coding review lint -m hadronmemory.com::hadron-portal   → ✓ 24 check(s) OK

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-ratchets was created 2026-07-31T19:01Z with a child-of label. I confirmed by running the pre-change binary built from merged main, which reports the same 4.

🤖 Generated with Claude Code

)

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>
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 to label-present / label-is-condition finding messages (truncated by default, full with --suggest).
  • Extend the lint model to carry node Content from NodeBatch, and add a --suggest flag to control truncation.
  • Add unit tests covering body-trigger extraction, “stays quiet” behavior when no trigger is present, and determinism/--fix non-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.

Comment on lines +46 to +50
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 {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread internal/cmd/coding/coding.go Outdated
Comment on lines +250 to +252
if n.Content != nil {
cn.Content = *n.Content
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@shadowbrush

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread internal/cmd/coding/bodytrigger.go Outdated
Comment on lines +101 to +102
if i := strings.LastIndexAny(cut, " \t"); i > n*3/4 {
cut = cut[:i]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@shadowbrush
shadowbrush merged commit f98f15e into main Aug 1, 2026
4 checks passed
@shadowbrush
shadowbrush deleted the claude/coding-lint-body-trigger branch August 1, 2026 08:13
shadowbrush added a commit that referenced this pull request Aug 1, 2026
…) (#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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

coding review lint: quote the body's trigger paragraph in label findings (and don't auto-promote it)

2 participants