pattern-mcp 0.6.0 → 0.8.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 (3) hide show
  1. package/README.md +464 -57
  2. package/dist/index.js +1143 -56
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,15 +2,32 @@
2
2
 
3
3
  [![Publish](https://github.com/donaldrichard19-LVD/pattern-mcp/actions/workflows/publish.yml/badge.svg)](https://github.com/donaldrichard19-LVD/pattern-mcp/actions/workflows/publish.yml)
4
4
  [![npm version](https://img.shields.io/npm/v/pattern-mcp.svg)](https://www.npmjs.com/package/pattern-mcp)
5
- [![npm downloads](https://img.shields.io/npm/dm/pattern-mcp.svg)](https://www.npmjs.com/package/pattern-mcp)
5
+ [![npm downloads](https://img.shields.io/npm/dt/pattern-mcp.svg)](https://www.npmjs.com/package/pattern-mcp)
6
6
  [![MIT license](https://img.shields.io/badge/license-MIT-111111.svg)](./LICENSE)
7
7
 
8
- Pattern is an MCP server that checks a UI component need against real,
9
- current evidence before your agent commits to it, so a wrong decision
10
- gets caught before it's built, not after.
8
+ Pattern solves a simple problem: agents don't always follow your design
9
+ guidance. Instead of relying on the agent to interpret and follow
10
+ instructions, Pattern turns that guidance into a checkable process. It
11
+ evaluates UI components from external libraries or your own design
12
+ system against a requirements checklist, then tells the agent whether to
13
+ use an existing component or build one from scratch using a concrete
14
+ design reference.
11
15
 
12
16
  [Website](https://usepattern.sh) · [npm](https://www.npmjs.com/package/pattern-mcp) · [Report an issue](https://github.com/donaldrichard19-LVD/pattern-mcp/issues/new/choose)
13
17
 
18
+ <details>
19
+ <summary><strong>Contents</strong> (click to expand)</summary>
20
+
21
+ - [Install](#install) · [What Pattern Does](#what-pattern-does) · [How it works](#how-it-works) · [Quick Start](#quick-start) · [Try it](#try-it) · [Validation examples](#validation-examples)
22
+ - **Make the judgment call:** [`recommend_component`](#tool-recommend_component) · [`extract_requirements`](#tool-extract_requirements)
23
+ - **Track cost and outcome:** [`record_component_decision`](#tool-record_component_decision) · [`read_ledger`](#tool-read_ledger) · [`report_build_cost`](#tool-report_build_cost) · [`report_outcome_proxy`](#tool-report_outcome_proxy) · [Feature cost attribution](#feature-cost-attribution) · [Outcome proxies](#outcome-proxies) · [Per-project judgment ledger](#per-project-judgment-ledger)
24
+ - **Verify and export old decisions:** [`check_ledger_liveness`](#tool-check_ledger_liveness) · [`sweep_ledger_liveness`](#tool-sweep_ledger_liveness) · [`export_ledger_provenance`](#tool-export_ledger_provenance) · [`backfill_ledger_snapshot_ref`](#tool-backfill_ledger_snapshot_ref) · [`post_ledger_provenance_to_github`](#tool-post_ledger_provenance_to_github) · [Ledger integrity and decision provenance](#ledger-integrity-and-decision-provenance) (design overview — start here for how the five fit together)
25
+ - [Per-project decision memory](#per-project-decision-memory) · [Security and privacy](#security-and-privacy) · [Telemetry](#telemetry)
26
+ - **Cost:** [The `_meta` field](#the-_meta-field) · [Prompt caching](#prompt-caching) · [Measured cache and fetch behavior](#measured-cache-and-fetch-behavior) · [Search limits](#search-limits) · [Ensemble cost](#ensemble-cost-boundary-risk-cases-only) · [Session call cap](#session-call-cap)
27
+ - [Local call log](#local-call-log) · [Known limitations](#known-limitations)
28
+
29
+ </details>
30
+
14
31
  ## Install
15
32
 
16
33
  ```bash
@@ -32,13 +49,18 @@ whether to:
32
49
 
33
50
  Pattern is designed for agents to use **while they are building**.
34
51
 
35
- It exposes eight tools:
52
+ It exposes eleven tools, in three groups:
53
+
54
+ **Make the judgment call.**
36
55
 
37
56
  - `recommend_component` — evaluates a UI component need and returns a
38
57
  structured recommendation.
39
58
  - `extract_requirements` — runs just the requirement-extraction step on
40
59
  its own, so you can inspect or hand-edit the checklist before
41
60
  `recommend_component` spends its search+score budget on it.
61
+
62
+ **Track what it cost and what actually happened.**
63
+
42
64
  - `record_component_decision` — records what the agent actually did so
43
65
  future recommendations in the same project can take that decision into
44
66
  account.
@@ -54,13 +76,27 @@ It exposes eight tools:
54
76
  merge, kept-vs-replaced) for one feature, deliberately independent of
55
77
  Pattern's own verdict -- see [Outcome
56
78
  proxies](#outcome-proxies).
79
+
80
+ **Verify old decisions still hold up, and export a record of them.** See
81
+ [Ledger integrity and decision
82
+ provenance](#ledger-integrity-and-decision-provenance) for how these five
83
+ fit together.
84
+
57
85
  - `check_ledger_liveness` — checks whether a ledger entry's recorded
58
- `file_path` still exists and still references its `chosen_candidate` --
59
- see [Tool: `check_ledger_liveness`](#tool-check_ledger_liveness).
86
+ `file_path` still exists and still references its `chosen_candidate`.
87
+ - `sweep_ledger_liveness` — batch version of `check_ledger_liveness`
88
+ across a whole project (or every project in the ledger), plus
89
+ dangling-cluster detection. Meant to be invoked by your own cron/CI, not
90
+ something Pattern schedules itself.
60
91
  - `export_ledger_provenance` — formats one ledger entry as a stable
61
92
  markdown block (checklist, candidates, verdict, `snapshot_ref`) you can
62
- paste into a PR or issue by hand -- see [Tool:
63
- `export_ledger_provenance`](#tool-export_ledger_provenance).
93
+ paste into a PR or issue by hand.
94
+ - `backfill_ledger_snapshot_ref` — best-effort `snapshot_ref`
95
+ reconstruction for entries written before that field existed.
96
+ - `post_ledger_provenance_to_github` — posts an `export_ledger_provenance`
97
+ artifact as a real comment on a GitHub PR/issue, idempotently. The one
98
+ tool here with a real, visible side effect outside your own machine;
99
+ confirm with the user before calling it.
64
100
 
65
101
  ## How it works
66
102
 
@@ -564,6 +600,114 @@ The calling agent should:
564
600
 
565
601
  See [SECURITY.md](./SECURITY.md) for more details.
566
602
 
603
+ ## Tool: `register_design_system`
604
+
605
+ Points `recommend_component` at *this project's own* design system instead
606
+ of shadcn/ui, 21st.dev, and ReUI -- for a solo dev with their own component
607
+ library or design spec who wants Pattern's coverage scoring against
608
+ candidates they'll actually use, not external libraries they won't. This is
609
+ the Solo Dev architecture from `pattern-solo-design-system-architecture.md`:
610
+ local, per-project, one-or-the-other -- registering a design system for a
611
+ `project_id` **replaces** external-library scoring for that project
612
+ entirely, it does not add to it. There's no shared/remote ledger, no
613
+ multi-user attribution, and no team auth in this scope -- those are
614
+ deliberately deferred to a team phase, only if this use case proves out.
615
+
616
+ ### Input
617
+
618
+ Exactly one of `manifest_path` or `directory_path` is required, both
619
+ relative to the project root (`PATTERN_PROJECT_ROOT`, defaults to this
620
+ server's working directory) -- never an absolute path.
621
+
622
+ ```json
623
+ {
624
+ "project_id": "my-booking-app",
625
+ "directory_path": "src/components"
626
+ }
627
+ ```
628
+
629
+ - **`manifest_path`** -- a components manifest. Two recognized shapes:
630
+ - A hand-authored JSON array of `{name, props, description,
631
+ usage_example}` objects, optionally wrapped in `{"components": [...]}`.
632
+ - A Storybook-exported `stories.json`/`index.json` file (an object with a
633
+ top-level `entries` or `stories` map). Component names only in this
634
+ case -- Storybook's basic export doesn't carry prop data, so candidates
635
+ from this path start with an empty `props` list.
636
+ - **`directory_path`** -- a directory of real component source files,
637
+ scanned recursively for `.jsx`/`.tsx`/`.js`/`.ts` files (excluding
638
+ `node_modules`/`dist`/`build`/`.git` and `.test.`/`.spec.`/`.stories.`
639
+ files). Each exported, uppercase-named function or const component found
640
+ becomes a candidate, with props read in priority order from a
641
+ `<Name>Props` interface/type, a `.propTypes` block, or (last resort) the
642
+ component's own destructured parameters. This is a heuristic scan, not a
643
+ full parser -- a sparse or partial props list for some components is
644
+ expected, not a bug, especially on plain JS with no prop typing at all.
645
+
646
+ ### Output
647
+
648
+ ```json
649
+ {
650
+ "status": "registered",
651
+ "registration": {
652
+ "project_id": "my-booking-app",
653
+ "source_kind": "directory_scan",
654
+ "source_path": "src/components",
655
+ "registered_at": "2026-09-03T18:04:11.201Z",
656
+ "candidate_count": 29,
657
+ "candidates": [
658
+ { "name": "ReferralBanner", "props": ["code", "bonusAmount"], "description": null, "usage_example": null, "file_path": "rewards/ReferralBanner.jsx" }
659
+ ]
660
+ }
661
+ }
662
+ ```
663
+
664
+ Registering overwrites (does not merge with) any prior registration for the
665
+ same `project_id`. Once registered, `recommend_component` scores ONLY
666
+ against these candidates for calls with this `project_id` -- no separate
667
+ flag needed, it's automatic based on `project_id` alone, and step 3's live
668
+ web search is skipped entirely (`web_search` is still available, but
669
+ reserved for a `custom_build` verdict's Mobbin/Figma Community reference
670
+ grounding, same as the external-library path). A `use_existing` verdict
671
+ scored this way always carries `"source": "design_system"` on the
672
+ resulting ledger entry, set server-side regardless of what the model wrote,
673
+ so `read_ledger` and `export_ledger_provenance` can match on it reliably.
674
+
675
+ This only writes local config to `~/.pattern/design_systems.json` (override
676
+ with `PATTERN_DESIGN_SYSTEMS_PATH`) -- it never calls the Anthropic API.
677
+ Registration is a point-in-time snapshot, not a live link: re-run this
678
+ whenever the design system's own components change meaningfully.
679
+
680
+ ### A safety net for a missed match
681
+
682
+ The model can occasionally say `custom_build`/`no_candidates_found`
683
+ against a registered design system even when a real match is sitting
684
+ right there in its own prompt -- a reading-comprehension miss over its own
685
+ known-complete candidate list, not evidence the list was actually empty.
686
+ When this happens, `recommend_component`'s response may carry a
687
+ `design_system_recall_check` field: a deterministic, zero-cost, local
688
+ keyword-overlap check (component name, props, description/usage_example
689
+ vs. `component_need`/`domain`) run automatically whenever reason is
690
+ `no_candidates_found` in this mode.
691
+
692
+ ```json
693
+ {
694
+ "verdict": "custom_build",
695
+ "reason": "no_candidates_found",
696
+ "design_system_recall_check": {
697
+ "possible_missed_candidates": [
698
+ { "name": "ReferralBanner", "shared_keywords": ["referral", "bonus"] }
699
+ ],
700
+ "note": "These registered design-system candidates share keywords with this component_need but were not selected as a match -- the verdict may have missed a real one. This is a weak, keyword-only signal, not proof of an actual match: double-check these candidates yourself (or re-run this call) before trusting custom_build here."
701
+ }
702
+ }
703
+ ```
704
+
705
+ This never overrides the verdict -- a shared keyword is weak evidence, not
706
+ proof of a real match -- it only surfaces the risk so you (or the calling
707
+ agent) know to double-check before accepting a `custom_build` verdict at
708
+ face value. Absent entirely when there's no overlap, or outside
709
+ design-system mode.
710
+
567
711
  ## Tool: `extract_requirements`
568
712
 
569
713
  Runs only the requirement-extraction step `recommend_component` normally
@@ -739,14 +883,16 @@ came back with `served_from_ledger: true`.
739
883
  "file_path": null,
740
884
  "snapshot_ref": "a1b2c3d4e5f6...",
741
885
  "last_verified_live": null,
742
- "live_status": "unknown"
886
+ "live_status": "unknown",
887
+ "reconstructed_snapshot_ref": null
743
888
  }
744
889
  ]
745
890
  }
746
891
  ```
747
892
 
748
- `file_path`/`snapshot_ref`/`last_verified_live`/`live_status` are the
749
- ledger integrity + decision provenance fields -- see [Ledger integrity and
893
+ `file_path`/`snapshot_ref`/`last_verified_live`/`live_status`/
894
+ `reconstructed_snapshot_ref` are the ledger integrity + decision
895
+ provenance fields -- see [Ledger integrity and
750
896
  decision provenance](#ledger-integrity-and-decision-provenance) and [Tool:
751
897
  `check_ledger_liveness`](#tool-check_ledger_liveness). Entries written
752
898
  before this feature shipped read back with `file_path`/`snapshot_ref`/
@@ -954,8 +1100,11 @@ an arbitrary shell command.
954
1100
  ```
955
1101
 
956
1102
  `live_status` is one of `"live"`, `"orphaned"`, `"unknown"`, or
957
- (reserved, not yet produced -- see [Ledger integrity and decision
958
- provenance](#ledger-integrity-and-decision-provenance)) `"dangling"`.
1103
+ `"dangling"` (only ever produced by
1104
+ [`sweep_ledger_liveness`](#tool-sweep_ledger_liveness)'s cluster
1105
+ detection, never by a single-entry `check_ledger_liveness` call -- see
1106
+ [Ledger integrity and decision
1107
+ provenance](#ledger-integrity-and-decision-provenance)).
959
1108
  Entries with no `file_path` are listed but never checked or written to
960
1109
  `ledger_liveness.jsonl` -- their status is permanently `"unknown"` since
961
1110
  there's nothing to check. Results here are also layered onto
@@ -963,6 +1112,72 @@ there's nothing to check. Results here are also layered onto
963
1112
  entries afterward -- `check_ledger_liveness` is the only thing that
964
1113
  advances those fields past their write-time defaults.
965
1114
 
1115
+ ## Tool: `sweep_ledger_liveness`
1116
+
1117
+ Batch version of [`check_ledger_liveness`](#tool-check_ledger_liveness):
1118
+ updates `live_status` for every `file_path`-bearing entry across an
1119
+ entire project, or -- when `project_id` is omitted -- every `project_id`
1120
+ present in the ledger. This is the "on a schedule (project open or cron)"
1121
+ half of the referential-integrity design that `check_ledger_liveness`'s
1122
+ on-demand, single-project call doesn't cover.
1123
+
1124
+ **Pattern has no daemon or scheduler of its own.** Each server invocation
1125
+ is transient, tied to its MCP host's lifecycle -- there is nowhere inside
1126
+ this server for a cron job to live. This tool is meant to be invoked by
1127
+ whatever external scheduler you already have (a cron job, a CI step
1128
+ running nightly), not something Pattern triggers automatically or ever
1129
+ will on its own.
1130
+
1131
+ ### Input
1132
+
1133
+ ```json
1134
+ {
1135
+ "project_id": "my-booking-app"
1136
+ }
1137
+ ```
1138
+
1139
+ `project_id` is optional -- omit it to sweep every `project_id` present
1140
+ in the ledger in one call.
1141
+
1142
+ ### Output
1143
+
1144
+ ```json
1145
+ {
1146
+ "projects_swept": 2,
1147
+ "total_entries_checked": 14,
1148
+ "dangling_clusters": [
1149
+ { "project_id": "my-booking-app", "feature_id": "3f9a21c0", "entry_ids": ["...", "..."] }
1150
+ ],
1151
+ "per_project": [
1152
+ { "project_id": "my-booking-app", "checked": 9, "total_entries": 12, "dangling_clusters": 1 },
1153
+ { "project_id": "other-project", "checked": 5, "total_entries": 5, "dangling_clusters": 0 }
1154
+ ]
1155
+ }
1156
+ ```
1157
+
1158
+ ### Dangling clusters, and how "cluster" maps onto what the ledger actually stores
1159
+
1160
+ The ledger has no explicit entry-to-entry reference field -- each line is
1161
+ an independent judgment record. `feature_id` (see [Feature cost
1162
+ attribution](#feature-cost-attribution)) is the one real grouping
1163
+ construct that already exists, so a "cluster" here means every entry
1164
+ sharing one `feature_id`, and "no live anchor" means none of them
1165
+ resolved to `live_status: "live"`. A single-entry group is just an
1166
+ ordinary orphaned/unknown entry, not a cluster phenomenon, so groups of
1167
+ one are never flagged.
1168
+
1169
+ Every entry in a qualifying cluster gets `live_status: "dangling"` --
1170
+ overriding whatever `"orphaned"`/`"unknown"` value it had -- visible on
1171
+ its next `read_ledger`/`check_ledger_liveness` read via the same
1172
+ `ledger_liveness.jsonl` overlay `check_ledger_liveness` already writes to
1173
+ (see [Referential integrity](#referential-integrity-file_path--live_status)).
1174
+ Tested against the exact repro shape reported by a user: 13 entries, 12
1175
+ sharing a `feature_id` with no live anchor among them, 1 separate and
1176
+ live -- all 12 flag `dangling`, the 13th doesn't. Also tested at 200 and
1177
+ 1,000 synthetic entries without reintroducing search+score-class latency
1178
+ (both complete in well under a second -- this is `fs.existsSync` calls
1179
+ and in-memory grouping, not API calls).
1180
+
966
1181
  ## Tool: `export_ledger_provenance`
967
1182
 
968
1183
  Formats one ledger entry -- requirements checklist, candidates compared,
@@ -975,8 +1190,9 @@ design and its deliberate limits.
975
1190
  Pure and deterministic: the same entry always produces byte-identical
976
1191
  markdown, since the function reads nothing but its input (no live system
977
1192
  time, no disk state). This only formats and returns text -- it does not
978
- post anything to GitHub or anywhere else; that's a separate action, not
979
- yet built.
1193
+ post anything anywhere; see
1194
+ [`post_ledger_provenance_to_github`](#tool-post_ledger_provenance_to_github)
1195
+ below for that.
980
1196
 
981
1197
  ### Input
982
1198
 
@@ -1012,6 +1228,134 @@ custom-build reference (Mobbin/Figma) to the ledger (see
1012
1228
  A `null` `snapshot_ref` (project root wasn't a git repository at judgment
1013
1229
  time) renders as prose too, not the literal word `null`.
1014
1230
 
1231
+ ## Tool: `backfill_ledger_snapshot_ref`
1232
+
1233
+ Best-effort reconstruction of `snapshot_ref` for ledger entries written
1234
+ before that field existed (or written outside a git repository): finds
1235
+ the commit that was `HEAD` at or just before each entry's own timestamp
1236
+ (`git log --before=<timestamp> -1 --format=%H`). Entries that already
1237
+ have a real `snapshot_ref` are reported but never touched -- backfill
1238
+ only ever fills a gap, never second-guesses a captured value.
1239
+
1240
+ ### Input
1241
+
1242
+ ```json
1243
+ {
1244
+ "project_id": "my-booking-app",
1245
+ "ledger_entry_id": "a1b2c3d4-..."
1246
+ }
1247
+ ```
1248
+
1249
+ `ledger_entry_id` is optional -- omit it to backfill every entry in the
1250
+ project missing `snapshot_ref`.
1251
+
1252
+ ### Output
1253
+
1254
+ ```json
1255
+ {
1256
+ "project_id": "my-booking-app",
1257
+ "attempted": 3,
1258
+ "reconstructed": 2,
1259
+ "results": [
1260
+ { "ledger_entry_id": "a1b2c3d4-...", "already_had_snapshot_ref": false, "reconstructed_snapshot_ref": "9f3a1c7e2b0d4f5a6b7c8d9e0f1a2b3c4d5e6f70" },
1261
+ { "ledger_entry_id": "e5f6a7b8-...", "already_had_snapshot_ref": false, "reconstructed_snapshot_ref": null }
1262
+ ]
1263
+ }
1264
+ ```
1265
+
1266
+ ### A reconstructed value is always labeled, never presented as real
1267
+
1268
+ Necessarily an approximation, not a guarantee: a rebase, force-push, or
1269
+ history rewrite since that timestamp can make "the commit `HEAD` pointed
1270
+ to then" no longer resolve to what the codebase actually looked like at
1271
+ judgment time. Every attempt is persisted (including failures -- a
1272
+ project whose git history doesn't reach back that far, or that isn't a
1273
+ git repository at all) to `~/.pattern/snapshot_backfill.jsonl` (override
1274
+ with `PATTERN_SNAPSHOT_BACKFILL_PATH`), and surfaces on later reads as
1275
+ `reconstructed_snapshot_ref` -- a field kept fully separate from
1276
+ `snapshot_ref` itself, never overwriting or being confused with it.
1277
+ [`export_ledger_provenance`](#tool-export_ledger_provenance) and
1278
+ [`post_ledger_provenance_to_github`](#tool-post_ledger_provenance_to_github)
1279
+ both render a reconstructed value with an explicit "(reconstructed via
1280
+ backfill -- best-effort approximation, not the original captured
1281
+ snapshot)" label, never silently as if it were equivalent to a value
1282
+ captured live.
1283
+
1284
+ Tested against a real throwaway git repo with known commit history (an
1285
+ entry timestamped between two real commits reconstructs to exactly the
1286
+ first one), a 200-entry synthetic ledger outside any git repo (every
1287
+ attempt fails fast and reports `null` rather than throwing), and a
1288
+ read-only run against this project's own real `coop-commerce` ledger
1289
+ entries, per the spec's own test plan.
1290
+
1291
+ ## Tool: `post_ledger_provenance_to_github`
1292
+
1293
+ Posts one ledger entry's provenance artifact (the same content
1294
+ `export_ledger_provenance` produces) as a real comment on a GitHub PR or
1295
+ issue. **This is the one tool in this server with a real, visible side
1296
+ effect on a third-party service** -- every other tool here only ever
1297
+ touches local files. Confirm with the user before calling it, the same
1298
+ way you're expected to confirm before running a suggested
1299
+ `install_command` (see [Installation commands are not
1300
+ trusted](#installation-commands-are-not-trusted) and SECURITY.md).
1301
+
1302
+ GitHub treats a PR and an issue identically for comments (both use the
1303
+ same `/issues/{number}/comments` endpoint), so there's one input shape
1304
+ for both -- no separate "is this a PR" flag.
1305
+
1306
+ ### Auth: `GITHUB_TOKEN`, not a GitHub App
1307
+
1308
+ This resolves the open question left in [Ledger integrity and decision
1309
+ provenance](#ledger-integrity-and-decision-provenance)'s earlier writeup
1310
+ in favor of a **personal access token**, read from the `GITHUB_TOKEN`
1311
+ environment variable -- the same convention every GitHub Action and the
1312
+ `gh` CLI itself already use. Needs `repo` scope. A GitHub App was the
1313
+ alternative on the table, but it needs a hosted installation flow and a
1314
+ webhook receiver, which contradicts this project's entire distribution
1315
+ model (a local npm package, no hosted infrastructure -- see [Ledger
1316
+ integrity and decision provenance](#ledger-integrity-and-decision-provenance)
1317
+ and the Pattern Primer's build-order principle). Pattern manages no
1318
+ GitHub credential of its own, the same way it manages no git credential
1319
+ for `snapshot_ref` -- it just reads what's already in your environment.
1320
+
1321
+ ### Input
1322
+
1323
+ ```json
1324
+ {
1325
+ "project_id": "my-booking-app",
1326
+ "ledger_entry_id": "a1b2c3d4-...",
1327
+ "repo": "my-org/my-booking-app",
1328
+ "issue_number": 42
1329
+ }
1330
+ ```
1331
+
1332
+ All four fields are required.
1333
+
1334
+ ### Output
1335
+
1336
+ ```json
1337
+ {
1338
+ "posted": true,
1339
+ "comment_url": "https://github.com/my-org/my-booking-app/pull/42#issuecomment-...",
1340
+ "comment_id": 123456789
1341
+ }
1342
+ ```
1343
+
1344
+ ### Idempotent by construction
1345
+
1346
+ Every posted comment is prefixed with a hidden HTML marker keyed to the
1347
+ ledger entry's id (`<!-- pattern-ledger-provenance:<id> -->`). A call
1348
+ first checks the thread's existing comments (most recent 100 -- full
1349
+ pagination isn't handled yet) for that marker; if found, it returns
1350
+ `{ "posted": false, "reason": "already_posted", "comment_url": "..." }`
1351
+ pointing at the existing comment instead of creating a duplicate. A
1352
+ repeat call is always safe to make.
1353
+
1354
+ Errors (`isError: true`) clearly on: no `GITHUB_TOKEN` set, a malformed
1355
+ `repo` (not `owner/repo`), an unknown `ledger_entry_id`, or a GitHub API
1356
+ error (bad credentials, repo/issue not found, rate limit) -- the error
1357
+ message includes the real HTTP status and GitHub's own error text.
1358
+
1015
1359
  ## Feature cost attribution
1016
1360
 
1017
1361
  Every `recommend_component` call that writes to the ledger -- a fresh
@@ -1133,25 +1477,33 @@ Two gaps in the ledger, surfaced from user feedback: it tracks that a
1133
1477
  decision was made, but not whether the thing it decided about is still
1134
1478
  live in your codebase, and it stores the checklist/verdict but not a
1135
1479
  version pin or an exportable artifact you can attach to a PR or issue.
1136
- This section covers what's shipped so far -- **P0/P1 of both halves**, not
1137
- the full spec. See `pattern-ledger-integrity-and-provenance-spec.md` for
1138
- the complete phased plan; P2/P3 (a scheduled/batch sweep, dangling-cluster
1139
- detection, the provenance-artifact exporter, and GitHub PR/issue posting)
1140
- are not built yet.
1141
-
1142
- **This is the one deliberate exception** to Pattern otherwise having [no
1143
- filesystem/git access to your repo](#per-project-judgment-ledger) at all
1144
- (the principle `report_build_cost`/`report_outcome_proxy` are built
1145
- around). It's narrow on purpose:
1146
-
1147
- - `git rev-parse HEAD` (read-only, never touches repo state) to capture
1148
- `snapshot_ref` on every ledger write.
1480
+ Both are now fully addressed, across five tools:
1481
+ [`check_ledger_liveness`](#tool-check_ledger_liveness) and
1482
+ [`sweep_ledger_liveness`](#tool-sweep_ledger_liveness) close the first gap;
1483
+ [`export_ledger_provenance`](#tool-export_ledger_provenance),
1484
+ [`backfill_ledger_snapshot_ref`](#tool-backfill_ledger_snapshot_ref), and
1485
+ [`post_ledger_provenance_to_github`](#tool-post_ledger_provenance_to_github)
1486
+ close the second. See `pattern-ledger-integrity-and-provenance-spec.md`
1487
+ for the original phased plan this was built against.
1488
+
1489
+ **This required the one deliberate exception** to Pattern otherwise having
1490
+ [no filesystem/git access to your repo](#per-project-judgment-ledger) at
1491
+ all (the principle `report_build_cost`/`report_outcome_proxy` are built
1492
+ around). Still narrow, still all read-only, and still nothing here ever
1493
+ writes to your repo or runs an arbitrary git/shell command:
1494
+
1495
+ - `git rev-parse HEAD`, on every ledger write, to capture `snapshot_ref`.
1496
+ - `git log --before=<timestamp> -1 --format=%H`, only inside
1497
+ `backfill_ledger_snapshot_ref`, to reconstruct a best-effort
1498
+ `snapshot_ref` for an entry that predates it.
1149
1499
  - `fs.existsSync` plus a plain-text read of one file, only for a
1150
1500
  `file_path` you explicitly passed to `recommend_component`, only inside
1151
- `PROJECT_ROOT` (see below), to answer `check_ledger_liveness`.
1152
-
1153
- Nothing here runs an arbitrary git or shell command, and nothing writes to
1154
- your repo.
1501
+ `PROJECT_ROOT` (see below) -- what
1502
+ [`check_ledger_liveness`](#tool-check_ledger_liveness)/[`sweep_ledger_liveness`](#tool-sweep_ledger_liveness)
1503
+ check. `post_ledger_provenance_to_github` additionally makes a real,
1504
+ visible network call to the GitHub API -- see that tool's own docs,
1505
+ it's a materially different kind of exception (a third-party service,
1506
+ not your local machine) from the four above.
1155
1507
 
1156
1508
  ### `PROJECT_ROOT`
1157
1509
 
@@ -1182,11 +1534,12 @@ original judgment's snapshot, look up the entry named in its
1182
1534
  [`export_ledger_provenance`](#tool-export_ledger_provenance) packages one
1183
1535
  entry's full record -- checklist, candidates, verdict, `snapshot_ref` --
1184
1536
  into a markdown block you can paste into a PR or issue by hand.
1185
-
1186
- Not yet built (P2-P3 of Feature 2): the MCP action to post that artifact
1187
- to a GitHub PR/issue automatically (blocked on an open question -- personal
1188
- token vs. GitHub App -- see BACKLOG.md), and provenance backfill for
1189
- entries that predate `snapshot_ref`.
1537
+ [`post_ledger_provenance_to_github`](#tool-post_ledger_provenance_to_github)
1538
+ posts that same artifact automatically, idempotently, using a personal
1539
+ `GITHUB_TOKEN` rather than a GitHub App (see that tool's docs for why).
1540
+ [`backfill_ledger_snapshot_ref`](#tool-backfill_ledger_snapshot_ref)
1541
+ reconstructs a best-effort `snapshot_ref` for entries that predate the
1542
+ field, always clearly labeled as reconstructed wherever it's rendered.
1190
1543
 
1191
1544
  ### Referential integrity: `file_path` / `live_status`
1192
1545
 
@@ -1203,26 +1556,26 @@ check whether that file still exists and still mentions
1203
1556
  `PROJECT_ROOT`, or the file exists but `chosen_candidate` can't be
1204
1557
  confirmed in it. Deliberately the default outcome for anything
1205
1558
  ambiguous: a false `"orphaned"` is worse than a lingering `"unknown"`.
1206
- - **`dangling`** -- reserved, not yet produced. Feature 1's second
1207
- staleness type (a cluster of entries that only reference each other,
1208
- with no live anchor anywhere) is graph-level analysis across the whole
1209
- ledger, not a single-entry check -- P3, not built here.
1210
-
1211
- Checks are on-demand only right now (call `check_ledger_liveness`
1212
- yourself, or on whatever schedule you want) -- there's no automatic
1213
- sweep. `live_status`/`last_verified_live` start `"unknown"`/`null` on
1214
- every entry at write time and only ever advance via a
1215
- `check_ledger_liveness` call; results are stored append-only in
1216
- `~/.pattern/ledger_liveness.jsonl` (override with
1217
- `PATTERN_LEDGER_LIVENESS_PATH`, same "append, never mutate the source
1218
- line, most recent record wins at read time" convention as
1559
+ - **`dangling`** -- only ever produced by
1560
+ [`sweep_ledger_liveness`](#tool-sweep_ledger_liveness), never by
1561
+ `check_ledger_liveness` on its own: a cluster of 2+ entries sharing a
1562
+ `feature_id` where none of them resolved to `"live"`. Graph-level
1563
+ analysis across a project's whole entry set, not a single-entry check
1564
+ -- see that tool's docs for why `feature_id` is the grouping used.
1565
+
1566
+ `check_ledger_liveness` remains on-demand and single-project;
1567
+ [`sweep_ledger_liveness`](#tool-sweep_ledger_liveness) is the
1568
+ scheduled/batch counterpart -- meant to be invoked by your own cron/CI,
1569
+ since Pattern has no scheduler of its own. `live_status`/`last_verified_live`
1570
+ start `"unknown"`/`null` on every entry at write time and only ever
1571
+ advance via a `check_ledger_liveness`/`sweep_ledger_liveness` call;
1572
+ results are stored append-only in `~/.pattern/ledger_liveness.jsonl`
1573
+ (override with `PATTERN_LEDGER_LIVENESS_PATH`, same "append, never mutate
1574
+ the source line, most recent record wins at read time" convention as
1219
1575
  `outcome_proxies.jsonl`, see [Outcome proxies](#outcome-proxies)) and
1220
1576
  layered onto `ledger.jsonl`'s own entries at read time -- the ledger line
1221
1577
  itself is never rewritten.
1222
1578
 
1223
- Not yet built (P2-P3 of Feature 1): a scheduled/batch sweep across an
1224
- entire ledger, and dangling-cluster detection.
1225
-
1226
1579
  ## Per-project decision memory
1227
1580
 
1228
1581
  Pattern stores confirmed decisions locally in:
@@ -1451,6 +1804,38 @@ Pattern caches its system instructions using `cache_control: ephemeral`.
1451
1804
  The instructions are the same across calls, so repeated requests don't
1452
1805
  pay the full input cost for that block.
1453
1806
 
1807
+ ### Measured cache and fetch behavior
1808
+
1809
+ `_meta.tokens_used.input_breakdown` splits input tokens into `fresh`,
1810
+ `cache_write`, and `cache_read` (see [The `_meta`
1811
+ field](#the-_meta-field)) -- added specifically to check assumptions
1812
+ about caching against real numbers rather than guessing. Two real
1813
+ findings so far:
1814
+
1815
+ - **A single, non-repeat call is not "all fresh."** The working
1816
+ assumption had been that only exact-repeat calls (the [ledger cache
1817
+ hit](#the-cache-hit-exception)) benefit from caching at all. A live
1818
+ test disproved that: a fresh, non-repeat toast-component call came back
1819
+ with roughly half its input tokens served from `cache_read`. A
1820
+ follow-up 4-case sample (2026-09-02, spanning a clean `use_existing`
1821
+ call, a `custom_build` call, and two historically boundary/inconsistent
1822
+ cases) confirmed this wasn't a fluke -- `cache_read` share stayed in a
1823
+ 46-63% band across all four, regardless of call shape.
1824
+ - **`fresh` (fully-priced, never-cached) tokens are driven by whether the
1825
+ call reaches step 6's Mobbin/Figma reference search, not by general
1826
+ complexity or the boundary-risk ensemble firing.** In that same
1827
+ 4-case sample, the two `use_existing` calls had negligible `fresh`
1828
+ tokens (0.2%); both `custom_build` calls (which searched Mobbin/Figma)
1829
+ had 24-27.5% `fresh` -- even though, in both of those cases, the actual
1830
+ Mobbin *fetch* failed (`url_not_accessible`, 0 bytes returned). That
1831
+ rules out fetched-page content size as the driver for this cost --
1832
+ it's the extra Mobbin/Figma-restricted *search* calls themselves. This
1833
+ is why [`PATTERN_FETCH_MAX_CONTENT_TOKENS`](#fetch-grounded-scoring-and-reference-verification)
1834
+ was trimmed (a fetch-content cap can't fix a search-call cost) rather
1835
+ than split per-step as originally considered, and why reducing
1836
+ Mobbin/Figma search overhead is tracked as its own, differently-scoped
1837
+ future item rather than folded into that change.
1838
+
1454
1839
  ### Search limits
1455
1840
 
1456
1841
  Pattern limits candidate discovery to 3 web searches -- one per source.
@@ -1488,9 +1873,12 @@ the category page), there's no safe fallback for an unverified met/not-met
1488
1873
  call, so nothing is silently corrected -- `scoring_fetch` just tells you
1489
1874
  whether the grounding actually ran.
1490
1875
 
1491
- A fetch can read up to 15,000 content tokens. `web_fetch` has no separate
1492
- per-call fee; the cost comes from the content added to the model's
1493
- context.
1876
+ A fetch can read up to `PATTERN_FETCH_MAX_CONTENT_TOKENS` content tokens
1877
+ (default 12,000 -- trimmed from 15,000 after a real instrumentation
1878
+ sample showed the largest actual fetched page was ~10.7k tokens, see
1879
+ [Measured cache and fetch behavior](#measured-cache-and-fetch-behavior)
1880
+ above). `web_fetch` has no separate per-call fee; the cost comes from the
1881
+ content added to the model's context.
1494
1882
 
1495
1883
  ### Choosing a cheaper model
1496
1884
 
@@ -1727,6 +2115,25 @@ search or code problem.
1727
2115
 
1728
2116
  The boundary-risk ensemble exists to detect and surface this uncertainty.
1729
2117
 
2118
+ ### A missed match in your own design system isn't automatically re-checked
2119
+
2120
+ In [design-system mode](#tool-register_design_system), the model can say
2121
+ `custom_build`/`no_candidates_found` even when a real, relevant candidate
2122
+ is sitting right in its own prompt -- a reading-comprehension miss over a
2123
+ fully-known candidate list, not a live-search gap. The boundary-risk
2124
+ ensemble above doesn't catch this: it only re-checks a `"scored"` result
2125
+ near the 40%/80% threshold, never a `"no_candidates_found"` verdict.
2126
+
2127
+ The [keyword-overlap safety net](#a-safety-net-for-a-missed-match) flags
2128
+ this risk (`design_system_recall_check` on the response) but does not fix
2129
+ it -- it's a detection layer, not a re-check. The actual fix (re-running
2130
+ the model on a suspicious `no_candidates_found` verdict, the same way a
2131
+ close `"scored"` call already gets re-checked) is scoped but not built.
2132
+ `custom_build` ledger entries in this mode also record zero candidates
2133
+ (`candidates_evaluated: []`), same as the external-library path, so a
2134
+ genuine miss and a correct "nothing here" still look identical in
2135
+ `read_ledger` afterward unless the recall check happened to catch it.
2136
+
1730
2137
  ### A staged pipeline was evaluated and not adopted
1731
2138
 
1732
2139
  To address the variance above, an alternative architecture was built and