Skip to content

feat(core): context-aware comment adjudication with evidence-backed verdicts - #2

Merged
ryanleecode merged 11 commits into
masterfrom
feat/sota-comment-adjudication
Aug 16, 2026
Merged

feat(core): context-aware comment adjudication with evidence-backed verdicts#2
ryanleecode merged 11 commits into
masterfrom
feat/sota-comment-adjudication

Conversation

@younna-ai-opencode

Copy link
Copy Markdown
Collaborator

feat(core): context-aware comment adjudication with evidence-backed verdicts

Summary

The classifier no longer treats comments as bare text: it consumes the structural context (adjacent code, scope, position, language) supplied by the Claude Code hook, and adjudicates per kind with cited evidence. Every verdict reason is checkable against the code:

Kind What the reason cites
RestatesCode Tokens shared with the adjacent statement, or a verb-to-operator match (increment ↔ +=)
NarratesControlFlow The verb (loop, iterate, retry…) that narrates the raw keyword (for, while, map…) the code shows
Changelog — (unchanged)
DeadCodeInComment — (unchanged)
TodoWithoutTicket — (unchanged)

Restatement never convicts on unreliable context: an Edit/MultiEdit fragment cannot vouch for the code around a comment, so the hook falls back and passes instead of blocking the user's write. Fail-open is deliberate (PreferDontConvict floor) and now documented.

Key decisions (settled, with plan)

  • Plan: docs/plans/2026-08-12-002-feat-sota-comment-adjudication-plan.md (implementation-ready, executed U1–U5).
  • Corpus: migrated from 50 unlabeled samples to 60 context-bearing cases — each row records text, code, position, scope, language, comment type, and kind — with per-kind and per-language F1 floors ≥ 0.85 plus a floor-regression test that trips when a single-case kind goes wrong.
  • Evidence synthesis runs on detected adjacency only: token containment / lexical overlap / operator matches; ref:/source: moved to the ATTRIBUTION pathway; 1-based/0-based intent markers recognized.
  • Public-API sparing now recognizes contract markup (# Returns:, @param, # panics) leading any comment kind at a contract position, not just docstrings.
  • String literals are masked before flow-construct keyword extraction, so print("for the win") can never be cited as a for construct.

Verification

  • One-shot gate (this session, on both review-fix head and simplify head): cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test -- --test-threads=4 — green.
    • 61 classifier + 10 context + 6 gate + 13 pipeline = 90 tests, incl. new containment-band, quote-masking, MultiEdit-fragment, and evidence-citation assertions.
  • Mutation gate: cargo mutants --file crates/comment-checker/src/classify.rs --timeout 90117/117 caught (0 missed, 4 unviable) on the fix head; 106/106 on the simplify head.

Review

  • Code review (31 merged findings, 6 reviewer personas) + independent validator batch: 15 probed, 12 validated, 13 applied; 3 rejected by evidence (file-size preference, unreachable-guard narrowing, public-API removal).
  • Remaining findings are advisory-only and report-matter (documented in the review run artifact).
  • Browser tests: N/A — pure Rust CLI hook, no web surface.

Post-Deploy Monitoring & Validation

None required: a local CLI hook, no network/telemetry. The F1 gate and mutation gate are the regression surface; the corpus is the human-reviewable fixture for both precision and recall claims.

Related

  • Follow-up idea (warrant: none — the git history of a comment block is not a comment property the hook sees; it would re-blame the file on every edit and re-files a stale check each time).

Known residual risks

  • Operator-only evidence can paint a coincidental match as proof; contained by corpus floors (KTD3 paraphrase mechanism).
  • NarratesControlFlow is exempt from text-only per-kind floors (the kind is production-only without context).
  • Huge comments are not length-capped; processing stays O(n).
  • eval/corpus.json is a tracked evaluation surface; label edits are governed by plan process.

Shipped by Compound Engineering — plan-driven, review-gated, verification-recorded. Feature branch feat/sota-comment-adjudication.

ryan added 11 commits August 13, 2026 03:38
…traction

Each detected comment now carries the structural context of the code it
annotates, and the classifier's judgements are constrained by it.

## Detection (src/detect.rs)

`CommentContext` records the annotated code, whether that code is a
declaration, the scope (module/function/nested-block), the position role
(docstring-head/leading/trailing/inline), and whether the context is
reliable.

Python docstrings are found by walking the tree rather than by a
tree-sitter query. The bundled grammar makes a docstring a direct
`string` child of `module`/`block` with no `expression_statement`
wrapper, so the query matched nothing and Python docstrings were never
detected at all. The walk accepts both shapes.

The walk no longer descends into comment nodes. tree-sitter-rust nests a
marker-stripped content node inside `///`, which was emitted as a second,
phantom comment whose adjacent code was `"/"`.

Adjacent code is the code the comment annotates: the next non-comment
sibling for a leading or head comment, the previous one for a trailing or
inline comment. It was previously taken from the previous sibling in all
cases, so a Go doc comment reported `package main` instead of the
function it documents.

A comment is a docstring head only when nothing but trivia precedes it in
its body — a doc comment on a class's second member is a leading comment.
The file root counts as a head slot, so module docstrings are recognised.
`PositionRole::DocstringHead` is positional only: a `#` line comment
first in a file occupies the slot without being a docstring, and the
`comment_type == Docstring` gate in the public-contract rule is what
keeps that safe.

## Classification (src/classify.rs)

`restates_adjacent` scores how much of a comment's vocabulary already
appears in the code it annotates. Structural context can only *narrow* a
judgement, never widen it: a docstring is a public contract when it
carries contract markup AND is positioned as one (head slot, or leading a
declaration) AND does not merely echo that declaration. Position alone
must never justify — the corpus labels a bare summary line
(`"""Fetch the user."""`) a restatement, and an earlier draft of this
rule contradicted that.

Commented-out code now recognises assignment shape (`x = x + 1`), which
single `=` did not previously match. The check is deliberately narrow:
the left side must be one identifier path, so `set retries = 3 because …`
and the legend `1 = enabled, 2 = disabled` are not mistaken for code.

Edit/MultiEdit fragments are marked unreliable, and an unreliable
catch-all `RestatesCode` is downgraded to `Justified(NonObviousIntent)`.
Fragment edges lose adjacent context, so only a concrete rule may convict
there; the `Write` path is unaffected.

## Measurement (eval/corpus.json, tests/)

`eval/corpus.json` is the single source of truth, carrying a `kind` per
case; the binary label is derived from it. The gate reports per-kind and
per-language precision/recall and asserts a per-kind precision floor, so
a weak kind cannot hide inside the aggregate.

`every_case_is_detectable_end_to_end` and
`detected_path_reaches_f1_threshold` run all 50 cases through the real
parse → detect → classify path. The text-only gate cannot see detector
defects; this one fails when detection breaks. It immediately found a
corpus data bug: two cases carried `language: "python"` with `//` text,
which is not a Python comment. Corrected to rust and javascript.

`tests/context.rs` pins the derived context per grammar — every case
there is a defect that shipped once.

`.cargo/config.toml` is committed because it is load-bearing:
`tree-sitter-language-pack` is declared with `default-features = false`,
so every grammar comes from `TSLP_LANGUAGES`. Without it CI compiles zero
grammars and detection silently returns nothing.

## Verification

  cargo fmt --check                    clean
  cargo clippy --all-targets -D warns  clean
  cargo test --all-targets             58 passed
                                       (37 classify, 9 context, 3 f1, 9 pipeline)
  cargo mutants (src/classify.rs)      59 caught / 4 unviable / 0 missed

Detected-path per-kind precision: 48/50 cases kind-exact; the two
remaining are binary-label-equivalent (`// ref: https://…` scores
NonObviousIntent over Attribution).

The CI mutation step's path is corrected to the crate's real location.
Each corpus case now carries the adjacent code it annotates, its position
and scope, so the gate exercises real parse->detect->classify against
context-bearing code instead of comment text in isolation.

- eval/corpus.json: 50 cases re-authored with authored context; labels
  re-verified under the kind-level rubric. Attribution vs intent marker
  precedence fixed: 'ref:'/'source:' provenance now classifies as
  Attribution, not NonObviousIntent (the old per-kind view hid a 2-case
  kind with no floor to trip).
- tests/common: schema grows (code, position, scope); synthesize_source
  embeds the authored context; evaluate() reports per-kind actual /
  predicted / correct (recall now has a denominator) and per-language
  tp/fp/fn; per_kind_violations asserts precision and recall floors on
  every kind with at least 2 cases.
- tests/f1: both the detected and the text-only path gate F1 >= 0.85 and
  the per-kind floors; malformed corpus fails loudly; zero-case kinds
  report gracefully; a crafted regression proves a kind-level failure
  trips the floor while the overall F1 stays above threshold.
- A comment inside a loop body reports Scope::NestedBlock with the
  loop statement as its adjacent code.
- A real Edit payload that introduces a would-be restatement passes: the
  fragment-edge context is marked unreliable, the classifier falls back to
  the text-only floor, and the hook never convicts on context the fragment
  cannot vouch for.
…able (U3)

RestatesCode is no longer a hand-waved catch-all: on reliable structural
context the verdict carries the cited overlap (U3, KTD3).

- comment.rs: UnnecessaryKind::RestatesCode now carries RestateEvidence
  (lexical tokens in comment order + (verb, operator) table matches);
  the empty-evidence form is the retained terminal text-only rule.
- classify.rs: restate_evidence runs only on reliable context and fires
  on >= 50% lexical containment OR a verb->operator table match
  (increment <-> +=, decrement <-> -=, returns <-> return, assign,
  add/subtract/multiply/divide, double/halve). Word-like operators match
  as whole tokens so 'add' cannot fire on 'address'. The operator table
  requires the operator to actually appear in the adjacent code.
  INTENT markers gain 1-based/0-based (constraint conventions).
- Attribution markers gain ref:/source: so provenance links classify as
  attribution, not generic intent (previously hidden by a 2-case kind
  with no display floor).
- Corpus: +5 context-bearing cases pinning the new behaviour, including
  an inline paraphrase and the precision moat (throttle comment spared).
- Report: the block reason cites the shared tokens and operator matches.
Two new branches in the restate detector needed dedicated pins after the
first mutation run left them unprotected:

- word-like operators match as whole tokens, not substrings
  (return_value cannot satisfy the 'return' operator);
- evidence requires >= 50% containment, not any nonzero overlap.

cargo mutants now reports 0 missed on classify.rs.
is_public_api_doc no longer requires the Docstring type: a line or block
comment whose contract markup leads the comment (after its marker) at a
contract position is interface documentation. Mid-sentence markup stays
unjustified, trailing/inline tags document nothing, and the text-only /
unreliable paths keep today's conservative docstring-only promotion.

The U3 docstring echo guard is unchanged for docstrings; a contract tag
on a line comment (the tag IS the contract) is not revoked for echoing the
declaration. Corpus gains a Ruby '# Returns: the user' case.
…uct (U5)

Corpus mining found the shape today's rules only catch through the
blanket terminal rule: a comment that narrates a loop/iteration the code
already expresses, e.g. '# loop over each item' beside 'for item in ...'.
It now gets its own kind and a checkable reason naming the construct.

- comment.rs: UnnecessaryKind::NarratesControlFlow { construct }.
- classify.rs: flow_narration fires when a flow verb (loop/iterate/...)
  in the comment matches a construct token (for/while/foreach/iter) in
  the reliable adjacent code, matched against raw keyword tokens because
  the stop-word list strips 'for'/'in'. Runs after the justification
  tables, so a loop comment explaining why (backoff, rate limit) is
  always spared; unreliable fragment context never convicts.
- Gate: NarratesControlFlow is asserted on the detected path and exempt
  from the text-only per-kind floor, where it correctly degrades to
  RestatesCode (no context, no construct).
- Corpus: 2 flow-narration cases + 1 precision case (retry-loop intent).
Three independent review passes (reuse, quality, efficiency) over the
U3-U5 diff; all findings applied behavior-preservingly; the mutation
gate stayed at 100% throughout (106/106 caught on the final run):

- leads_with_contract_markup reuses any_starts; CONTRACT_LEAD_MARKUP is
  now derived from DOC_MARKUP (minus @see/@author, plus @note/@warning)
  so the two tables cannot drift.
- One shared split_tokens + &str-based dedupe for the content-token and
  raw-keyword vocabularies (halves per-token allocations).
- restate/flow share reliable_adjacent, containment_ratio and the
  lexical-overlap helper; the classify fallback tokenizes the comment
  once and shares it between the flow and restate paths; flow only
  tokenizes the adjacent code when a flow verb is present.
- The lowercased adjacent-code copy is gone: symbolic operators are
  case-free and the only word-like operator matches via the token set.
- NarratesControlFlow now cites the (verb, construct) pair, making the
  verb-match equality observable to tests; flow_construct picks the
  first verbatim-matching verb row (previously a masked-equivalent
  double scan that three equality/negation mutants survived).
- KindMetrics::precision/recall unify matrix display and floor
  assertions; the corpus loader stores CommentType directly instead of
  a mirrored enum; the snippet builder special-cases only python
  function scope.
Twelve of fifteen review findings validated independent of the original
reviewers; all applied behavior-preservingly (the remaining three were
rejected: file-size preference, unreachable guard narrowing, and a public
API removal that would break library consumers).

- classify.rs: rewrite the stale module doc (the fallback is no longer a
  pure fold); mask string literals before flow-construct keyword
  extraction so print("for the win") cannot cite a phantom 'for'
  construct; anchor ref:/source: attribution tags to comment starts.
- tests: pin the 0.4 containment band (two-of-five shared tokens must stay
  empty), pin full-payload quote masking (two mutants survived until
  both payload shapes were exercised), add a corpus row for the
  string-literal precision case, exercise MultiEdit through the pipeline
  (restatement supplies a pass, while rules still block).
- gate: the detected path now asserts at least one restatement verdict
  cites evidence, so a detector that silently drops adjacent_code can no
  longer alias through both F1 gates; per-kind floors now trip when a
  single-case kind (GeneratedFile) goes wrong, closing the MIN_BUCKET
  blind spot.
- corpus: the fn-scope python docstring case now embeds a valid statement
  (return fetch_user(user_id)) instead of invalid Python.
- README: five kinds with the cited-evidence appendix, context-bearing
  60-case corpus, line/block lead-markup sparing, and the Edit
  restatement-disabled rule documented.

Verification: one-shot gate (fmt/clippy/90 tests) green; cargo mutants
117/117 caught (0 missed).
README now documents the five verdict kinds with their cited-evidence
reasons, the context-bearing 60-case corpus behind the F1 gate, the
line/block lead-markup public-API sparing rule, and the Edit/MultiEdit
restatement-disabled behavior. AGENTS.md and CLAUDE.md carry the repo's
distribution-layer governance updates.
Solves: how a deterministic classifier cites verified evidence, fails open
on fragment-unreliable context, and gates the wiring (F1 evidence assertion,
per-kind floors incl. the single-case-kind hole). Verified by 117/117
mutation, pipeline pins, and corpus floors; CONCEPTS.md seeded with the
classifier domain vocabulary.
@ryanleecode
ryanleecode merged commit 56dfe9e into master Aug 16, 2026
2 checks passed
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.

2 participants