fix(gates): boundary-target resolver checks only the first target per arrow clause #107

Closed
opened 2026-08-30 12:39:59 +00:00 by Claude · 2 comments
Collaborator

A boundary clause naming two or more targets in one arrow clause has only its first target resolved. The rest are neither resolved nor reported as dangling — they are invisible. validate.sh then prints 1 of 1 boundary target(s) resolve on a clause that names two, so the gate reports full coverage while checking half.

A typo in a second target ships through a green gate.

Evidence

Confirmed four times independently during the #99 retrofit, across three different separators (/, ,, or). One agent extracted the resolver out of scripts/skill-size-check.sh and executed it directly against gitea-labels-milestones' folded description:

boundary_targets:      ['gitea-issues']
has_boundary_clause:   True
gitea-prs in universe: True     (it exists — it just is not looked for)
unresolved_targets:    ([], [])

Mechanism

Three interacting rules, none wrong on its own:

  1. BOUNDARY_MARKER matches only do not|instead|rather than|not for. ADR-0020's own compressed opener — Not <thing> -> <skill-name>. — matches none of them, so _extract_sentence sets boundary=False and the backtick sweep never runs.
  2. ARROW_MARKED matches the single arrow and stops.
  3. _scan's conjunction continuation (CONT_*) is wired to ROUTE_* only, never to arrows.

has_boundary_clause still returns True via BOUNDARY_ARROW, so there is no missing-clause SUGGESTION either. The failure is completely silent.

Why this matters more than it looks

ADR-0020 mandates the compressed form and its Enforcement table lists the resolvable-target check as a deterministic ERROR — one of the few rules the ADR trusts a script with. The check is real and does work; it just under-reports its own coverage, which is the one failure mode a gate must not have. ADR-0020 is explicit that a bad gate is worse than no gate "because it would be believed".

Current state of the corpus

Every gitea-* skill retrofitted under #99 was written one arrow per target as a workaround, and now reports honestly:

skill line
gitea-branches 3 of 3 boundary target(s) resolve: git-branches, git-history, gitea-prs
gitea-issues 2 of 2 boundary target(s) resolve: gitea-labels-milestones, gitea-prs
gitea-labels-milestones 2 of 2 boundary target(s) resolve: gitea-issues, gitea-prs
gitea-workflow 2 of 2 boundary target(s) resolve: gitea-issues, gitea-prs

That convention is not written down anywhere. Skills outside this wave may still carry multi-target arrow clauses that look checked and are not.

Fix options

  1. Extend the arrow scan to continuation targets after /, , and or within the same clause. Fixes existing prose; the risk is the usual false-positive exposure ADR-0020 tuned these regexes against.
  2. Reject multi-target arrow clauses outright with a SUGGESTION telling the author to split. Cheaper, keeps the resolver's conservative tuning, and makes the already-adopted convention explicit rather than folkloric.

Recommend 2, plus a line in skill-author/references/contract.md stating one arrow per target — the convention exists in practice and should not stay undocumented.

Files

  • scripts/skill-size-check.sh (the resolver; constants mirrored downstream)
  • plugins/kyberforge/.apm/skills/skill-audit/scripts/validate.sh
  • plugins/kyberforge/.apm/skills/agent-audit/scripts/validate.sh
  • tests/test-adr0020-targets.sh — needs a case pinning that a second target in one clause is seen

Found while retrofitting the gitea plugin for #99. Reference: docs/adr/0020-skill-description-and-body-context-contract.md.

Note for whoever picks this up: ADR-0020's Decision section cites gitea-workflow -> git-workflow by name as one of four cross-plugin pairs proving the walk-up resolver works. The #99 retrofit removed that route deliberately (it resolves in this monorepo but not in a gitea-only install). Three of the four cited pairs remain, so the ADR's argument stands, but it now cites one example a reader cannot verify in the tree — worth a one-line note in the ADR while touching this area.

A boundary clause naming two or more targets in **one** arrow clause has only its first target resolved. The rest are neither resolved nor reported as dangling — they are invisible. `validate.sh` then prints `1 of 1 boundary target(s) resolve` on a clause that names two, so the gate reports full coverage while checking half. A typo in a second target ships through a green gate. ## Evidence Confirmed four times independently during the #99 retrofit, across three different separators (`/`, `,`, `or`). One agent extracted the resolver out of `scripts/skill-size-check.sh` and executed it directly against `gitea-labels-milestones`' folded description: ``` boundary_targets: ['gitea-issues'] has_boundary_clause: True gitea-prs in universe: True (it exists — it just is not looked for) unresolved_targets: ([], []) ``` ## Mechanism Three interacting rules, none wrong on its own: 1. `BOUNDARY_MARKER` matches only `do not|instead|rather than|not for`. ADR-0020's own compressed opener — `Not <thing> -> <skill-name>.` — matches none of them, so `_extract_sentence` sets `boundary=False` and the backtick sweep never runs. 2. `ARROW_MARKED` matches the single arrow and stops. 3. `_scan`'s conjunction continuation (`CONT_*`) is wired to `ROUTE_*` only, never to arrows. `has_boundary_clause` still returns True via `BOUNDARY_ARROW`, so there is no missing-clause SUGGESTION either. The failure is completely silent. ## Why this matters more than it looks ADR-0020 mandates the compressed form and its Enforcement table lists the resolvable-target check as a **deterministic ERROR** — one of the few rules the ADR trusts a script with. The check is real and does work; it just under-reports its own coverage, which is the one failure mode a gate must not have. ADR-0020 is explicit that a bad gate is worse than no gate "because it would be believed". ## Current state of the corpus Every `gitea-*` skill retrofitted under #99 was written **one arrow per target** as a workaround, and now reports honestly: | skill | line | |---|---| | `gitea-branches` | `3 of 3 boundary target(s) resolve: git-branches, git-history, gitea-prs` | | `gitea-issues` | `2 of 2 boundary target(s) resolve: gitea-labels-milestones, gitea-prs` | | `gitea-labels-milestones` | `2 of 2 boundary target(s) resolve: gitea-issues, gitea-prs` | | `gitea-workflow` | `2 of 2 boundary target(s) resolve: gitea-issues, gitea-prs` | That convention is not written down anywhere. Skills outside this wave may still carry multi-target arrow clauses that look checked and are not. ## Fix options 1. **Extend the arrow scan to continuation targets** after `/`, `,` and `or` within the same clause. Fixes existing prose; the risk is the usual false-positive exposure ADR-0020 tuned these regexes against. 2. **Reject multi-target arrow clauses outright** with a SUGGESTION telling the author to split. Cheaper, keeps the resolver's conservative tuning, and makes the already-adopted convention explicit rather than folkloric. Recommend 2, plus a line in `skill-author/references/contract.md` stating one arrow per target — the convention exists in practice and should not stay undocumented. ## Files - `scripts/skill-size-check.sh` (the resolver; constants mirrored downstream) - `plugins/kyberforge/.apm/skills/skill-audit/scripts/validate.sh` - `plugins/kyberforge/.apm/skills/agent-audit/scripts/validate.sh` - `tests/test-adr0020-targets.sh` — needs a case pinning that a second target in one clause is seen ## Related Found while retrofitting the `gitea` plugin for #99. Reference: `docs/adr/0020-skill-description-and-body-context-contract.md`. Note for whoever picks this up: ADR-0020's Decision section cites `gitea-workflow` -> `git-workflow` by name as one of four cross-plugin pairs proving the walk-up resolver works. The #99 retrofit removed that route deliberately (it resolves in this monorepo but not in a gitea-only install). Three of the four cited pairs remain, so the ADR's argument stands, but it now cites one example a reader cannot verify in the tree — worth a one-line note in the ADR while touching this area.
Claude added this to the Skills & Agents milestone 2026-08-30 12:39:59 +00:00
Claude added the Kind/Bug
Reviewed
Confirmed
1
Priority
High
2
labels 2026-08-30 12:39:59 +00:00
Author
Collaborator

Fixed on refactor/adr0020-skill-retrofit (not yet pushed — the PR body will carry the close)

Implemented via this issue's recommended option 2: reject the shape rather than widen the arrow scan.

multi_target_arrow_clauses() walks each ARROW_MARKED / ARROW_BOUNDARY hit, applies CONT_ANY at the match end, and returns the (first, second) pair. The finding names both targets and tells the author to split:

an arrow boundary clause names more than one target ('X', then 'Y'), and only the first is resolved — the second is checked by nothing (ADR-0020). Split it into one arrow per target: "Not X -> X. Not Y -> Y."

SUGGESTION tier, as recommended — the resolver's conservative tuning is untouched, so the ~30 arrow clauses standardised by #99 carry no new false-positive exposure.

The convention is now written down. skill-author/references/contract.md:66 — "One arrow, one target. The resolver reads only the first name after an arrow, so a second is [not checked]". That closes the "not written down anywhere" half of this report.

All three hosts. scripts/skill-size-check.sh, skill-audit/scripts/validate.sh, agent-audit/scripts/validate.sh — verified the function is present in each.

Tests. tests/test-adr0020-targets.sh section 2e, three cases: /-style or continuation, comma continuation, and a one-arrow-one-target control asserting silence — without the control the SUGGESTION could be satisfied by a check that fires on every arrow clause in the corpus. The section header records this issue's exact symptom (1 of 1 boundary target(s) resolve on a clause naming two) as the failure being pinned.

Not addressed here: the ADR's gitea-workflow -> git-workflow citation noted at the end of this issue. Still an unverifiable example in the tree.

## Fixed on `refactor/adr0020-skill-retrofit` (not yet pushed — the PR body will carry the close) Implemented via this issue's recommended **option 2**: reject the shape rather than widen the arrow scan. `multi_target_arrow_clauses()` walks each `ARROW_MARKED` / `ARROW_BOUNDARY` hit, applies `CONT_ANY` at the match end, and returns the `(first, second)` pair. The finding names both targets and tells the author to split: > an arrow boundary clause names more than one target ('X', then 'Y'), and only the first is resolved — the second is checked by nothing (ADR-0020). Split it into one arrow per target: "Not X -> X. Not Y -> Y." SUGGESTION tier, as recommended — the resolver's conservative tuning is untouched, so the ~30 arrow clauses standardised by #99 carry no new false-positive exposure. **The convention is now written down.** `skill-author/references/contract.md:66` — "**One arrow, one target.** The resolver reads only the first name after an arrow, so a second is [not checked]". That closes the "not written down anywhere" half of this report. **All three hosts.** `scripts/skill-size-check.sh`, `skill-audit/scripts/validate.sh`, `agent-audit/scripts/validate.sh` — verified the function is present in each. **Tests.** `tests/test-adr0020-targets.sh` section 2e, three cases: `/`-style `or` continuation, comma continuation, and a one-arrow-one-target control asserting silence — without the control the SUGGESTION could be satisfied by a check that fires on every arrow clause in the corpus. The section header records this issue's exact symptom (`1 of 1 boundary target(s) resolve` on a clause naming two) as the failure being pinned. Not addressed here: the ADR's `gitea-workflow -> git-workflow` citation noted at the end of this issue. Still an unverifiable example in the tree.
Author
Collaborator

Fixed by #129, squash-merged to main as 598a7c3.

Resolved via fix option 2 — a multi-target arrow clause is rejected with a SUGGESTION telling the author to split, rather than extending the arrow scan to continuation targets. That keeps the resolver's conservative tuning and makes the already-folkloric one-arrow-per-target convention explicit.

Verified against the merged tree on a fixture carrying Not X -> \git-commits` or `no-such-skill-xyz``:

SUGGESTION: an arrow boundary clause names more than one target ('git-commits', then
'no-such-skill-xyz'), and only the first is resolved — the second is checked by nothing
(ADR-0020). Split it into one arrow per target: "Not X -> git-commits. Not Y -> no-such-skill-xyz."

Both targets are named in the finding, so the under-reporting this issue describes is gone.

Reopened automatically? No — this stayed open after the merge because Gitea's closing-keyword parser consumed only the first reference in Closes #99, #107, …. Closing manually.

Fixed by #129, squash-merged to `main` as `598a7c3`. Resolved via **fix option 2** — a multi-target arrow clause is rejected with a SUGGESTION telling the author to split, rather than extending the arrow scan to continuation targets. That keeps the resolver's conservative tuning and makes the already-folkloric one-arrow-per-target convention explicit. Verified against the merged tree on a fixture carrying `Not X -> \`git-commits\` or \`no-such-skill-xyz\``: ``` SUGGESTION: an arrow boundary clause names more than one target ('git-commits', then 'no-such-skill-xyz'), and only the first is resolved — the second is checked by nothing (ADR-0020). Split it into one arrow per target: "Not X -> git-commits. Not Y -> no-such-skill-xyz." ``` Both targets are named in the finding, so the under-reporting this issue describes is gone. Reopened automatically? No — this stayed open after the merge because Gitea's closing-keyword parser consumed only the first reference in `Closes #99, #107, …`. Closing manually.
Sign in to join this conversation.