fix(specverb-resolution): exact operationId word-set match should beat a superset (kills needless op pins) #195

Closed
opened 2026-07-07 07:30:39 +00:00 by coilysiren · 2 comments
Owner

Bug

specverb.resolveOp's operationId fallback (specverb-resolution.md) matches a grant's verb+resource against the words of each operationId (camelCase / kebab / snake split), then applies the plural / least-nested disambiguation and fails closed on a tie. But it treats an exact word-set match and a strict superset match as equal candidates. So a grant that exactly names one operationId still fails closed when another operationId is a superset of its words, forcing a needless op pin.

Repro (skillsmp)

Two ops: searchSkills (words [search, skills]) and aiSearchSkills (words [ai, search, skills]). Grant can search skills (words [search, skills]):

  • searchSkills - exact word-set match.
  • aiSearchSkills - superset (contains [search, skills] plus ai).

Both are kept as candidates → ambiguous → fail-closed → the author is forced to pin op searchSkills. That is exactly why the skillsmp guardfile (ward .ward/ward-kdl/ward-kdl.skillsmp.guardfile.kdl) carries two op pins the docs otherwise call "rarely needed."

Fix

When the grant's word-set matches one operationId exactly (set-equal) and the other candidates match only as supersets, the exact match wins - the same spirit as the existing "prefer plural collection / least-nested path" disambiguation, extended to the operationId fallback. A genuine tie (two exact matches, or two equal-distance supersets) still fails closed and still names the candidates.

Why it matters

op is meant to be a rare override for genuinely ambiguous or machine-garbage operationIds (glitchtip's apps_projects_api_create_project). An exact match losing to a superset is not that. Tightening this removes a whole class of needless pins and makes verb+resource auto-resolution live up to the "op is rarely needed" promise.

Scope

specverb.resolveOp / the operationId fallback, plus a resolution test with the search / ai-search shape. No grammar change. Affects every spec-backed consumer (the ward-kdl CLI guardfiles today).

Related: cli-guard#147 (the convention + op-override seam). Note: ward-mcp is moving to inline operation authoring (no operationIds at all), so this fix is for the spec-backed CLI path, not ward-mcp.

## Bug `specverb.resolveOp`'s operationId fallback ([specverb-resolution.md](docs/specverb-resolution.md)) matches a grant's verb+resource against the words of each operationId (camelCase / kebab / snake split), then applies the plural / least-nested disambiguation and fails closed on a tie. But it treats an **exact word-set match** and a **strict superset match** as equal candidates. So a grant that *exactly* names one operationId still fails closed when another operationId is a superset of its words, forcing a needless `op` pin. ## Repro (skillsmp) Two ops: `searchSkills` (words `[search, skills]`) and `aiSearchSkills` (words `[ai, search, skills]`). Grant `can search skills` (words `[search, skills]`): - `searchSkills` - **exact** word-set match. - `aiSearchSkills` - **superset** (contains `[search, skills]` plus `ai`). Both are kept as candidates → ambiguous → fail-closed → the author is forced to pin `op searchSkills`. That is exactly why the skillsmp guardfile (ward `.ward/ward-kdl/ward-kdl.skillsmp.guardfile.kdl`) carries two `op` pins the docs otherwise call "rarely needed." ## Fix When the grant's word-set matches one operationId **exactly** (set-equal) and the other candidates match only as **supersets**, the exact match wins - the same spirit as the existing "prefer plural collection / least-nested path" disambiguation, extended to the operationId fallback. A genuine tie (two exact matches, or two equal-distance supersets) still fails closed and still names the candidates. ## Why it matters `op` is meant to be a rare override for genuinely ambiguous or machine-garbage operationIds (glitchtip's `apps_projects_api_create_project`). An exact match losing to a superset is not that. Tightening this removes a whole class of needless pins and makes verb+resource auto-resolution live up to the "op is rarely needed" promise. ## Scope `specverb.resolveOp` / the operationId fallback, plus a resolution test with the search / ai-search shape. No grammar change. Affects every spec-backed consumer (the ward-kdl CLI guardfiles today). Related: cli-guard#147 (the convention + `op`-override seam). Note: ward-mcp is moving to inline operation authoring (no operationIds at all), so this fix is for the **spec-backed CLI path**, not ward-mcp.
Member

🔒 Reserved by ward agent --driver claude — container engineer-claude-cli-guard-195 on host KAI-DESKTOP-TOWER is carrying this issue (reserved 2026-07-07T07:38:09Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

Do not comment on or edit this issue to steer the run while it is reserved. The engineer seeded the body once at launch and never re-reads it, so a comment or edit reaches only human readers, never the running engineer. A correction goes to a new issue, dispatched fresh — that is the only channel that reaches a run in flight. Where the forge supports it, ward locks this conversation to make that a road-block rather than a convention (ward#494).

run seed context — what this run is carrying (ward#609)
  • Resolved: coilyco-flight-deck/cli-guard#195 · branch issue-195 · driver claude · workflow direct-main
  • Run: engineer-claude-cli-guard-195 · ward v0.419.0 · dispatched 2026-07-07T07:38:09Z
  • Comment thread: 0 included in the pre-flight read, 0 stripped (ward's own automated comments).

Issue body as seeded:

## Bug

`specverb.resolveOp`'s operationId fallback ([specverb-resolution.md](docs/specverb-resolution.md)) matches a grant's verb+resource against the words of each operationId (camelCase / kebab / snake split), then applies the plural / least-nested disambiguation and fails closed on a tie. But it treats an **exact word-set match** and a **strict superset match** as equal candidates. So a grant that *exactly* names one operationId still fails closed when another operationId is a superset of its words, forcing a needless `op` pin.

## Repro (skillsmp)

Two ops: `searchSkills` (words `[search, skills]`) and `aiSearchSkills` (words `[ai, search, skills]`). Grant `can search skills` (words `[search, skills]`):

- `searchSkills` - **exact** word-set match.
- `aiSearchSkills` - **superset** (contains `[search, skills]` plus `ai`).

Both are kept as candidates → ambiguous → fail-closed → the author is forced to pin `op searchSkills`. That is exactly why the skillsmp guardfile (ward `.ward/ward-kdl/ward-kdl.skillsmp.guardfile.kdl`) carries two `op` pins the docs otherwise call "rarely needed."

## Fix

When the grant's word-set matches one operationId **exactly** (set-equal) and the other candidates match only as **supersets**, the exact match wins - the same spirit as the existing "prefer plural collection / least-nested path" disambiguation, extended to the operationId fallback. A genuine tie (two exact matches, or two equal-distance supersets) still fails closed and still names the candidates.

## Why it matters

`op` is meant to be a rare override for genuinely ambiguous or machine-garbage operationIds (glitchtip's `apps_projects_api_create_project`). An exact match losing to a superset is not that. Tightening this removes a whole class of needless pins and makes verb+resource auto-resolution live up to the "op is rarely needed" promise.

## Scope

`specverb.resolveOp` / the operationId fallback, plus a resolution test with the search / ai-search shape. No gramm

… (truncated to 2000 chars; full body is on this issue)

Static container doctrine and seed boilerplate are identical every run and omitted here (they ride ward v0.419.0).

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent --driver claude` — container `engineer-claude-cli-guard-195` on host `KAI-DESKTOP-TOWER` is carrying this issue (reserved 2026-07-07T07:38:09Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. **Do not comment on or edit this issue to steer the run while it is reserved.** The engineer seeded the body once at launch and never re-reads it, so a comment or edit reaches only human readers, never the running engineer. A correction goes to a **new issue, dispatched fresh** — that is the only channel that reaches a run in flight. Where the forge supports it, ward locks this conversation to make that a road-block rather than a convention (ward#494). <details><summary>run seed context — what this run is carrying (ward#609)</summary> - **Resolved:** `coilyco-flight-deck/cli-guard#195` · branch `issue-195` · driver `claude` · workflow `direct-main` - **Run:** `engineer-claude-cli-guard-195` · ward `v0.419.0` · dispatched `2026-07-07T07:38:09Z` - **Comment thread:** 0 included in the pre-flight read, 0 stripped (ward's own automated comments). **Issue body as seeded:** ``` ## Bug `specverb.resolveOp`'s operationId fallback ([specverb-resolution.md](docs/specverb-resolution.md)) matches a grant's verb+resource against the words of each operationId (camelCase / kebab / snake split), then applies the plural / least-nested disambiguation and fails closed on a tie. But it treats an **exact word-set match** and a **strict superset match** as equal candidates. So a grant that *exactly* names one operationId still fails closed when another operationId is a superset of its words, forcing a needless `op` pin. ## Repro (skillsmp) Two ops: `searchSkills` (words `[search, skills]`) and `aiSearchSkills` (words `[ai, search, skills]`). Grant `can search skills` (words `[search, skills]`): - `searchSkills` - **exact** word-set match. - `aiSearchSkills` - **superset** (contains `[search, skills]` plus `ai`). Both are kept as candidates → ambiguous → fail-closed → the author is forced to pin `op searchSkills`. That is exactly why the skillsmp guardfile (ward `.ward/ward-kdl/ward-kdl.skillsmp.guardfile.kdl`) carries two `op` pins the docs otherwise call "rarely needed." ## Fix When the grant's word-set matches one operationId **exactly** (set-equal) and the other candidates match only as **supersets**, the exact match wins - the same spirit as the existing "prefer plural collection / least-nested path" disambiguation, extended to the operationId fallback. A genuine tie (two exact matches, or two equal-distance supersets) still fails closed and still names the candidates. ## Why it matters `op` is meant to be a rare override for genuinely ambiguous or machine-garbage operationIds (glitchtip's `apps_projects_api_create_project`). An exact match losing to a superset is not that. Tightening this removes a whole class of needless pins and makes verb+resource auto-resolution live up to the "op is rarely needed" promise. ## Scope `specverb.resolveOp` / the operationId fallback, plus a resolution test with the search / ai-search shape. No gramm ``` … (truncated to 2000 chars; full body is on this issue) Static container doctrine and seed boilerplate are identical every run and omitted here (they ride ward v0.419.0). </details> <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Author
Owner

WARD-OUTCOME: done - exact operationId word-set now beats a superset in the specverb fallback; skillsmp search/ai-search grants resolve pin-free.

The honest part of this one: the issue's repro was an idealized model, not the actual code path. It described search skills reaching the operationId fallback and losing a superset tie. But when I traced the real skillsmp shape (paths /search and /ai-search, no REST resource segment), the search verb early-returned from searchCandidates and never reached the fallback at all, and ai-search didn't tokenize-match because the predicate compared the verb as a single word. So the stated fix (exact-beats-superset) was necessary but, alone, completely inert for the motivating case.

I verified that with a throwaway test against the real spec before writing a line of the fix, which I'm glad I did. The real fix is three interlocking pieces: exact-beats-superset (keepExact), verb word-splitting so ai-search matches aiSearchSkills, and letting search fall through to the fallback on a structural miss. All three are needed for both pins to become removable downstream. Fairly confident: fail-closed semantics are preserved (genuine ties still error and name candidates), and single-word verbs are unchanged since the new path only diverges for multi-word verbs that previously always failed closed anyway.

Rough edges / follow-ups: the actual op pins live in the ward-kdl guardfile, not here, so removing them is a downstream job when ward bumps cli-guard off this tag. Worth a follow-up issue over there to drop the two skillsmp pins and confirm the lock re-resolves. Also spent more time than I'd like squeezing the doc under the 4000-char markdown cap, which nibbled at some unrelated prose.

WARD-OUTCOME: done - exact operationId word-set now beats a superset in the specverb fallback; skillsmp `search`/`ai-search` grants resolve pin-free. The honest part of this one: the issue's repro was an idealized model, not the actual code path. It described `search skills` reaching the operationId fallback and losing a superset tie. But when I traced the real skillsmp shape (paths `/search` and `/ai-search`, no REST resource segment), the `search` verb early-returned from `searchCandidates` and never reached the fallback at all, and `ai-search` didn't tokenize-match because the predicate compared the verb as a single word. So the stated fix (exact-beats-superset) was necessary but, alone, completely inert for the motivating case. I verified that with a throwaway test against the real spec before writing a line of the fix, which I'm glad I did. The real fix is three interlocking pieces: exact-beats-superset (`keepExact`), verb word-splitting so `ai-search` matches `aiSearchSkills`, and letting `search` fall through to the fallback on a structural miss. All three are needed for both pins to become removable downstream. Fairly confident: fail-closed semantics are preserved (genuine ties still error and name candidates), and single-word verbs are unchanged since the new path only diverges for multi-word verbs that previously always failed closed anyway. Rough edges / follow-ups: the actual `op` pins live in the ward-kdl guardfile, not here, so removing them is a downstream job when ward bumps cli-guard off this tag. Worth a follow-up issue over there to drop the two skillsmp pins and confirm the lock re-resolves. Also spent more time than I'd like squeezing the doc under the 4000-char markdown cap, which nibbled at some unrelated prose.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
coilyco-flight-deck/cli-guard#195
No description provided.