pattern-mcp 0.1.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +543 -0
  3. package/dist/index.js +1078 -0
  4. package/package.json +48 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Don Richard
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,543 @@
1
+ # Pattern
2
+
3
+ MCP server exposing two tools. `recommend_component` judges whether a UI
4
+ component need should be met with an existing shadcn/ui or 21st.dev
5
+ component, or requires a custom build guided by a real-app reference from
6
+ Mobbin and/or Figma Community. Returns a structured verdict, not a list of
7
+ search results — built for an agent to consume mid-build, not for a human
8
+ to browse. `record_component_decision` records a decision the calling agent
9
+ has actually acted on, so a later `recommend_component` call in the same
10
+ project can weigh it as a consistency signal — see
11
+ [Per-project decision memory](#per-project-decision-memory).
12
+
13
+ This implements the judgment layer validated in the product brief: field/
14
+ requirement coverage scored against real component evidence, thresholded
15
+ into `use_existing` / `custom_build`, with a `no_candidates_found` bucket
16
+ kept distinct from low coverage, a static skip-list for trivial primitives,
17
+ and a `computed_at` timestamp since coverage is a snapshot, not a permanent
18
+ fact.
19
+
20
+ ## How it works
21
+
22
+ The server does not scrape shadcn/21st.dev/Mobbin/Figma itself. Each tool
23
+ call makes one or more requests to the Anthropic Messages API
24
+ (`claude-sonnet-5` by default) with the server-side `web_search` tool
25
+ enabled, and a system prompt that encodes the full process: skip-list
26
+ check, requirement extraction, candidate search, real-evidence coverage
27
+ scoring, threshold, and — on `custom_build` — reference lookups against
28
+ Mobbin and Figma Community. No new credentials are required for the Figma
29
+ lookup — it uses the same plain `web_search` mechanism as everything else
30
+ in the tool, not the Figma API. The model returns structured JSON; the
31
+ server recomputes the coverage fraction from the `requirements_checked`
32
+ array itself (rather than trusting the model's stated percentage) and
33
+ applies the verdict/confidence threshold in code.
34
+
35
+ **Boundary-risk ensemble.** Validation found that a single run's coverage
36
+ score can vary between calls on the same input — not because search results
37
+ differ, but because the model can judge the same piece of evidence
38
+ differently run to run (see Known limitations). When a call's recounted
39
+ coverage lands close enough to a threshold boundary to plausibly flip the
40
+ verdict, the server automatically re-runs the judgment 2 more times and
41
+ takes the majority verdict. If the 3 runs disagree (a 2/3 split), the result
42
+ ships with `confidence: "low"` and an `ensemble` field so the calling agent
43
+ can see it was a close call rather than a confident read. Calls that land
44
+ clearly inside a threshold band never trigger this and stay single-run —
45
+ see [Cost](#cost) below for the measured impact.
46
+
47
+ Trivial primitives (button, input, checkbox, label, badge, spinner, tooltip,
48
+ avatar, icon) are caught locally before any API call, so they don't spend a
49
+ request.
50
+
51
+ ## Setup — quickstart
52
+
53
+ ```bash
54
+ git clone <this repo>
55
+ cd pattern-mcp
56
+ npm install
57
+ npm run build
58
+ ```
59
+
60
+ Requires `ANTHROPIC_API_KEY` — the account whose key you use pays for every
61
+ call this tool makes (see [Cost](#cost) below). Get one from the
62
+ [Anthropic Console](https://console.anthropic.com) (Settings → API Keys);
63
+ this requires its own billing setup. **This is not the same thing as a
64
+ Claude.ai or Claude Code subscription** — a Pro/Max plan does not cover
65
+ API usage, and a subscription login won't get you a key. You need a
66
+ separate Console account with credits or a payment method attached.
67
+
68
+ **Point your MCP client at it** — this is a standard MCP server, so it works
69
+ with any MCP-compatible client, not just one. Drop this into your client's
70
+ config (adjusting the path per client), swapping in your own project path
71
+ and key:
72
+
73
+ - **Claude Code**: either add `"pattern": { ... }` (the
74
+ block below) to the `mcpServers` object in `.mcp.json` at your project
75
+ root, or run:
76
+ ```bash
77
+ claude mcp add pattern \
78
+ -e ANTHROPIC_API_KEY=sk-ant-... \
79
+ -- node /absolute/path/to/pattern-mcp/dist/index.js
80
+ ```
81
+ This registers under `--scope local` (the default) — tied to the
82
+ current project directory only. Add `--scope user` (or `-s user`)
83
+ instead to make it available across **all** your projects:
84
+ ```bash
85
+ claude mcp add pattern \
86
+ -e ANTHROPIC_API_KEY=sk-ant-... \
87
+ --scope user \
88
+ -- node /absolute/path/to/pattern-mcp/dist/index.js
89
+ ```
90
+ **Flag order matters here.** `-e`/`--env` and `-s`/`--scope` must come
91
+ *before* the `--` separator and command — `claude mcp add`'s
92
+ `[args...]` capture is variadic, so a flag placed *after* the command
93
+ (e.g. `node dist/index.js --scope user`) is liable to be swallowed as
94
+ an argument to `node` itself instead of being parsed as a flag for
95
+ `claude mcp add`. Keep all your flags on the left of `--`, the command
96
+ and its own args on the right.
97
+
98
+ `claude mcp add` stores this in `~/.claude.json` (a local- or
99
+ user-scoped entry depending on `--scope`), not in a project file —
100
+ check with `claude mcp list` (should show
101
+ `pattern ... ✔ Connected`). Avoid `claude mcp get
102
+ pattern` if you can — it prints your key back to the
103
+ terminal in plaintext, so `claude mcp list`'s connection status is
104
+ usually enough without that risk.
105
+ - Cursor: `.cursor/mcp.json`
106
+ - Codex CLI: `~/.codex/config.toml` (global) or `.codex/config.json`
107
+ (project-level) — same `mcpServers` shape, TOML or JSON depending on file
108
+ - Claude Desktop: its MCP settings file
109
+
110
+ ```json
111
+ {
112
+ "mcpServers": {
113
+ "pattern": {
114
+ "command": "node",
115
+ "args": ["/absolute/path/to/pattern-mcp/dist/index.js"],
116
+ "env": { "ANTHROPIC_API_KEY": "sk-ant-..." }
117
+ }
118
+ }
119
+ }
120
+ ```
121
+
122
+ Restart your MCP client, then confirm it picked up the tool — ask your
123
+ agent to list its available MCP tools and look for `recommend_component`.
124
+ For Claude Code specifically, `claude mcp list` will show a health-checked
125
+ `✔ Connected` status without needing to ask the agent directly.
126
+
127
+ ## Try it
128
+
129
+ Ask your agent something like: *"Use recommend_component to find me a UI
130
+ component for a price breakdown showing nightly rate, cleaning fee, service
131
+ fee, and taxes — I'm building an Airbnb-style booking checkout in React with
132
+ Tailwind."* The agent should call the tool and act on the verdict directly
133
+ (install a real component, or start from the returned checklist and
134
+ Mobbin/Figma Community reference) rather than just describing what it
135
+ found.
136
+
137
+ **What you'll actually see:** both verdict paths now include a written,
138
+ grounded description, not just a bare link or install command. A
139
+ `use_existing` verdict includes `component_description` — what the
140
+ recommended component actually does and looks like, described before the
141
+ agent installs anything. A `custom_build` verdict includes
142
+ `reference_description` for each reference it found — what that Mobbin
143
+ screen or Figma Community file actually shows. Either way, testers get a
144
+ specific, readable description grounded in what the model actually found
145
+ during search, not generic filler.
146
+
147
+ If you want to sanity-check the tool itself rather than a real feature,
148
+ these five needs are the ones this project's own validation was built
149
+ against, spanning the full range of outcomes (clean commodity match,
150
+ false-positive-prone case, zero candidates, and boundary/near-tie cases):
151
+ price breakdown with fees and taxes, cancellation policy display, host
152
+ earnings dashboard, image gallery for a property listing, and a host-guest
153
+ messaging inbox — all in the same Airbnb-style rental marketplace domain.
154
+
155
+ ## Tool: `recommend_component`
156
+
157
+ **Input:**
158
+ ```json
159
+ {
160
+ "component_need": "price breakdown with fees and taxes",
161
+ "domain": "Airbnb-style rental marketplace",
162
+ "framework": "React + Tailwind",
163
+ "existing_stack": "already using shadcn/ui",
164
+ "project_id": "my-booking-app"
165
+ }
166
+ ```
167
+ `component_need` should be specific, not a category — "price breakdown with
168
+ fees and taxes" not "pricing". Vague category names are what produced
169
+ false-positive matches during validation (a generic SaaS pricing-tier
170
+ component scoring as a match for a booking checkout).
171
+
172
+ `project_id` is optional — a project name or path the calling agent
173
+ supplies. When present, past decisions recorded for that same `project_id`
174
+ via `record_component_decision` are pulled from
175
+ [per-project decision memory](#per-project-decision-memory) and included in
176
+ the prompt as a *signal, not a rule*: the model is instructed to weigh
177
+ consistency with a highly similar past decision, but never to let it
178
+ override a genuinely better match this search finds, and never to skip
179
+ searching or scoring because a past decision exists. Coverage is still
180
+ computed fresh on every call regardless — see
181
+ [No caching, by design](#known-limitations-carried-over-from-validation).
182
+ Omit `project_id` to skip memory entirely; there's no shared/global bucket
183
+ it falls back to.
184
+
185
+ **Output:** JSON matching:
186
+ ```json
187
+ {
188
+ "verdict": "use_existing | custom_build",
189
+ "confidence": "high | medium | low",
190
+ "reason": "scored | no_candidates_found | skip_list",
191
+ "computed_at": "2026-08-23",
192
+ "requirements_checked": [ { "requirement": "...", "met": true, "evidence": "..." } ],
193
+ "coverage": "5/7 (71%)",
194
+ "recommendation": {
195
+ "source": "21st.dev | shadcn | null",
196
+ "install_command": "string | null",
197
+ "component_description": "string (use_existing only) | null",
198
+ "reference": {
199
+ "source": "Mobbin | Figma Community",
200
+ "url": "...",
201
+ "flow_name": "... (Mobbin only)",
202
+ "file_name": "... (Figma Community only)",
203
+ "reference_description": "...",
204
+ "url_type": "deep_link | entry_point"
205
+ }
206
+ },
207
+ "ensemble": { "triggered": false },
208
+ "past_decision_signal": { "considered": true, "note": "..." }
209
+ }
210
+ ```
211
+ `ensemble.triggered` is `false` on the normal single-pass path. On a
212
+ boundary-risk coverage result it becomes
213
+ `{ "triggered": true, "runs": ["use_existing", "custom_build", "use_existing"], "agreement": "2/3" }`
214
+ — see [Ensemble cost](#ensemble-cost-boundary-risk-cases-only) below.
215
+
216
+ `past_decision_signal` only appears when `project_id` was provided **and**
217
+ that project has at least one past decision recorded — omitted entirely
218
+ otherwise, never a hollow `{ "considered": false }` on a call with nothing
219
+ to consider. `considered` is `true` only when a past decision was
220
+ genuinely similar enough to factor into scoring or recommendation, not
221
+ just present in the list; `note` names which decision and how, or why none
222
+ applied. This is enforced server-side, not just prompted: a
223
+ `considered`/`note` pair the model returns on a call that had no
224
+ past-decision context in its prompt is discarded rather than trusted — see
225
+ [Per-project decision memory](#per-project-decision-memory).
226
+
227
+ **`recommendation.reference` shape depends on how many sources actually
228
+ grounded**, not just on the verdict. On a `custom_build` verdict:
229
+ - Both Mobbin and Figma Community returned a real, grounded result:
230
+ `reference` is an **array of both** objects.
231
+ - Only one of the two grounded: `reference` is a **single object**, same
232
+ shape as before this feature existed — never a one-element array.
233
+ - Neither grounded: `reference` is `null`, same as today's
234
+ no-fabrication rule for a Mobbin-only lookup that found nothing.
235
+
236
+ No new credentials are required for the Figma Community reference — it
237
+ uses the same `web_search` mechanism as every other lookup in this tool,
238
+ not the Figma API, so there's no separate token to configure.
239
+
240
+ **`reference.url_type` tells you whether the URL is a deep link or just a
241
+ search entry point.** A Mobbin or Figma Community search result is very
242
+ often a category/browse page (e.g.
243
+ `mobbin.com/explore/mobile/screens/notifications`), not a direct link to
244
+ the specific screen or flow the model actually identified (e.g. "Saturn
245
+ Calendar - Notifications List") — the original gap this field exists to
246
+ disclose. On a `custom_build` verdict:
247
+
248
+ - **Mobbin**: the server fetches the search result page (via the
249
+ `web_fetch` tool) and looks for a more specific permalink to the
250
+ identified screen/flow actually written on that page. Found and
251
+ confirmed → `url_type: "deep_link"` and `url` is that permalink. Not
252
+ found (including when the fetch itself fails) → `url_type:
253
+ "entry_point"`, `url` stays the category/search page, and
254
+ `reference_description` is guaranteed to say so explicitly (append or
255
+ auto-generated server-side, never left to the model alone) — so a
256
+ reader always knows whether they're getting the exact screen or a
257
+ browse page they'll need to search themselves.
258
+ - **Figma Community**: a result URL containing `/community/file/` is
259
+ already file-specific by Figma's own URL structure, so it's treated as
260
+ `url_type: "deep_link"` without spending a fetch on it. A result that
261
+ *isn't* a `/community/file/` URL (an occasional browse/tag page) goes
262
+ through the same fetch-and-verify path as Mobbin. In practice a Figma
263
+ fetch will almost always fail regardless — `figma.com/robots.txt`
264
+ disallows `ClaudeBot` site-wide — so a non-file Figma result reliably
265
+ ends up `entry_point`, honestly.
266
+
267
+ This is enforced the same way as every other grounding rule in this
268
+ project: **server-side, not just prompt instruction.** A claimed deep
269
+ link is only kept if it's literally present in the text of a page the
270
+ server actually fetched; a claim that fails that check is silently
271
+ replaced with a real URL from an actual search/fetch result (never
272
+ discarded to a guess), and the entry-point caveat is force-appended to
273
+ `reference_description` if the model's own text didn't already disclose
274
+ it. `src/index.ts`'s `applyDeepLinkGrounding` is the single place this
275
+ happens — see its comments for the exact rules, including why a
276
+ model-guessed URL-pattern retry (e.g. stripping a path segment after a
277
+ fetch fails) is both prompted against and independently rejected by the
278
+ `web_fetch` tool itself (`url_not_in_prior_context`).
279
+
280
+ **`install_command` is untrusted text.** It's derived from a web search
281
+ result the model read, not a verified package registry, and the server
282
+ does not execute or validate it. The calling agent is instructed (in the
283
+ tool description and system prompt) to always display it to the user for
284
+ confirmation before running it, and never execute it automatically or
285
+ silently — this is expected agent behavior this project depends on, not
286
+ something the server enforces. See [SECURITY.md](./SECURITY.md).
287
+
288
+ ## Tool: `record_component_decision`
289
+
290
+ Records a decision the calling agent has actually acted on — call it
291
+ **after** installing an existing component or finishing a custom build, not
292
+ on every `recommend_component` verdict returned. Its only job is appending
293
+ one entry to local [per-project decision memory](#per-project-decision-memory);
294
+ it runs no judgment logic and makes no Anthropic API call, so it's
295
+ effectively free and instant.
296
+
297
+ **Input:**
298
+ ```json
299
+ {
300
+ "project_id": "my-booking-app",
301
+ "component_need": "price breakdown with fees and taxes",
302
+ "domain": "Airbnb-style rental marketplace",
303
+ "action": "custom_built",
304
+ "source": "custom",
305
+ "timestamp": "2026-08-25T14:32:00.000Z"
306
+ }
307
+ ```
308
+ - `project_id` (required) — must match the `project_id` you pass to
309
+ `recommend_component` for this decision to ever be surfaced there. Use a
310
+ stable value, e.g. the project's directory path or name.
311
+ - `component_need` (required), `domain` (optional) — same fields as
312
+ `recommend_component`'s input; free text, not matched against anything
313
+ server-side.
314
+ - `action` (required) — `"installed"` or `"custom_built"`.
315
+ - `source` (required) — e.g. `"shadcn"`, `"21st.dev"`, or `"custom"` for a
316
+ custom build.
317
+ - `timestamp` (optional) — ISO 8601; defaults to the current time if
318
+ omitted.
319
+
320
+ **Output:**
321
+ ```json
322
+ { "status": "recorded", "project_id": "my-booking-app", "entry": { "...": "..." } }
323
+ ```
324
+
325
+ ## Per-project decision memory
326
+
327
+ `record_component_decision` appends to a local JSON file, default path
328
+ `~/.pattern/memory.json`, overridable via
329
+ `PATTERN_MEMORY_PATH` — same override pattern as
330
+ [`PATTERN_LOG_PATH`](#local-call-log). It's a flat object keyed by
331
+ `project_id`, each value an array of decision entries in the same shape as
332
+ `record_component_decision`'s input (minus `project_id` itself, since
333
+ that's the key):
334
+
335
+ ```json
336
+ {
337
+ "my-booking-app": [
338
+ {
339
+ "component_need": "price breakdown with fees and taxes",
340
+ "domain": "Airbnb-style rental marketplace",
341
+ "action": "custom_built",
342
+ "source": "custom",
343
+ "timestamp": "2026-08-25T14:32:00.000Z"
344
+ }
345
+ ]
346
+ }
347
+ ```
348
+
349
+ Each project's array is capped at the **50 most recent entries** — once a
350
+ project hits the cap, the oldest entry is dropped as a new one is added, so
351
+ the file stays bounded for a long-lived project without manual cleanup.
352
+
353
+ **Only explicitly confirmed decisions are stored here — not every verdict
354
+ `recommend_component` returns.** The server never writes to this file on
355
+ its own; `recommend_component` only ever *reads* it (when `project_id` is
356
+ provided) and never writes to it. A verdict you don't act on, or act on
357
+ differently than recommended, leaves no trace here unless you call
358
+ `record_component_decision` yourself to say what you actually did.
359
+
360
+ **This is local-only plaintext**, same caveat pattern as the
361
+ [local call log](#local-call-log): nothing in this file is sent anywhere by
362
+ this server. `component_need` and `domain` are written here the same way
363
+ they're written to `calls.log` — see
364
+ [SECURITY.md](./SECURITY.md#what-actually-leaves-your-machine) before
365
+ putting anything sensitive in those fields. A write failure (disk full,
366
+ read-only filesystem, permissions) surfaces as a tool error on
367
+ `record_component_decision` itself, since — unlike the best-effort call
368
+ log — writing the decision *is* that tool's entire job, not a side effect
369
+ of it.
370
+
371
+ **This does not weaken the no-verdict-caching rule.** Memory only ever adds
372
+ past-decision context to the prompt for a fresh judgment pass — see
373
+ [No caching, by design](#known-limitations-carried-over-from-validation)
374
+ and the `project_id` note under
375
+ [Tool: `recommend_component`](#tool-recommend_component). Coverage is
376
+ recomputed from a real search every single call, with or without a
377
+ `project_id`.
378
+
379
+ ## Cost
380
+
381
+ A single pass (search → score → respond) costs roughly $0.06–$0.10 with
382
+ Sonnet 5 at current pricing ($2/M input, $10/M output, $0.01 per
383
+ web_search call) — skip-listed primitives cost $0 since they never reach
384
+ the API. Three things keep a single pass down without touching quality:
385
+
386
+ - **Prompt caching** on the system block (`cache_control: ephemeral`) —
387
+ the instructions are identical every call, so repeated turns and repeated
388
+ invocations read from cache instead of re-billing full price.
389
+ - **A 2-search budget** for candidate discovery, plus 2 more reserved
390
+ specifically for the `custom_build` reference lookups (one each for
391
+ Mobbin and Figma Community) so neither has to compete with discovery
392
+ for the same cap — shadcn and 21st.dev are searched in the same turn
393
+ rather than sequentially, so the growing conversation gets re-sent
394
+ fewer times per call.
395
+ - **A separate 2-call `web_fetch` budget**, used only for the step-6
396
+ deep-link check described above (`max_content_tokens: 15000` caps what
397
+ a single category-page fetch can cost). `web_fetch` itself has no
398
+ per-call charge beyond the tokens the fetched page adds to context, and
399
+ the system prompt explicitly reserves this tool for step 6 only — the
400
+ model is instructed not to reach for it during requirement scoring
401
+ (step 4), so it doesn't compete with the reference lookups it exists
402
+ for.
403
+ - **`PATTERN_MODEL` env var** (defaults to `claude-sonnet-5`) — lets you
404
+ swap in a cheaper model (e.g. Haiku 4.5) without a code change. Before
405
+ trusting a cheaper model in production, re-run the 5 validated test cases
406
+ from the product brief (price breakdown, cancellation policy, earnings
407
+ dashboard, gallery, messaging) and diff the verdicts against Sonnet's —
408
+ this hasn't been tested, only reasoned about.
409
+
410
+ ### Ensemble cost (boundary-risk cases only)
411
+
412
+ Testing found that a single pass isn't reliable near the verdict
413
+ thresholds: with the requirement checklist fixed at exactly 8 items,
414
+ coverage can only land on one of 9 discrete values (0, 12.5, 25, 37.5,
415
+ 50, 62.5, 75, 87.5, 100%), and the 40%/80% thresholds sit *between* two
416
+ of those values (37.5↔50, and 75↔87.5). For met-counts of 3, 4, 6, or 7,
417
+ a single item's met/unmet judgment flipping is enough to change the
418
+ verdict — and it does, run to run, on identical input.
419
+
420
+ To catch that, the server runs a **targeted ensemble**: every pass still
421
+ runs once as normal, but if the result lands on one of those four risky
422
+ met-counts (`isBoundaryRisk` in `src/index.ts`), it triggers 2 additional
423
+ full passes (3 total) and takes the majority verdict. Confidence is
424
+ forced to `"low"` on a genuine 2/3 split, regardless of what any
425
+ individual pass reported — a real disagreement across identical inputs
426
+ is uncertainty the tool should surface, not paper over. Everything else
427
+ (0, 1, 2, 5, 8 met — far enough from both thresholds that a 1-item swing
428
+ can't flip the verdict) returns the single pass as-is, at 1x cost. An
429
+ earlier version also triggered on `reason: "no_candidates_found"`
430
+ (a separate source of run-to-run inconsistency); that trigger was removed
431
+ after testing showed it never actually changed a verdict in this
432
+ session and was pure added cost.
433
+
434
+ The output includes an `ensemble` field so callers can see whether this
435
+ happened: `{ "triggered": false }` on the fast path, or
436
+ `{ "triggered": true, "runs": ["use_existing", "custom_build", "use_existing"], "agreement": "2/3" }`
437
+ when it fired.
438
+
439
+ **Measured cost, not just worst case:** across the last 5-case × 3-run
440
+ test batch (15 outer calls), 8 stayed single-run and 7 triggered the
441
+ ensemble (21 calls), for **29 total API calls — a ~1.9x blended average
442
+ multiplier**, not the 3x a naive "ensemble triggered" framing implies.
443
+ Worst case is still 3x per call when it triggers; most calls don't.
444
+
445
+ Ensembling does *not* fully eliminate the underlying variance for the
446
+ hardest cases. When a case's true coverage sits close enough to a
447
+ threshold that per-item judgment is close to a coin flip, majority-of-3
448
+ is a noisy estimator: it protects any single call against one unlucky
449
+ draw, but a *different* set of 3 draws on the next invocation can still
450
+ land on the other side. One case (image gallery) kept flipping across
451
+ outer runs even with the ensemble active, always with a 2/3 split and
452
+ `confidence: "low"` — the tool is correctly reporting low confidence on
453
+ a genuinely ambiguous case rather than a bug to fix with a bigger N.
454
+
455
+ ### Session call cap
456
+
457
+ The server caps itself at **40 calls per process lifetime** by default,
458
+ configurable via `PATTERN_SESSION_CAP`. This protects against a
459
+ *buggy calling agent* looping on the tool — a retry loop, a stuck agent
460
+ re-calling the same need repeatedly — not against normal project usage.
461
+ The number is grounded in real usage, not arbitrary: a full pass through
462
+ a realistic ~25-component project (scaled up from this project's own
463
+ 5-case Airbnb-style validation list) costs 25 calls, so 40 leaves
464
+ headroom for iteration on top of that without being so high it fails to
465
+ catch an actual runaway loop before it gets expensive. Skip-listed
466
+ primitives don't count toward the cap, since they never reach the API.
467
+ The counter is in-memory and resets when the server process restarts —
468
+ raise the cap via the env var if 40 is genuinely too low for your
469
+ project, don't just restart repeatedly to reset it.
470
+
471
+ ## Local call log
472
+
473
+ Every call that reaches the API (skip-list hits excluded, same exclusion
474
+ as the session cap) appends one JSON line to a local log file — default
475
+ path `~/.pattern/calls.log`, overridable via
476
+ `PATTERN_LOG_PATH`. This is **local-only**: nothing here is sent
477
+ anywhere by this server, it's purely for your own debugging/usage
478
+ visibility.
479
+
480
+ Each line looks like:
481
+ ```json
482
+ {"timestamp":"2026-08-24T21:12:43.882Z","component_need":"cancellation policy display","domain":"Airbnb-style rental marketplace","framework":"React + Tailwind","verdict":"custom_build","confidence":"high","reason":"scored","coverage":"2/8 (25%)","ensemble_triggered":false,"reference_sources_grounded":["Mobbin","Figma Community"]}
483
+ ```
484
+ `ensemble_agreement` is only present when `ensemble_triggered` is `true`.
485
+ `reference_sources_grounded` is only present on `custom_build` verdicts,
486
+ and only lists sources (`"Mobbin"`, `"Figma Community"`) that actually
487
+ grounded — matches whatever `recommendation.reference` ended up being
488
+ after grounding is enforced (see the
489
+ [Tool](#tool-recommend_component) section above for the full shape
490
+ rules).
491
+
492
+ **Deliberately excluded**: full `requirements_checked` evidence text, and
493
+ the API key — never written here. **Included in plaintext**:
494
+ `component_need` and `domain` — see
495
+ [SECURITY.md](./SECURITY.md#what-actually-leaves-your-machine) before
496
+ putting anything sensitive in those fields. The log directory is created
497
+ automatically if it doesn't exist, and a write failure (disk full,
498
+ read-only filesystem, permissions) is caught and reported to stderr —
499
+ it never breaks the tool call itself.
500
+
501
+ **Reviewing a log file** — including a tester's, if they send you
502
+ theirs (there's no automatic collection; this project doesn't phone
503
+ home): run `node summarize-log.js [path]`, defaulting to the same
504
+ location the server itself uses. It prints a verdict/confidence/reason
505
+ breakdown, ensemble trigger and agreement rates, reference-source
506
+ grounding rates on `custom_build` verdicts, and flags any
507
+ `component_need` called more than once — a signal worth checking
508
+ against the [session cap](#session-call-cap) if you see it.
509
+
510
+ ## Known limitations (carried over from validation)
511
+
512
+ - **Evidence judgment varies run to run, independent of search results.**
513
+ Validation traced a real case where two runs found the exact same named
514
+ candidate components via the exact same search queries, but the model
515
+ judged the same evidence differently — e.g. reading one candidate's
516
+ "Export" action as present in one run and absent in another, for the
517
+ identical component. This isn't a search-consistency or code bug; it's
518
+ inherent to how the model reads natural-language evidence, and it's what
519
+ the boundary-risk ensemble exists to catch and disclose (as a 2/3
520
+ `agreement` split) rather than eliminate. If you see a verdict flip
521
+ between your own runs on the same input, this is almost certainly why.
522
+ - **No caching, by design.** Every call re-searches and re-scores from
523
+ scratch. A `custom_build` verdict can go stale as libraries ship new
524
+ components (validated: shadcn's June 2026 chat primitives turned a likely
525
+ custom-build messaging component into a near-perfect match). If you add
526
+ caching at the calling-agent layer, keep it session-scoped only — never
527
+ persist a verdict across sessions or builds. This still holds with
528
+ [per-project decision memory](#per-project-decision-memory) in the
529
+ picture: memory only ever adds context to the prompt for a fresh
530
+ judgment pass, it never substitutes for one — a `recommend_component`
531
+ call with a `project_id` still always re-searches and re-scores.
532
+ - **Skip-list is a starting point, not validated against real usage yet.**
533
+ Log every call and whether it hit the skip-list; watch for agents calling
534
+ the tool anyway on skip-listed items (list too narrow) or shipping generic
535
+ UI for something that should've been skipped (list missing an entry).
536
+ - **Not testable end-to-end in a fully sandboxed environment.** This server
537
+ needs outbound network access to `api.anthropic.com` plus whatever the
538
+ model's web_search tool reaches — it won't run somewhere that blocks
539
+ general internet access.
540
+ - **Requirement extraction and coverage scoring are judgment calls made by
541
+ the model**, not deterministic lookups, even with the ensemble and
542
+ server-side recount in place. Spot-check early outputs against real
543
+ components before trusting the pipeline unattended.