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'(?