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.
- package/README.md +464 -57
- package/dist/index.js +1143 -56
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,15 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/donaldrichard19-LVD/pattern-mcp/actions/workflows/publish.yml)
|
|
4
4
|
[](https://www.npmjs.com/package/pattern-mcp)
|
|
5
|
-
[](https://www.npmjs.com/package/pattern-mcp)
|
|
6
6
|
[](./LICENSE)
|
|
7
7
|
|
|
8
|
-
Pattern
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
63
|
-
|
|
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
|
|
749
|
-
ledger integrity + decision
|
|
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
|
-
(
|
|
958
|
-
|
|
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
|
|
979
|
-
|
|
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
|
-
|
|
1137
|
-
|
|
1138
|
-
the
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
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)
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
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
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
entries that predate
|
|
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`** --
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
`
|
|
1218
|
-
|
|
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
|
|
1492
|
-
|
|
1493
|
-
|
|
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
|