fix(kyberforge): scope corroboration to a real sentence boundary

A prose-form routing target blocks a commit only when its own sentence names
another target that resolves. That makes the sentence splitter part of the
ADR-0020 contract rather than an implementation detail, and the naive
"period, space, capital" rule got it wrong in both directions:

- OVER-SPLIT: `e.g. "..."` is not a sentence end, but the quote looks like a
  start. The clause was cut in half and the corroborator stranded on the far
  side, so a genuinely dangling target silently demoted to SUGGESTION — a
  measurement taken and then discarded, the vacuous-green shape this gate
  exists to prevent. Seven such splits are live in the current corpus.
- UNDER-SPLIT: a sentence opening with a code span or a lowercase skill name
  was not seen as a start, so two sentences merged and a resolving target
  vouched for an unresolvable one it never stood beside — a hard FAIL with no
  escape hatch, which is the exact failure corroboration was added to prevent.

The splitter now excludes the five abbreviations that occur in routing prose
and admits a backtick or lowercase letter as a sentence opener. Applied
byte-identically to all three copies of the shared resolver.

Verified zero-delta against the corpus: 37 ERROR / 58 SUGGESTION / 2 dangling
before and after, findings byte-identical. The exposure this closes is to the
descriptions #99 is about to rewrite, not to the ones already measured — which
is why the deferral reason recorded on PR #103 ("can move the documented corpus
counts") does not hold and the fix lands here rather than after the retrofit.

Three regression tests, one per direction plus the backtick opener, each proven
non-vacuous by reverting the splitter alone and watching it go red.

Refs: #99
ADR: 0020

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-16 20:44:58 +00:00
parent 79c9089122
commit 75a13c82f6
7 changed files with 142 additions and 5 deletions

View File

@@ -564,6 +564,30 @@ grammar_case tp-backticked errors "routes to 'no-such-backticked-skill'" \
grammar_case tp-bare-terminal errors "routes to 'no-such-bare-skill'" \
"Use when doing the thing. Do not use for improvements — use sibling-skill or no-such-bare-skill instead."
echo ""
echo "--- corroboration is scoped to a REAL sentence, not to whatever the splitter says ---"
# Corroboration decides SUGGESTION vs blocking ERROR, so a mis-placed sentence
# boundary moves a target between the two tiers. The naive "period, space,
# capital" rule got this wrong in both directions, and both were live:
#
# OVER-SPLIT. `e.g. "..."` is not a sentence end, but the quote looks like a
# start. The clause was cut in half and the corroborator stranded on the far
# side, so a target that DOES sit beside a resolving sibling silently demoted
# to SUGGESTION — a measurement taken and then discarded.
grammar_case abbrev-split errors "routes to 'no-such-abbrev-skill'" \
"Use when doing the thing. Do not use for improvements — use sibling-skill first, e.g. \"run the audit\", then use no-such-abbrev-skill instead."
#
# UNDER-SPLIT. A sentence opening with a lowercase word or a code span was not
# seen as a start at all, so two sentences merged and a resolving target in the
# FIRST vouched for an unresolvable one in the SECOND that it never stood
# beside — a hard FAIL with no escape hatch, which is the exact failure
# corroboration was added to prevent. The target must still be REPORTED; only
# the power to block is withdrawn.
grammar_case lowercase-start suggests "routes to 'no-such-lower-skill'" \
"Use when doing the thing. Use sibling-skill for the main case. do not use for improvements — use no-such-lower-skill instead."
grammar_case backtick-start suggests "routes to 'no-such-tick-skill'" \
"Use when doing the thing. Use sibling-skill for the main case. \`no-such-tick-skill\` is not for this — do not use it instead."
# And the confirming half of the grammar rule: a compound-modifier target is
# CONFIRM-ONLY, not ignored. When the name does exist it still counts as a route
# — the rule suppresses the ERROR, it does not delete the target.