Skip to content

Commit 4483eee

Browse files
baonguyenNavaclaude
andcommitted
Record findings for the skill-gate spike
The port is faithful. Twenty thousand generated cases across four seeds produced zero disagreements with the original, on a harness whose detection power was demonstrated rather than assumed. Corpus scoping comes out stronger than the Lambda's, and the deployment surface, the Cursor dependency, and corpus staleness all disappear with it. What does not survive is the guarantee that the check runs at all. In the Lambda, run.ts calls ground() unconditionally, so trusted code wraps the untrusted model. In a skill the agent decides. That inversion is not closable by code, only observable, which turns the remaining question into a product judgment rather than an engineering one. Amends the 2026-07-27 design spec status so an approved spec is not silently orphaned while that judgment is pending. Relates to #42 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C81WXLzPWLPryrEPsjZ6BC
1 parent d0cc9ce commit 4483eee

2 files changed

Lines changed: 190 additions & 1 deletion

File tree

docs/superpowers/specs/2026-07-27-strata-qa-slack-bot-design.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
# Design: reach strata-qa from Slack
22

33
Date: 2026-07-27
4-
Status: approved, not yet implemented
4+
Status: approved, not yet implemented. On hold pending #42, which tests whether a Claude Tag skill
5+
carrying deterministic gates can serve the same audience without the gateway designed here. See
6+
`2026-07-28-claude-tag-gate-spike-findings.md`; if that direction is adopted, this spec is superseded
7+
rather than merely unbuilt.
58

69
## Problem
710

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
# Findings: can a skill-shipped gate replace the strata-qa Lambda?
2+
3+
Date: 2026-07-28
4+
Status: findings complete, decision open
5+
Issue: #42
6+
7+
## Question
8+
9+
Can a Claude Tag skill that ships deterministic JavaScript gates preserve the grounding guarantee
10+
`strata-qa` enforces today, without deploying the Slack gateway #40 specifies?
11+
12+
The guarantee at stake: `ground()` mechanically checks that every quote appears verbatim in the file
13+
it cites and demotes the model's own verdict when it does not, and `run.ts` calls it unconditionally.
14+
A model cannot claim `answered`; it can only propose it. A skill inverts that control, because the
15+
agent decides whether to invoke the gate.
16+
17+
## Answer
18+
19+
**The port is faithful. The invocation guarantee is not recoverable, only observable.** Everything
20+
that can be moved into code was moved into code and behaves identically to the original. What cannot
21+
be moved is the certainty that the code runs.
22+
23+
## What was built
24+
25+
Under `skills/answer-strata-question/`:
26+
27+
| File | Lines | What it is |
28+
|---|---|---|
29+
| `SKILL.md` | 105 | The three-stage procedure |
30+
| `bin/verify-answer.mjs` | 322 | The gate: `ground()` plus the CLI the Lambda gets from `run.ts` and `cli.ts` |
31+
| `bin/verify-answer.test.mjs` | 404 | 17 cases ported from `grounding.test.ts`, 15 new for the CLI surface |
32+
| `bin/select-candidates.mjs` | 159 | Deterministic candidate selection over `docs/graph.json` |
33+
| `bin/differential.test.mjs` | 182 | Generated differential sweep against the original |
34+
| `bin/reference-bridge.mts` | 32 | Spike scaffolding: runs the original TypeScript `ground()` |
35+
36+
Zero dependencies. No `package.json` under `skills/`, no `node_modules`, nothing added to the
37+
repository's dependency surface. The runner is Node 22's built-in `node --test`.
38+
39+
### Finding 1: the port estimate was wrong by roughly a factor of two
40+
41+
The issue predicted "roughly 155 lines" and flagged that estimate as something the spike would check.
42+
The gate came in at **322**.
43+
44+
The estimate was not wrong about `grounding.ts`, which ported almost verbatim. It was wrong because
45+
it counted only the library. In the Lambda, `run.ts` and `cli.ts` supply answer extraction, the
46+
refusal wording, exit codes, and the corpus plumbing; a standalone script has to carry those itself.
47+
The lesson generalizes: when estimating a port out of a service, count the entry point, not the
48+
function.
49+
50+
## Finding 2: the port is behaviourally identical (the headline)
51+
52+
`golden.json` could not be used here. It holds nine `{question, expect}` fixtures and records no
53+
model answers, so there is nothing to replay through a gate. The harness generates cases from the
54+
real corpus instead, with a fixed seed, and runs each through both implementations, comparing the
55+
entire `GroundingResult`: status, sources, all five counts, and every per-citation flag.
56+
57+
| Seed | Cases | `answered` | `no_match` | `low_confidence` | Disagreements |
58+
|---|---|---|---|---|---|
59+
| 20260728 | 5000 | 2921 | 995 | 1084 | **0** |
60+
| 1 | 5000 | 2896 | 1015 | 1089 | **0** |
61+
| 99991 | 5000 | 2872 | 1020 | 1108 | **0** |
62+
| 424242 | 5000 | 2924 | 1004 | 1072 | **0** |
63+
64+
20,000 generated cases, zero disagreements. The generator mutates both quotes (verbatim, rewrapped,
65+
unicode-swapped, markdown-stripped, typo'd, truncated, blank, fabricated) and paths (`docs/` prefix,
66+
`./` prefix, `#anchor`, `:12` and `:12:40` suffixes, padding, fabricated), and deliberately cites the
67+
same document twice so the per-document gate is exercised rather than assumed.
68+
69+
**The harness was verified to fail.** A passing differential test proves nothing unless it can
70+
detect a wrong port, so the exact semantic error the original's comments warn about was injected:
71+
per-quote gating (`quotesVerified < citationsTotal`) in place of per-document gating
72+
(`distinctDocs < docsCited`). The sweep caught **53 of 500** cases and the unit suite caught one.
73+
Both were restored to green afterward.
74+
75+
Reproduce:
76+
77+
```bash
78+
node --test skills/answer-strata-question/bin/differential.test.mjs
79+
CASES=5000 SEED=1 node --test skills/answer-strata-question/bin/differential.test.mjs
80+
```
81+
82+
## Finding 3: candidate selection narrows the corpus, and refuses some questions for free
83+
84+
`graph.json` indexes 56 documents, against 135 markdown files under `docs/`. Selecting from the graph
85+
therefore excludes `.verification`, `.curation`, and `superpowers` **by data structure**, which is the
86+
same exclusion `.dockerignore` performs for the image and for the same stated reason: extra search
87+
space is the main cost driver, and a design spec must never be cited as a source.
88+
89+
Measured across the nine golden questions:
90+
91+
| | Lexical matches of 56 | At `--limit 8` |
92+
|---|---|---|
93+
| Range | 0 to 28 | 0 to 8 |
94+
| Mean | 17.0 | 7.1 |
95+
| Corpus excluded | 70 percent | 87 percent |
96+
97+
One result is worth calling out. "What is the best pizza topping?" scores **zero** candidates, so the
98+
skill refuses it before reading a document or composing an answer. That refusal costs nothing. The
99+
other three refusal fixtures are topically adjacent to real documents ("the production database
100+
password for OSCER" matches the OSCER docs on every word that matters) and still score 8 candidates,
101+
so they must go to the model and be caught by the gate. Cheap refusals are available only for
102+
questions that are off-corpus, not for questions that are merely unanswerable.
103+
104+
**Selection cannot cause a wrong answer.** It decides only what gets read; `verify-answer.mjs`
105+
independently checks every quote against the same graph. A selector that misses the right document
106+
costs recall, never soundness. That asymmetry is what makes a plain lexical scorer acceptable.
107+
108+
**Recall is unmeasured.** The top-ranked candidate is topically correct for all five answerable
109+
fixtures, and the fixture about the CLI wrapping Copier ranks "How install and update work (Copier
110+
wrapper)" fourth, which is encouraging. But whether the model can actually answer from the top eight
111+
needs a live run, and this spike could not do one.
112+
113+
## Finding 4: control inversion, and what it actually costs
114+
115+
In the Lambda, trusted code wraps the untrusted model. In a skill, the agent calls the trusted code.
116+
That inversion cannot be closed by better JavaScript. It is not closable by the available levers
117+
either: auto mode allow rules permit actions rather than compelling them, and a repository hook
118+
cannot gate the Slack post, because posting is the harness's own output channel and not a tool call
119+
the repository intercepts.
120+
121+
The mitigation built here has two halves:
122+
123+
1. **The gate is the sole producer of the final message.** `verify-answer.mjs` prints the Slack text
124+
itself. An agent that skips it has no formatted output to post, so skipping is not a shortcut, it
125+
is a visible departure from the procedure.
126+
2. **Every message carries a verdict line** (`✅ 3/3 quotes verified · 2 docs`). A message without one
127+
is legibly off-script to any human reading the channel.
128+
129+
This converts prevention into detection. That is a genuine downgrade and should be named as one. It
130+
is also worth being precise about the blast radius: the answer text is released only on `answered`,
131+
exactly as `run.ts` does it, and a test asserts that `low_confidence` and `no_match` never leak the
132+
proposed answer. So the failure mode of a skipped gate is an unverified answer posted with no verdict
133+
line, not a silently corrupted one.
134+
135+
## Finding 5: a skill cannot ship an executable and its tests in one directory
136+
137+
`node --test <dir>` fails on `bin/`, because directory mode executes `verify-answer.mjs` itself; the
138+
CLI guard fires on empty stdin and exits 6. The glob form works:
139+
140+
```bash
141+
node --test "skills/answer-strata-question/bin/*.test.mjs"
142+
```
143+
144+
Minor, but it belongs in any CI wiring, and it is the kind of thing that silently disables a test
145+
suite if nobody notices the runner was passing a directory.
146+
147+
## Recommendation on the duplication
148+
149+
`ground()` now exists twice: `strata-qa/src/grounding.ts` and the ported copy. Resolving that was out
150+
of scope, and the differential harness makes the duplication safe for now by failing the moment the
151+
two diverge. If this direction is adopted, the follow-on story should make the `.mjs` the single
152+
source of truth and have the TypeScript import it, rather than the reverse. The port is dependency
153+
free and the TypeScript is not, so the dependency should point toward the simpler artifact.
154+
155+
If this direction is not adopted, delete `skills/answer-strata-question/` entirely rather than leaving
156+
it as a second implementation nobody runs.
157+
158+
## What this spike could not answer
159+
160+
Each of these needs a live Claude Tag workspace and a Team or Enterprise entitlement:
161+
162+
- **Whether Tag reliably invokes the gate.** The central risk, and the reason this was a spike.
163+
- **Whether the `Workflow` tool exists in Tag sandboxes.** `generate-strata-docs` commits to it with
164+
no fallback, so this matters beyond the present question.
165+
- **End to end answer quality** against the nine fixtures, and therefore whether the eight-candidate
166+
cap costs recall.
167+
- **Real per-question cost and latency.**
168+
- **Whether Agent Proxy can sign a request to an IAM-authed Lambda Function URL**, which is the gate
169+
on the competing option, not on this one.
170+
171+
Confirming the plan entitlement is the cheapest next step and blocks the other four.
172+
173+
## Where this leaves the decision
174+
175+
The evidence supports the narrow claim and not the broad one. Everything mechanical survives the move
176+
out of the Lambda intact, proven against 20,000 cases by a harness demonstrated to detect a wrong
177+
port. Corpus scoping comes out stronger than the Lambda's, because it derives from the graph rather
178+
than from a build-time exclusion list. Deployment surface, Cursor dependency, and corpus staleness all
179+
disappear.
180+
181+
What does not survive is the certainty that the check runs at all, and no amount of further offline
182+
work will recover it. That is now a product judgment rather than an engineering one: whether a
183+
verifier that runs almost always, and whose absence is visible, is good enough for answers this
184+
audience will act on. If it is, this direction is ready for a piloted story and #40's transport should
185+
be replaced. If it is not, the Lambda keeps the gate in its control flow and the SigV4 connection
186+
question becomes the one worth spiking next.

0 commit comments

Comments
 (0)