From 75a13c82f6243176130a385d7e5f6aeccf2d5537 Mon Sep 17 00:00:00 2001 From: Defame1297 Date: Sun, 16 Aug 2026 20:44:58 +0000 Subject: [PATCH] fix(kyberforge): scope corroboration to a real sentence boundary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- ...l-description-and-body-context-contract.md | 8 +++++++ .../skills/agent-audit/scripts/validate.sh | 23 +++++++++++++++++- .../skills/skill-audit/scripts/validate.sh | 23 +++++++++++++++++- .../skills/agent-audit/scripts/validate.sh | 23 +++++++++++++++++- .../skills/skill-audit/scripts/validate.sh | 23 +++++++++++++++++- scripts/skill-size-check.sh | 23 +++++++++++++++++- tests/test-adr0020-targets.sh | 24 +++++++++++++++++++ 7 files changed, 142 insertions(+), 5 deletions(-) diff --git a/docs/adr/0020-skill-description-and-body-context-contract.md b/docs/adr/0020-skill-description-and-body-context-contract.md index 77efc66..601eb12 100644 --- a/docs/adr/0020-skill-description-and-body-context-contract.md +++ b/docs/adr/0020-skill-description-and-body-context-contract.md @@ -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 diff --git a/plugins/kyberforge/.apm/skills/agent-audit/scripts/validate.sh b/plugins/kyberforge/.apm/skills/agent-audit/scripts/validate.sh index 2e1882f..fc0f711 100755 --- a/plugins/kyberforge/.apm/skills/agent-audit/scripts/validate.sh +++ b/plugins/kyberforge/.apm/skills/agent-audit/scripts/validate.sh @@ -554,7 +554,28 @@ BACKTICK = re.compile(r"`(%s)`" % NAME_HYPH, re.I) # ADR-0020's compressed arrow form `Not -> `. 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'(? -> `. 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'(? -> `. 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'(? -> `. 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'(? -> `. 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'(?