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:
@@ -129,6 +129,14 @@ clause**, and a **boundary clause**. Capability enumeration, output-format detai
|
||||
dependency, and none needs to. Verified: a tree holding only `plugins/` and the root `apm.yml`,
|
||||
with no `.claude/` or `.agents/` anywhere, now produces findings identical to the working tree —
|
||||
26 description FAILs, 9 body FAILs, 2 dangling targets, 0 missing references, 58 SUGGESTIONs.
|
||||
- **Blocking is scoped to a sentence, which makes sentence boundaries load-bearing.** A prose-form
|
||||
target earns a hard error only when its own sentence names another target that *resolves*; route
|
||||
notation (`/name`, `→ name`) is exempt and always blocks. So the splitter is part of the contract,
|
||||
not a detail of it. `e.g. "…"` is not a sentence end, and a sentence opening with a code span or a
|
||||
lowercase skill name is a start; getting either wrong moves targets between the two tiers in
|
||||
opposite directions — a stranded corroborator silently demotes a real finding to SUGGESTION, and a
|
||||
missed boundary lets one sentence vouch for a target it never stood beside, producing a hard FAIL
|
||||
with no escape hatch.
|
||||
- **The blanket pushiness rules are deleted.** `skill-author/SKILL.md:104` and
|
||||
`description-quality.md:21` are replaced by a conditional: add an indirect trigger only where the
|
||||
user's natural phrasing genuinely omits the domain word — true for the `gitea-*` family, false for
|
||||
|
||||
@@ -554,7 +554,28 @@ BACKTICK = re.compile(r"`(%s)`" % NAME_HYPH, re.I)
|
||||
# ADR-0020's compressed arrow form `Not <thing> -> <name>`.
|
||||
BOUNDARY_MARKER = re.compile(r"\b(?:do\s+not|instead|rather\s+than|not\s+for)\b", re.I)
|
||||
BOUNDARY_ARROW = re.compile(r"\bnot\b[^.;]*?(?:->|→)", re.I)
|
||||
SENTENCE_SPLIT = re.compile(u'(?<=[.!?])\\s+(?=[A-Z"“(])')
|
||||
# Sentence boundaries decide the CORROBORATION scope above, so getting one wrong
|
||||
# is not cosmetic — it moves a target between SUGGESTION and blocking ERROR. Two
|
||||
# shapes common in these descriptions defeat the naive "period, space, capital"
|
||||
# rule, in OPPOSITE directions:
|
||||
# OVER-SPLIT. `e.g. "set up the manifest"` ends no sentence, but the quote
|
||||
# looks like one starting. The clause is cut in half, the corroborating
|
||||
# target lands on the far side of the cut, and a genuinely dangling target
|
||||
# silently demotes to SUGGESTION — the gate takes a measurement and then
|
||||
# throws it away, which is the vacuous-green shape this file exists to stop.
|
||||
# UNDER-SPLIT. A real sentence opening with a code span or a lowercase skill
|
||||
# name ("... Composes it. `gitea-prs` also uses it.") is not seen as a start
|
||||
# at all, so two sentences merge and a resolving target vouches for an
|
||||
# unresolvable one it never stood beside — a hard FAIL with no escape hatch,
|
||||
# which is exactly the failure the corroboration rule was added to prevent.
|
||||
# Both are closed here: the five abbreviations that actually occur in routing
|
||||
# prose are excluded as sentence ends, and the opener class admits a backtick or
|
||||
# a lowercase letter. Verified zero-delta on the current corpus (37 ERROR / 58
|
||||
# SUGGESTION / 2 dangling before and after) — this protects the descriptions
|
||||
# issue #99 is about to rewrite, not the ones already measured.
|
||||
SENTENCE_SPLIT = re.compile(
|
||||
u'(?<!\\be\\.g\\.)(?<!\\bi\\.e\\.)(?<!\\betc\\.)(?<!\\bvs\\.)(?<!\\bcf\\.)'
|
||||
u'(?<=[.!?])\\s+(?=[A-Za-z`"“(])')
|
||||
|
||||
# The token that may follow a route target without turning it into a compound
|
||||
# modifier: punctuation, end of sentence, a conjunction, a boundary word, or a
|
||||
|
||||
@@ -480,7 +480,28 @@ BACKTICK = re.compile(r"`(%s)`" % NAME_HYPH, re.I)
|
||||
# ADR-0020's compressed arrow form `Not <thing> -> <name>`.
|
||||
BOUNDARY_MARKER = re.compile(r"\b(?:do\s+not|instead|rather\s+than|not\s+for)\b", re.I)
|
||||
BOUNDARY_ARROW = re.compile(r"\bnot\b[^.;]*?(?:->|→)", re.I)
|
||||
SENTENCE_SPLIT = re.compile(u'(?<=[.!?])\\s+(?=[A-Z"“(])')
|
||||
# Sentence boundaries decide the CORROBORATION scope above, so getting one wrong
|
||||
# is not cosmetic — it moves a target between SUGGESTION and blocking ERROR. Two
|
||||
# shapes common in these descriptions defeat the naive "period, space, capital"
|
||||
# rule, in OPPOSITE directions:
|
||||
# OVER-SPLIT. `e.g. "set up the manifest"` ends no sentence, but the quote
|
||||
# looks like one starting. The clause is cut in half, the corroborating
|
||||
# target lands on the far side of the cut, and a genuinely dangling target
|
||||
# silently demotes to SUGGESTION — the gate takes a measurement and then
|
||||
# throws it away, which is the vacuous-green shape this file exists to stop.
|
||||
# UNDER-SPLIT. A real sentence opening with a code span or a lowercase skill
|
||||
# name ("... Composes it. `gitea-prs` also uses it.") is not seen as a start
|
||||
# at all, so two sentences merge and a resolving target vouches for an
|
||||
# unresolvable one it never stood beside — a hard FAIL with no escape hatch,
|
||||
# which is exactly the failure the corroboration rule was added to prevent.
|
||||
# Both are closed here: the five abbreviations that actually occur in routing
|
||||
# prose are excluded as sentence ends, and the opener class admits a backtick or
|
||||
# a lowercase letter. Verified zero-delta on the current corpus (37 ERROR / 58
|
||||
# SUGGESTION / 2 dangling before and after) — this protects the descriptions
|
||||
# issue #99 is about to rewrite, not the ones already measured.
|
||||
SENTENCE_SPLIT = re.compile(
|
||||
u'(?<!\\be\\.g\\.)(?<!\\bi\\.e\\.)(?<!\\betc\\.)(?<!\\bvs\\.)(?<!\\bcf\\.)'
|
||||
u'(?<=[.!?])\\s+(?=[A-Za-z`"“(])')
|
||||
|
||||
# The token that may follow a route target without turning it into a compound
|
||||
# modifier: punctuation, end of sentence, a conjunction, a boundary word, or a
|
||||
|
||||
@@ -554,7 +554,28 @@ BACKTICK = re.compile(r"`(%s)`" % NAME_HYPH, re.I)
|
||||
# ADR-0020's compressed arrow form `Not <thing> -> <name>`.
|
||||
BOUNDARY_MARKER = re.compile(r"\b(?:do\s+not|instead|rather\s+than|not\s+for)\b", re.I)
|
||||
BOUNDARY_ARROW = re.compile(r"\bnot\b[^.;]*?(?:->|→)", re.I)
|
||||
SENTENCE_SPLIT = re.compile(u'(?<=[.!?])\\s+(?=[A-Z"“(])')
|
||||
# Sentence boundaries decide the CORROBORATION scope above, so getting one wrong
|
||||
# is not cosmetic — it moves a target between SUGGESTION and blocking ERROR. Two
|
||||
# shapes common in these descriptions defeat the naive "period, space, capital"
|
||||
# rule, in OPPOSITE directions:
|
||||
# OVER-SPLIT. `e.g. "set up the manifest"` ends no sentence, but the quote
|
||||
# looks like one starting. The clause is cut in half, the corroborating
|
||||
# target lands on the far side of the cut, and a genuinely dangling target
|
||||
# silently demotes to SUGGESTION — the gate takes a measurement and then
|
||||
# throws it away, which is the vacuous-green shape this file exists to stop.
|
||||
# UNDER-SPLIT. A real sentence opening with a code span or a lowercase skill
|
||||
# name ("... Composes it. `gitea-prs` also uses it.") is not seen as a start
|
||||
# at all, so two sentences merge and a resolving target vouches for an
|
||||
# unresolvable one it never stood beside — a hard FAIL with no escape hatch,
|
||||
# which is exactly the failure the corroboration rule was added to prevent.
|
||||
# Both are closed here: the five abbreviations that actually occur in routing
|
||||
# prose are excluded as sentence ends, and the opener class admits a backtick or
|
||||
# a lowercase letter. Verified zero-delta on the current corpus (37 ERROR / 58
|
||||
# SUGGESTION / 2 dangling before and after) — this protects the descriptions
|
||||
# issue #99 is about to rewrite, not the ones already measured.
|
||||
SENTENCE_SPLIT = re.compile(
|
||||
u'(?<!\\be\\.g\\.)(?<!\\bi\\.e\\.)(?<!\\betc\\.)(?<!\\bvs\\.)(?<!\\bcf\\.)'
|
||||
u'(?<=[.!?])\\s+(?=[A-Za-z`"“(])')
|
||||
|
||||
# The token that may follow a route target without turning it into a compound
|
||||
# modifier: punctuation, end of sentence, a conjunction, a boundary word, or a
|
||||
|
||||
@@ -480,7 +480,28 @@ BACKTICK = re.compile(r"`(%s)`" % NAME_HYPH, re.I)
|
||||
# ADR-0020's compressed arrow form `Not <thing> -> <name>`.
|
||||
BOUNDARY_MARKER = re.compile(r"\b(?:do\s+not|instead|rather\s+than|not\s+for)\b", re.I)
|
||||
BOUNDARY_ARROW = re.compile(r"\bnot\b[^.;]*?(?:->|→)", re.I)
|
||||
SENTENCE_SPLIT = re.compile(u'(?<=[.!?])\\s+(?=[A-Z"“(])')
|
||||
# Sentence boundaries decide the CORROBORATION scope above, so getting one wrong
|
||||
# is not cosmetic — it moves a target between SUGGESTION and blocking ERROR. Two
|
||||
# shapes common in these descriptions defeat the naive "period, space, capital"
|
||||
# rule, in OPPOSITE directions:
|
||||
# OVER-SPLIT. `e.g. "set up the manifest"` ends no sentence, but the quote
|
||||
# looks like one starting. The clause is cut in half, the corroborating
|
||||
# target lands on the far side of the cut, and a genuinely dangling target
|
||||
# silently demotes to SUGGESTION — the gate takes a measurement and then
|
||||
# throws it away, which is the vacuous-green shape this file exists to stop.
|
||||
# UNDER-SPLIT. A real sentence opening with a code span or a lowercase skill
|
||||
# name ("... Composes it. `gitea-prs` also uses it.") is not seen as a start
|
||||
# at all, so two sentences merge and a resolving target vouches for an
|
||||
# unresolvable one it never stood beside — a hard FAIL with no escape hatch,
|
||||
# which is exactly the failure the corroboration rule was added to prevent.
|
||||
# Both are closed here: the five abbreviations that actually occur in routing
|
||||
# prose are excluded as sentence ends, and the opener class admits a backtick or
|
||||
# a lowercase letter. Verified zero-delta on the current corpus (37 ERROR / 58
|
||||
# SUGGESTION / 2 dangling before and after) — this protects the descriptions
|
||||
# issue #99 is about to rewrite, not the ones already measured.
|
||||
SENTENCE_SPLIT = re.compile(
|
||||
u'(?<!\\be\\.g\\.)(?<!\\bi\\.e\\.)(?<!\\betc\\.)(?<!\\bvs\\.)(?<!\\bcf\\.)'
|
||||
u'(?<=[.!?])\\s+(?=[A-Za-z`"“(])')
|
||||
|
||||
# The token that may follow a route target without turning it into a compound
|
||||
# modifier: punctuation, end of sentence, a conjunction, a boundary word, or a
|
||||
|
||||
@@ -564,7 +564,28 @@ BACKTICK = re.compile(r"`(%s)`" % NAME_HYPH, re.I)
|
||||
# ADR-0020's compressed arrow form `Not <thing> -> <name>`.
|
||||
BOUNDARY_MARKER = re.compile(r"\b(?:do\s+not|instead|rather\s+than|not\s+for)\b", re.I)
|
||||
BOUNDARY_ARROW = re.compile(r"\bnot\b[^.;]*?(?:->|→)", re.I)
|
||||
SENTENCE_SPLIT = re.compile(u'(?<=[.!?])\\s+(?=[A-Z"“(])')
|
||||
# Sentence boundaries decide the CORROBORATION scope above, so getting one wrong
|
||||
# is not cosmetic — it moves a target between SUGGESTION and blocking ERROR. Two
|
||||
# shapes common in these descriptions defeat the naive "period, space, capital"
|
||||
# rule, in OPPOSITE directions:
|
||||
# OVER-SPLIT. `e.g. "set up the manifest"` ends no sentence, but the quote
|
||||
# looks like one starting. The clause is cut in half, the corroborating
|
||||
# target lands on the far side of the cut, and a genuinely dangling target
|
||||
# silently demotes to SUGGESTION — the gate takes a measurement and then
|
||||
# throws it away, which is the vacuous-green shape this file exists to stop.
|
||||
# UNDER-SPLIT. A real sentence opening with a code span or a lowercase skill
|
||||
# name ("... Composes it. `gitea-prs` also uses it.") is not seen as a start
|
||||
# at all, so two sentences merge and a resolving target vouches for an
|
||||
# unresolvable one it never stood beside — a hard FAIL with no escape hatch,
|
||||
# which is exactly the failure the corroboration rule was added to prevent.
|
||||
# Both are closed here: the five abbreviations that actually occur in routing
|
||||
# prose are excluded as sentence ends, and the opener class admits a backtick or
|
||||
# a lowercase letter. Verified zero-delta on the current corpus (37 ERROR / 58
|
||||
# SUGGESTION / 2 dangling before and after) — this protects the descriptions
|
||||
# issue #99 is about to rewrite, not the ones already measured.
|
||||
SENTENCE_SPLIT = re.compile(
|
||||
u'(?<!\\be\\.g\\.)(?<!\\bi\\.e\\.)(?<!\\betc\\.)(?<!\\bvs\\.)(?<!\\bcf\\.)'
|
||||
u'(?<=[.!?])\\s+(?=[A-Za-z`"“(])')
|
||||
|
||||
# The token that may follow a route target without turning it into a compound
|
||||
# modifier: punctuation, end of sentence, a conjunction, a boundary word, or a
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user