efaimo 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -1
- package/README.md +68 -6
- package/SKILL.md +14 -7
- package/dist/cli.js +162 -22
- package/dist/core/engine.d.ts +2 -1
- package/dist/core/engine.js +4 -0
- package/dist/core/types.d.ts +121 -0
- package/dist/find/bm25.d.ts +77 -0
- package/dist/find/bm25.js +107 -0
- package/dist/find/find.d.ts +120 -0
- package/dist/find/find.js +403 -0
- package/dist/find/tokenize.d.ts +64 -0
- package/dist/find/tokenize.js +116 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +13 -3
- package/dist/reporters/json.d.ts +19 -1
- package/dist/reporters/json.js +13 -2
- package/dist/reporters/markdown.d.ts +2 -1
- package/dist/reporters/markdown.js +109 -11
- package/dist/reporters/pretty.d.ts +12 -1
- package/dist/reporters/pretty.js +174 -9
- package/dist/rules/find/index.d.ts +4 -0
- package/dist/rules/find/index.js +262 -0
- package/dist/rules/version.d.ts +26 -0
- package/dist/rules/version.js +26 -0
- package/dist/testing/anthropicRunner.d.ts +30 -0
- package/dist/testing/anthropicRunner.js +43 -8
- package/dist/testing/harness.d.ts +32 -1
- package/dist/testing/harness.js +28 -3
- package/dist/testing/stats.d.ts +16 -0
- package/dist/testing/stats.js +26 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/weigh/anthropicExact.d.ts +12 -1
- package/dist/weigh/anthropicExact.js +9 -1
- package/dist/weigh/weigh.js +10 -3
- package/docs/INTEGRATIONS.md +15 -1
- package/docs/METHODOLOGY.md +110 -6
- package/docs/RULES.md +40 -3
- package/examples/scenario.crm.yaml +8 -0
- package/examples/scenario.example.yaml +8 -0
- package/package.json +1 -1
package/dist/weigh/weigh.js
CHANGED
|
@@ -33,13 +33,19 @@ export async function weighServer(intro, opts = {}) {
|
|
|
33
33
|
"token counts are estimates using the o200k_base tokenizer; hosts add fixed framing text on top (see docs/METHODOLOGY.md)",
|
|
34
34
|
];
|
|
35
35
|
let anthropicExactTotal;
|
|
36
|
+
let anthropicExactModel;
|
|
36
37
|
if (opts.anthropicApiKey && intro.tools.length > 0) {
|
|
37
|
-
|
|
38
|
+
const exact = await countClaudeToolTokens(intro.tools, {
|
|
38
39
|
apiKey: opts.anthropicApiKey,
|
|
39
40
|
model: opts.anthropicModel,
|
|
40
41
|
});
|
|
41
|
-
if (
|
|
42
|
-
|
|
42
|
+
if (exact !== undefined) {
|
|
43
|
+
anthropicExactTotal = exact.tokens;
|
|
44
|
+
anthropicExactModel = exact.model;
|
|
45
|
+
// Name the model. Claude model lines do not share a tokenizer, so this
|
|
46
|
+
// figure is exact for `exact.model` and an estimate for anything else,
|
|
47
|
+
// and a reader comparing two runs has to be able to see which is which.
|
|
48
|
+
notes.push(`anthropic-exact measured via /v1/messages/count_tokens (tools delta) against ${exact.model}`);
|
|
43
49
|
}
|
|
44
50
|
else {
|
|
45
51
|
notes.push("anthropic-exact measurement failed; showing o200k estimates only");
|
|
@@ -56,6 +62,7 @@ export async function weighServer(intro, opts = {}) {
|
|
|
56
62
|
framingTokens,
|
|
57
63
|
instructionsTokens: count(intro.instructions ?? ""),
|
|
58
64
|
anthropicExactTotal,
|
|
65
|
+
anthropicExactModel,
|
|
59
66
|
notes,
|
|
60
67
|
};
|
|
61
68
|
}
|
package/docs/INTEGRATIONS.md
CHANGED
|
@@ -36,6 +36,18 @@ Ratchet MCP tool-definition cost with the budget gate (commit `base.json` once w
|
|
|
36
36
|
- run: npx efaimo weigh "npx -y your-server" --diff base.json --allow-increase 10
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
Keep every tool findable once a host defers your catalog. `--min-distinct` fails the run
|
|
40
|
+
when a tool owns no word the other tools lack, which means no search can return
|
|
41
|
+
it without also returning a competitor:
|
|
42
|
+
|
|
43
|
+
```yaml
|
|
44
|
+
- run: npx efaimo find "npx -y your-server" --min-distinct 100
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Start it at the number you have today rather than at 100 and raise it as you
|
|
48
|
+
fix tools; the command prints the line to copy. It refuses to run on a one-tool
|
|
49
|
+
catalog, where the figure is 100% by construction, rather than passing.
|
|
50
|
+
|
|
39
51
|
Post the grade on the PR with `--md` (pipe into any comment action):
|
|
40
52
|
|
|
41
53
|
```yaml
|
|
@@ -108,7 +120,9 @@ the current SDK.
|
|
|
108
120
|
|
|
109
121
|
## Programmatic use
|
|
110
122
|
|
|
111
|
-
Every capability is also a typed function
|
|
123
|
+
Every capability is also a typed function. `analyzeFind`, `runFindRules` and the
|
|
124
|
+
two `find` renderers are exported alongside the rest; the rule
|
|
125
|
+
tables themselves stay private, like the other two families.
|
|
112
126
|
|
|
113
127
|
```ts
|
|
114
128
|
import { weighServer, introspectServer, checkSkillSet } from "efaimo";
|
package/docs/METHODOLOGY.md
CHANGED
|
@@ -131,19 +131,28 @@ trials. At that size 5/5 against 4/5 is +20 points and p = 1.0000.
|
|
|
131
131
|
|
|
132
132
|
Two things the judge does, which matter for reading a result:
|
|
133
133
|
|
|
134
|
-
- it
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
- it is asked to sample deterministically **where the model still accepts a
|
|
135
|
+
sampling parameter**. Claude removed `temperature` from the 4.7 line onward,
|
|
136
|
+
so on Sonnet 5, Opus 5 and their siblings the judge runs at the model's own
|
|
137
|
+
default and its variance is part of the measurement. The report says so on
|
|
138
|
+
every run that this applies to, rather than claiming a determinism it does
|
|
139
|
+
not have. The subject arm is unaffected: it
|
|
140
|
+
wanted temperature 1, which is the default anyway;
|
|
137
141
|
- a reply that is neither PASS nor FAIL is **excluded and counted separately**,
|
|
138
142
|
not scored as a failure. A refusal or an API error is not evidence about the
|
|
139
|
-
skill.
|
|
143
|
+
skill. Exclusion is only unbiased while it falls on both arms alike, so the
|
|
144
|
+
report also carries a **two-sided Fisher exact p on the unparseable counts**,
|
|
145
|
+
and a skew significant at the same 0.05 makes the verdict `inconclusive`:
|
|
146
|
+
two pass rates computed over different populations cannot be subtracted.
|
|
140
147
|
|
|
141
148
|
### What this design still cannot tell you
|
|
142
149
|
|
|
143
150
|
Named here rather than left for a reader to discover:
|
|
144
151
|
|
|
145
|
-
- **The judge
|
|
146
|
-
|
|
152
|
+
- **The judge defaults to the same model as the subject**, so part of any
|
|
153
|
+
measured effect is a model preferring its own output. `judge_model:` in the
|
|
154
|
+
scenario, or `--judge-model`, points it at another model, and the report
|
|
155
|
+
prints both. Subject and judge may be different providers.
|
|
147
156
|
- **The control arm receives no system prompt at all**, so "the skill's
|
|
148
157
|
content" is confounded with "having any system prompt". A length-matched
|
|
149
158
|
placebo is the fix and is not implemented.
|
|
@@ -151,3 +160,98 @@ Named here rather than left for a reader to discover:
|
|
|
151
160
|
show the method works, not that skills in general do or do not help.
|
|
152
161
|
|
|
153
162
|
Treat a single scenario as evidence about that scenario.
|
|
163
|
+
|
|
164
|
+
## `efaimo find`: findability
|
|
165
|
+
|
|
166
|
+
A host may mark tools `defer_loading: true`, which keeps their definitions out
|
|
167
|
+
of the system-prompt prefix until a search returns them. Anthropic lists five
|
|
168
|
+
conditions for turning that on, joined by "any of the following apply", and two
|
|
169
|
+
of them can be checked from a tool list: **ten or more tools**, or **more than
|
|
170
|
+
10k tokens of definitions**. `find` checks both and names the one that fired.
|
|
171
|
+
An earlier version implemented only the token clause, which told a reader that
|
|
172
|
+
a 24-tool, 3.5k-token catalog was probably loaded up front when Anthropic's
|
|
173
|
+
first condition already applied to it.
|
|
174
|
+
|
|
175
|
+
Under deferral, a tool nothing surfaces costs no context and provides no
|
|
176
|
+
capability, so "can this be found" becomes a separate question from "what does
|
|
177
|
+
this cost" and "how good is this tool".
|
|
178
|
+
|
|
179
|
+
`find` answers it with two numbers that are **not the same kind of claim**, and
|
|
180
|
+
prints both with their denominators. In JSON they are `data.distinct` and
|
|
181
|
+
`data.probe`; the terminal uses the same two words.
|
|
182
|
+
|
|
183
|
+
### `distinct`: a property of the catalog
|
|
184
|
+
|
|
185
|
+
For every tool, which of its terms does no other tool in the catalog have?
|
|
186
|
+
|
|
187
|
+
If the answer is none, then every word that tool contains appears somewhere
|
|
188
|
+
else, so **no query exists that matches it without also matching a
|
|
189
|
+
competitor**. That is not a prediction about a particular search. It follows
|
|
190
|
+
from the index, and the only assumption in it is the tokenizer.
|
|
191
|
+
|
|
192
|
+
The precise limit of the claim, stated because it is easy to overstate: a tool
|
|
193
|
+
with no exclusive vocabulary can still be ranked *first* for a shared query,
|
|
194
|
+
because BM25 also weighs term frequency and document length. What it can never
|
|
195
|
+
do is come back alone. Owning a word is a much stronger position than winning a
|
|
196
|
+
tie-break.
|
|
197
|
+
|
|
198
|
+
Terms come from the four fields the tool search documentation names as
|
|
199
|
+
searchable: **tool name, description, argument names, argument descriptions**.
|
|
200
|
+
Tokenization splits camelCase, `snake_case` and `kebab-case` onto the same
|
|
201
|
+
terms, lowercases, drops single characters, and does no stemming, so `page` and
|
|
202
|
+
`pages` are different terms (`src/find/tokenize.ts`, identified in JSON as
|
|
203
|
+
`data.method.tokenizer`, a string that changes when the splitting rules do).
|
|
204
|
+
English function words are excluded from ownership as well as from queries: a
|
|
205
|
+
tool can be the only one in a catalog that says "the", and counting that as
|
|
206
|
+
vocabulary it owns would be true and useless.
|
|
207
|
+
|
|
208
|
+
**`title` is deliberately not indexed**, though MCP tools may carry one.
|
|
209
|
+
Anthropic's list of searchable fields does not include it, and a `title` is
|
|
210
|
+
usually the name restated for humans, so reading it would put the tool's own
|
|
211
|
+
name back into its own probe. Indexing a field the real search cannot read also
|
|
212
|
+
makes the number optimistic: a tool would "own" a word no query can reach.
|
|
213
|
+
Measured cost of getting this wrong: Notion's server scored 17/24 with `title`
|
|
214
|
+
indexed and 16/24 without, and the 16 is the honest one.
|
|
215
|
+
|
|
216
|
+
**At one tool the number cannot be computed.** Every term is trivially
|
|
217
|
+
exclusive, so the figure is 100% for any tool whatsoever. The report says so and
|
|
218
|
+
`--min-distinct` refuses to be evaluated rather than reporting a pass.
|
|
219
|
+
|
|
220
|
+
### `probe`: a simulation, and a saturated one
|
|
221
|
+
|
|
222
|
+
BM25 (k1=1.2, b=0.75) over those same fields, with each tool queried using the
|
|
223
|
+
highest tf-idf terms **of its own description** and a result window of 5, which
|
|
224
|
+
is the documented default number of results a search returns.
|
|
225
|
+
|
|
226
|
+
Two honest limits:
|
|
227
|
+
|
|
228
|
+
- The real search runs server-side. Its analyzer and parameters are not
|
|
229
|
+
published, so this is a model of a documented mechanism, not a reproduction.
|
|
230
|
+
- **It saturates.** Measured on four public servers: 13/13, 24/24, 26/26 and
|
|
231
|
+
24/24, and every query variant tried (tf-idf, raw tf, leading terms) gave the
|
|
232
|
+
same 100%. The query comes from the tool's own words, which is friendlier
|
|
233
|
+
than anything a user would type. Treat it as a floor test that catches an
|
|
234
|
+
empty description or a literal duplicate, not as a ranking.
|
|
235
|
+
[The findability report](../research/findability/REPORT.md) has the numbers
|
|
236
|
+
and the commands that reproduce them.
|
|
237
|
+
|
|
238
|
+
The query deliberately excludes the tool's **name** and its **title**. An
|
|
239
|
+
earlier version included both and the metric could barely fail: a unique name
|
|
240
|
+
token has maximal idf, so it entered every query and pulled its own tool to the
|
|
241
|
+
top, and two tools with identical descriptions both scored a clean first place.
|
|
242
|
+
The name stays in the index, so a name whose words echo the description still
|
|
243
|
+
helps it rank.
|
|
244
|
+
|
|
245
|
+
### Why there is no findability grade
|
|
246
|
+
|
|
247
|
+
Because the two numbers answer different questions and only one of them is a
|
|
248
|
+
measurement rather than a model, and because a letter would have inherited the
|
|
249
|
+
defect the quality grade already has: it cannot see size. See ADR-030. The gate
|
|
250
|
+
is `--min-distinct`, which the operator sets, on the number that can actually
|
|
251
|
+
fail at two tools and up.
|
|
252
|
+
|
|
253
|
+
The colour the terminal puts on `distinct` is not a grade either: red means the
|
|
254
|
+
E141 condition holds (some tool owns nothing), green means it does not. An
|
|
255
|
+
earlier version banded it at 95 and 80 percent, two cutoffs that appeared in no
|
|
256
|
+
rule and no document, inside the one command whose design note says it does not
|
|
257
|
+
grade.
|
package/docs/RULES.md
CHANGED
|
@@ -6,9 +6,14 @@ Severities: **error** (will bite you), **warn** (should fix), **info** (worth kn
|
|
|
6
6
|
Grade starts at 100; each error costs 15, each warning 5, each info 1.
|
|
7
7
|
`A >= 90, B >= 80, C >= 70, D >= 60, else F`.
|
|
8
8
|
|
|
9
|
+
Every JSON report carries a `rulesVersion`. The tool version does not identify a ruleset on its own: a patch
|
|
10
|
+
release can change what a rule fires on, so a published grade is only
|
|
11
|
+
reproducible next to the ruleset that produced it.
|
|
12
|
+
|
|
9
13
|
MCP checks split into **2026-07-28 readiness** (E101-E118) and **quality**
|
|
10
|
-
(E121-E128 and E130, with no E129); skill checks are S1xx
|
|
11
|
-
|
|
14
|
+
(E121-E128 and E130, with no E129); skill checks are S1xx; **findability**
|
|
15
|
+
(E141-E145) is a third family reported by `efaimo find`.
|
|
16
|
+
**Only quality and skill findings are graded.** Readiness findings are reported as a separate, ungraded **migration
|
|
12
17
|
diff**: the two answer different questions. The grade is what a model
|
|
13
18
|
experiences from this tool surface; readiness is whether a 2026-07-28 client can
|
|
14
19
|
talk to this server. Combining them scored the official reference server a C(79)
|
|
@@ -54,7 +59,7 @@ never reused, so a gap is permanent and is better stated than smoothed over.
|
|
|
54
59
|
| E122 | warn | description misses 3+ of 4 quality axes (length, when-to-use, params documented, mentions result), mirrors Glama's tool-definition-quality dimensions, computed locally |
|
|
55
60
|
| E123 | warn/info | no tools declare annotations; a destructive-looking tool lacks `destructiveHint` |
|
|
56
61
|
| E124 | warn | many undocumented parameters, or an oversized `enum` inflating every prompt |
|
|
57
|
-
| E125 | warn | tool count high enough (30+, 60+) to degrade routing and inflate context |
|
|
62
|
+
| E125 | warn | tool count high enough (30+, 60+) to degrade routing and inflate context. The lower boundary is not ours: Anthropic's tool search documentation states that "Claude's ability to pick the right tool degrades once you exceed 30-50 available tools". The 60 boundary is ours, and has no published source |
|
|
58
63
|
| E126 | warn/info | tool names mix conventions or exceed 64 chars |
|
|
59
64
|
| E127 | warn | a single tool definition costs 800+ tokens (needs `weigh` data) |
|
|
60
65
|
| E128 | warn | total tool-definition tax over ~10k / ~25k tokens, estimated (needs `weigh` data); hard CI failure is left to the `--max-tokens`/`--diff` gate |
|
|
@@ -71,6 +76,38 @@ never reused, so a gap is permanent and is better stated than smoothed over.
|
|
|
71
76
|
| S105 | info | instruction-injection patterns in the skill body or description (shallow heuristic, never a security verdict). **Reported but never scored** | internal |
|
|
72
77
|
| S106 | error/warn/info | referenced file missing, escaping the skill dir (`..`), or nested more than one level deep | agentskills.io |
|
|
73
78
|
|
|
79
|
+
## Findability (E141-E145)
|
|
80
|
+
|
|
81
|
+
Reported by `efaimo find`, never by `check`, and **never graded**. They answer a
|
|
82
|
+
question the other two families do not: when a host marks tools
|
|
83
|
+
`defer_loading: true`, the definitions stay out of the context window until a
|
|
84
|
+
search finds them, so a tool nothing surfaces costs nothing and does nothing.
|
|
85
|
+
Anthropic recommends turning that on once definitions pass ~10k tokens.
|
|
86
|
+
|
|
87
|
+
`find` prints two measured proportions, not a letter grade (ADR-030), and they
|
|
88
|
+
are different kinds of claim:
|
|
89
|
+
|
|
90
|
+
- **`distinct`** is a property of the catalog. A tool that owns no term the
|
|
91
|
+
other tools lack cannot be matched by any query that does not also match a
|
|
92
|
+
competitor. No model of how anyone searches is involved. This is the headline
|
|
93
|
+
and the number `--min-distinct` gates on.
|
|
94
|
+
- **`probe`** is a simulation: BM25 over the four searchable fields, each tool
|
|
95
|
+
queried with the top terms of its own description. It reads 100% on the
|
|
96
|
+
official reference server and on `@playwright/mcp`, so it is a floor test
|
|
97
|
+
that catches an empty description or a literal duplicate, not a ranking. The
|
|
98
|
+
output says so every run.
|
|
99
|
+
|
|
100
|
+
Method, parameters and limits: [METHODOLOGY.md](./METHODOLOGY.md).
|
|
101
|
+
|
|
102
|
+
| id | sev | what it catches |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| E141 | warn | the tool owns no term that no other tool has, so no search returns it without also returning a competitor. The one rule here that is not a heuristic |
|
|
105
|
+
| E142 | warn | the probe does not return the tool inside the result window (default 5), or its description offers no word to search for at all |
|
|
106
|
+
| E143 | warn | two tools score identically for one of their own queries, so only the alphabetical tie-break separates them |
|
|
107
|
+
| E144 | info | the tool name is built only from generic words (`run`, `execute_query`), so a broad pattern search reaches it only through its description |
|
|
108
|
+
| E145 | warn | Anthropic's guidance recommends tool search for a catalog this shape (10 or more tools, or over 10k tokens of definitions) **and** some tools cannot be singled out: once the catalog is deferred, those tools cost nothing and do nothing |
|
|
109
|
+
| E146 | info | every word the tool owns comes from its own name, so nothing in its description is unique. Names are searchable, so this is not E141; it means only someone who already knows the name can single it out |
|
|
110
|
+
|
|
74
111
|
## Notes on heuristics
|
|
75
112
|
|
|
76
113
|
Rules that scan **source code** (E101-E104, E108, E110, E111, E114, E115) are
|
|
@@ -4,7 +4,15 @@
|
|
|
4
4
|
name: contoso-crm-import helps on an unknowable format
|
|
5
5
|
skill: ./contoso-crm-import
|
|
6
6
|
model: claude-sonnet-5
|
|
7
|
+
# 8, because that is what the two committed runs in the README used. The dry
|
|
8
|
+
# run prints what a plan this size can detect: at 8 per arm nothing under about
|
|
9
|
+
# 63 points can reach p < 0.05, so raise it for anything less clear-cut than
|
|
10
|
+
# these two. The default when the key is absent is 20.
|
|
7
11
|
trials: 8
|
|
12
|
+
|
|
13
|
+
# The judge defaults to the model above, which means a model grades its own
|
|
14
|
+
# answers. Point it somewhere else to remove that. (unreleased: needs 0.2.0)
|
|
15
|
+
# judge_model: claude-opus-5
|
|
8
16
|
task: |
|
|
9
17
|
Prepare these contacts for Contoso CRM import:
|
|
10
18
|
|
|
@@ -4,7 +4,15 @@
|
|
|
4
4
|
name: csv-cleanup helps on a messy CSV
|
|
5
5
|
skill: ./csv-cleanup
|
|
6
6
|
model: claude-sonnet-5
|
|
7
|
+
# 8, because that is what the two committed runs in the README used. The dry
|
|
8
|
+
# run prints what a plan this size can detect: at 8 per arm nothing under about
|
|
9
|
+
# 63 points can reach p < 0.05, so raise it for anything less clear-cut than
|
|
10
|
+
# these two. The default when the key is absent is 20.
|
|
7
11
|
trials: 8
|
|
12
|
+
|
|
13
|
+
# The judge defaults to the model above, which means a model grades its own
|
|
14
|
+
# answers. Point it somewhere else to remove that. (unreleased: needs 0.2.0)
|
|
15
|
+
# judge_model: claude-opus-5
|
|
8
16
|
task: |
|
|
9
17
|
Clean this CSV and summarize what you changed:
|
|
10
18
|
|