3 Commits

Author SHA1 Message Date
9385c77ac7 Merge pull request 'feat(kyberforge): ADR-0020 context contract for skills and agents' (#103) from refactor/trim-skills-agents-context into main
Reviewed-on: https://git.dev.rkdr.net/Defame1297/holocron/pulls/103
Reviewed-by: Defame1297 <gitea@rkdr.net>
2026-08-16 21:20:02 +00:00
54d7bd80ba docs: rule host built-ins out of the routing target universe
Closes the second open design decision on PR #103. The `/compact` finding was
recorded as a false positive needing an allowlist or a suppression mechanism.
It is neither: the routing universe is the apm marketplace, so a target either
resolves to a skill or an agent or it does not resolve, and `/compact`,
`/clear` and `/init` are Claude Code slash commands with no counterpart in
Copilot CLI or Codex. `.apm/` source compiles for all three, so a
vendor-neutral description routing to one is a portability defect and the hard
FAIL is a true positive.

An allowlist was rejected for a concrete reason, not a stylistic one: it
answers a different question ("does this exist on some host?"), it cannot
answer that portably from a single source file, and it goes stale the next
time a host ships a command — reintroducing the same-commit-two-verdicts
failure ADR-0020 already closed for deployed trees.

Nothing is blocked today: zero of the 43 descriptions name a host built-in,
and an author who needs to mention one writes it un-slashed, which is not
route notation and carries no routing claim.

Recorded in ADR-0020 and in both author-facing contract references, so the
next agent reads the decision rather than "fixing" the gate.

ADR: 0020

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 20:45:19 +00:00
75a13c82f6 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>
2026-08-16 20:44:58 +00:00
11 changed files with 176 additions and 5 deletions

View File

@@ -129,6 +129,24 @@ 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`, 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 — 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. 26 description FAILs, 9 body FAILs, 2 dangling targets, 0 missing references, 58 SUGGESTIONs.
- **The universe is the apm marketplace, and nothing else.** A routing target resolves to a skill or
an agent, or it does not resolve. Host built-ins are deliberately outside it: `/compact`, `/clear`
and `/init` are Claude Code slash commands with no counterpart in Copilot CLI or Codex, so a
vendor-neutral `.apm/` description routing to one is a portability defect and the hard FAIL is a
true positive, not a false one. An allowlist of known built-ins was **rejected**: it answers a
different question ("does this exist on *some* host?"), it cannot answer that portably from a
single source file, and it goes stale the next time a host ships a command — reintroducing the
same-commit-two-verdicts failure the bullet above exists to close. An author who needs to mention
one writes it un-slashed (``the `compact` built-in``), which is not route notation and makes no
routing claim.
- **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 - **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 `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 user's natural phrasing genuinely omits the domain word — true for the `gitea-*` family, false for

View File

@@ -554,7 +554,28 @@ BACKTICK = re.compile(r"`(%s)`" % NAME_HYPH, re.I)
# ADR-0020's compressed arrow form `Not <thing> -> <name>`. # 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_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) 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 # 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 # modifier: punctuation, end of sentence, a conjunction, a boundary word, or a

View File

@@ -70,6 +70,12 @@ that package declares in `apm.yml` under `dependencies.apm`. A sibling plugin in
therefore resolves; a skill in an unrelated repo does not. A target outside that universe sends the therefore resolves; a skill in an unrelated repo does not. A target outside that universe sends the
router nowhere. Verify it before writing it — do not invent a plausible sibling. router nowhere. Verify it before writing it — do not invent a plausible sibling.
That universe is the apm marketplace and stops there. A **host built-in is not a routing target**:
`/compact`, `/clear` and `/init` are Claude Code slash commands with no counterpart in Copilot CLI
or Codex, and `.apm/` source compiles for all three, so routing to one is a portability defect. The
gate is right to fail it and there is no allowlist. If a built-in genuinely needs mentioning, write
it un-slashed — ``the `compact` built-in`` — which makes no routing claim and is not checked.
**Length.** 250 characters SUGGESTION, 400 characters FAIL, counting the frontmatter value only **Length.** 250 characters SUGGESTION, 400 characters FAIL, counting the frontmatter value only
with YAML folding resolved. Treat 250 as the target: the SUGGESTION tier is what moves the corpus with YAML folding resolved. Treat 250 as the target: the SUGGESTION tier is what moves the corpus
average, the FAIL tier only stops outliers. average, the FAIL tier only stops outliers.

View File

@@ -480,7 +480,28 @@ BACKTICK = re.compile(r"`(%s)`" % NAME_HYPH, re.I)
# ADR-0020's compressed arrow form `Not <thing> -> <name>`. # 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_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) 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 # 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 # modifier: punctuation, end of sentence, a conjunction, a boundary word, or a

View File

@@ -57,6 +57,12 @@ therefore resolves; a skill in an unrelated repo does not. A boundary clause nam
outside that universe sends the router nowhere and fails the audit. Check the target exists before outside that universe sends the router nowhere and fails the audit. Check the target exists before
writing it — do not invent a plausible sibling name. writing it — do not invent a plausible sibling name.
That universe is the apm marketplace and stops there. A **host built-in is not a routing target**:
`/compact`, `/clear` and `/init` are Claude Code slash commands with no counterpart in Copilot CLI
or Codex, and `.apm/` source compiles for all three, so routing to one is a portability defect. The
gate is right to fail it and there is no allowlist. If a built-in genuinely needs mentioning, write
it un-slashed — ``the `compact` built-in`` — which makes no routing claim and is not checked.
**Length.** 250 characters SUGGESTION, 400 characters FAIL, counting the frontmatter value only **Length.** 250 characters SUGGESTION, 400 characters FAIL, counting the frontmatter value only
with YAML folding resolved. The agentskills.io 1,024-character spec limit is unchanged and sits with YAML folding resolved. The agentskills.io 1,024-character spec limit is unchanged and sits
above both. The SUGGESTION tier is the one that moves the average; treat 250 as the target and 400 above both. The SUGGESTION tier is the one that moves the average; treat 250 as the target and 400

View File

@@ -554,7 +554,28 @@ BACKTICK = re.compile(r"`(%s)`" % NAME_HYPH, re.I)
# ADR-0020's compressed arrow form `Not <thing> -> <name>`. # 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_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) 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 # 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 # modifier: punctuation, end of sentence, a conjunction, a boundary word, or a

View File

@@ -70,6 +70,12 @@ that package declares in `apm.yml` under `dependencies.apm`. A sibling plugin in
therefore resolves; a skill in an unrelated repo does not. A target outside that universe sends the therefore resolves; a skill in an unrelated repo does not. A target outside that universe sends the
router nowhere. Verify it before writing it — do not invent a plausible sibling. router nowhere. Verify it before writing it — do not invent a plausible sibling.
That universe is the apm marketplace and stops there. A **host built-in is not a routing target**:
`/compact`, `/clear` and `/init` are Claude Code slash commands with no counterpart in Copilot CLI
or Codex, and `.apm/` source compiles for all three, so routing to one is a portability defect. The
gate is right to fail it and there is no allowlist. If a built-in genuinely needs mentioning, write
it un-slashed — ``the `compact` built-in`` — which makes no routing claim and is not checked.
**Length.** 250 characters SUGGESTION, 400 characters FAIL, counting the frontmatter value only **Length.** 250 characters SUGGESTION, 400 characters FAIL, counting the frontmatter value only
with YAML folding resolved. Treat 250 as the target: the SUGGESTION tier is what moves the corpus with YAML folding resolved. Treat 250 as the target: the SUGGESTION tier is what moves the corpus
average, the FAIL tier only stops outliers. average, the FAIL tier only stops outliers.

View File

@@ -480,7 +480,28 @@ BACKTICK = re.compile(r"`(%s)`" % NAME_HYPH, re.I)
# ADR-0020's compressed arrow form `Not <thing> -> <name>`. # 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_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) 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 # 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 # modifier: punctuation, end of sentence, a conjunction, a boundary word, or a

View File

@@ -57,6 +57,12 @@ therefore resolves; a skill in an unrelated repo does not. A boundary clause nam
outside that universe sends the router nowhere and fails the audit. Check the target exists before outside that universe sends the router nowhere and fails the audit. Check the target exists before
writing it — do not invent a plausible sibling name. writing it — do not invent a plausible sibling name.
That universe is the apm marketplace and stops there. A **host built-in is not a routing target**:
`/compact`, `/clear` and `/init` are Claude Code slash commands with no counterpart in Copilot CLI
or Codex, and `.apm/` source compiles for all three, so routing to one is a portability defect. The
gate is right to fail it and there is no allowlist. If a built-in genuinely needs mentioning, write
it un-slashed — ``the `compact` built-in`` — which makes no routing claim and is not checked.
**Length.** 250 characters SUGGESTION, 400 characters FAIL, counting the frontmatter value only **Length.** 250 characters SUGGESTION, 400 characters FAIL, counting the frontmatter value only
with YAML folding resolved. The agentskills.io 1,024-character spec limit is unchanged and sits with YAML folding resolved. The agentskills.io 1,024-character spec limit is unchanged and sits
above both. The SUGGESTION tier is the one that moves the average; treat 250 as the target and 400 above both. The SUGGESTION tier is the one that moves the average; treat 250 as the target and 400

View File

@@ -564,7 +564,28 @@ BACKTICK = re.compile(r"`(%s)`" % NAME_HYPH, re.I)
# ADR-0020's compressed arrow form `Not <thing> -> <name>`. # 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_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) 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 # 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 # modifier: punctuation, end of sentence, a conjunction, a boundary word, or a

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'" \ 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." "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 # 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 # 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. # — the rule suppresses the ERROR, it does not delete the target.