pattern-mcp 0.2.0 → 0.4.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 +503 -27
- package/dist/index.js +874 -37
- package/dist/telemetry.js +222 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/pattern-mcp)
|
|
6
6
|
[](./LICENSE)
|
|
7
7
|
|
|
8
|
-
Pattern is an MCP server that
|
|
9
|
-
|
|
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.
|
|
10
11
|
|
|
11
12
|
[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)
|
|
12
13
|
|
|
@@ -31,7 +32,7 @@ whether to:
|
|
|
31
32
|
|
|
32
33
|
Pattern is designed for agents to use **while they are building**.
|
|
33
34
|
|
|
34
|
-
It exposes
|
|
35
|
+
It exposes six tools:
|
|
35
36
|
|
|
36
37
|
- `recommend_component` — evaluates a UI component need and returns a
|
|
37
38
|
structured recommendation.
|
|
@@ -41,6 +42,18 @@ It exposes three tools:
|
|
|
41
42
|
- `record_component_decision` — records what the agent actually did so
|
|
42
43
|
future recommendations in the same project can take that decision into
|
|
43
44
|
account.
|
|
45
|
+
- `read_ledger` — lists past `recommend_component` judgments for a
|
|
46
|
+
`project_id`, including any that were served from the ledger cache (see
|
|
47
|
+
[Per-project judgment ledger](#per-project-judgment-ledger)); pass
|
|
48
|
+
`feature_id` instead of browsing by keyword to get a full cost rollup for
|
|
49
|
+
one feature (see [Tool: `report_build_cost`](#tool-report_build_cost)).
|
|
50
|
+
- `report_build_cost` — self-reports the end-to-end build cost for one
|
|
51
|
+
feature, so cost incurred after Pattern's own verdict (the actual
|
|
52
|
+
scaffold/install/build) is still attributable back to it.
|
|
53
|
+
- `report_outcome_proxy` — self-reports a value signal (rework, time to
|
|
54
|
+
merge, kept-vs-replaced) for one feature, deliberately independent of
|
|
55
|
+
Pattern's own verdict -- see [Outcome
|
|
56
|
+
proxies](#outcome-proxies).
|
|
44
57
|
|
|
45
58
|
## How it works
|
|
46
59
|
|
|
@@ -70,6 +83,9 @@ A result can also be:
|
|
|
70
83
|
- `custom_build`
|
|
71
84
|
- `no_candidates_found`
|
|
72
85
|
- `skip_list`
|
|
86
|
+
- `ledger_cache_hit` — served from a recent, matching prior judgment
|
|
87
|
+
instead of a fresh search+score (see
|
|
88
|
+
[Per-project judgment ledger](#per-project-judgment-ledger)).
|
|
73
89
|
|
|
74
90
|
`no_candidates_found` is kept separate from a low-coverage result. Not
|
|
75
91
|
finding a candidate is different from finding candidates that don't cover
|
|
@@ -78,7 +94,14 @@ the requirements.
|
|
|
78
94
|
If `project_id` is supplied, Pattern also checks for past confirmed
|
|
79
95
|
decisions on that project and factors them in as a consistency signal —
|
|
80
96
|
never a rule that overrides a genuinely better match found in the current
|
|
81
|
-
search.
|
|
97
|
+
search. Separately, `project_id` also enables the judgment ledger: a
|
|
98
|
+
high-confidence prior judgment matching this exact
|
|
99
|
+
component_need/domain/framework/existing_stack, recorded recently enough,
|
|
100
|
+
can be served directly (`ledger_cache_hit`) instead of running a fresh
|
|
101
|
+
search+score. This is the one deliberate exception to "every recommendation
|
|
102
|
+
searches and scores again" — see
|
|
103
|
+
[Per-project judgment ledger](#per-project-judgment-ledger) for the exact
|
|
104
|
+
rules and why it's safe.
|
|
82
105
|
|
|
83
106
|
Every result includes `computed_at`, because coverage is a snapshot of the
|
|
84
107
|
search at that point in time, not a permanent fact. Every result also
|
|
@@ -395,6 +418,16 @@ Leave `checklist` out to keep today's default behavior: `recommend_component`
|
|
|
395
418
|
extracts its own checklist internally, exactly as before this option
|
|
396
419
|
existed.
|
|
397
420
|
|
|
421
|
+
#### `feature_id`
|
|
422
|
+
|
|
423
|
+
`feature_id` is optional -- a stable identifier for the feature this
|
|
424
|
+
component need belongs to (e.g. a ticket id or branch name). Its only use
|
|
425
|
+
is joining this call's cost with a later
|
|
426
|
+
[`report_build_cost`](#tool-report_build_cost) call for the same feature.
|
|
427
|
+
Omit it to have one derived deterministically from `project_id` +
|
|
428
|
+
`component_need`; only meaningful together with `project_id`. See
|
|
429
|
+
[Feature cost attribution](#feature-cost-attribution).
|
|
430
|
+
|
|
398
431
|
**Is the checklist actually skipped, not just re-derived?** Checked, not
|
|
399
432
|
assumed. `breakdown_ms.extract` for a `checklist`-provided call is smaller
|
|
400
433
|
than the default path's, but not near-zero -- which raised the question of
|
|
@@ -600,7 +633,8 @@ Anthropic API call.
|
|
|
600
633
|
"domain": "Airbnb-style rental marketplace",
|
|
601
634
|
"action": "custom_built",
|
|
602
635
|
"source": "custom",
|
|
603
|
-
"timestamp": "2026-08-25T14:32:00.000Z"
|
|
636
|
+
"timestamp": "2026-08-25T14:32:00.000Z",
|
|
637
|
+
"time_saved_minutes": 25
|
|
604
638
|
}
|
|
605
639
|
```
|
|
606
640
|
|
|
@@ -609,6 +643,15 @@ Anthropic API call.
|
|
|
609
643
|
- `action` must be `"installed"` or `"custom_built"`.
|
|
610
644
|
- `source` can be `"shadcn"`, `"21st.dev"`, `"reui"`, or `"custom"`.
|
|
611
645
|
- `timestamp` is optional. If omitted, Pattern uses the current time.
|
|
646
|
+
- `time_saved_minutes` is optional -- the calling agent's own estimate,
|
|
647
|
+
in minutes, of how much time this decision saved by having Pattern's
|
|
648
|
+
verdict instead of researching candidates and judging fit from scratch.
|
|
649
|
+
This is entirely self-reported. Pattern has no way to measure a
|
|
650
|
+
counterfactual ("how long would this have taken without Pattern?"), so
|
|
651
|
+
unlike `_meta` (Pattern's own real cost/latency for the call that
|
|
652
|
+
produced the verdict), this number is never computed or verified --
|
|
653
|
+
it's just recorded as-given. Omit it rather than guess a number to fill
|
|
654
|
+
the field.
|
|
612
655
|
|
|
613
656
|
### Output
|
|
614
657
|
|
|
@@ -620,6 +663,322 @@ Anthropic API call.
|
|
|
620
663
|
}
|
|
621
664
|
```
|
|
622
665
|
|
|
666
|
+
## Tool: `read_ledger`
|
|
667
|
+
|
|
668
|
+
Lists past `recommend_component` judgments for a `project_id` -- every
|
|
669
|
+
call that reached the API and produced a verdict, not just ones explicitly
|
|
670
|
+
confirmed via `record_component_decision`. Useful for auditing what
|
|
671
|
+
Pattern has already judged for a project, or for understanding why a call
|
|
672
|
+
came back with `served_from_ledger: true`.
|
|
673
|
+
|
|
674
|
+
### Input
|
|
675
|
+
|
|
676
|
+
```json
|
|
677
|
+
{
|
|
678
|
+
"project_id": "my-booking-app",
|
|
679
|
+
"component_need": "cancellation",
|
|
680
|
+
"limit": 10
|
|
681
|
+
}
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
- `project_id` is required.
|
|
685
|
+
- `component_need` is optional -- a simple keyword filter (substring
|
|
686
|
+
match, no embeddings) against stored entries' `component_need`. Omit to
|
|
687
|
+
list everything for the project.
|
|
688
|
+
- `limit` is optional, defaults to 20. Most recent entries first.
|
|
689
|
+
- `feature_id` is optional. When provided, `component_need` and `limit`
|
|
690
|
+
are ignored and the response is a full cost rollup for that one feature
|
|
691
|
+
instead of a keyword listing -- see [Feature cost
|
|
692
|
+
attribution](#feature-cost-attribution).
|
|
693
|
+
|
|
694
|
+
### Output
|
|
695
|
+
|
|
696
|
+
```json
|
|
697
|
+
{
|
|
698
|
+
"project_id": "my-booking-app",
|
|
699
|
+
"entries": [
|
|
700
|
+
{
|
|
701
|
+
"id": "a1b2c3d4-...",
|
|
702
|
+
"timestamp": "2026-08-29T19:50:47.073Z",
|
|
703
|
+
"project_id": "my-booking-app",
|
|
704
|
+
"feature_id": "3f9a21c0",
|
|
705
|
+
"component_need": "cancellation policy display with refund tiers by date",
|
|
706
|
+
"domain": "Airbnb-style rental marketplace",
|
|
707
|
+
"framework": "React + Tailwind",
|
|
708
|
+
"checklist": ["...", "..."],
|
|
709
|
+
"checklist_source": "extracted",
|
|
710
|
+
"candidates_evaluated": [
|
|
711
|
+
{ "source": "ReUI (reui.io)", "name": "Timeline", "url": "https://reui.io/components/timeline", "coverage_pct": 62.5 }
|
|
712
|
+
],
|
|
713
|
+
"verdict": "use_existing",
|
|
714
|
+
"chosen_candidate": "Timeline",
|
|
715
|
+
"confidence": "low",
|
|
716
|
+
"reason": "scored",
|
|
717
|
+
"coverage": "5/8 (62.5%)",
|
|
718
|
+
"cost_usd": 0.087,
|
|
719
|
+
"cache_hit": false,
|
|
720
|
+
"project_conventions_snapshot": "9f3a1c7e2b0d4f5a"
|
|
721
|
+
}
|
|
722
|
+
]
|
|
723
|
+
}
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
Passing `feature_id` instead returns:
|
|
727
|
+
|
|
728
|
+
```json
|
|
729
|
+
{
|
|
730
|
+
"project_id": "my-booking-app",
|
|
731
|
+
"feature_id": "3f9a21c0",
|
|
732
|
+
"verdict_entries": [ "...same shape as above, filtered to this feature_id..." ],
|
|
733
|
+
"build_records": [
|
|
734
|
+
{ "id": "...", "timestamp": "...", "project_id": "my-booking-app", "feature_id": "3f9a21c0", "tokens_used": 9000, "cost_usd": 1.25, "outcome": "shipped" }
|
|
735
|
+
],
|
|
736
|
+
"total_cost_usd": 1.34,
|
|
737
|
+
"outcome_proxy": { "time_to_merge_hours": 3.5, "reworked": true, "days_to_rework": 12, "status_at_30d": "kept" },
|
|
738
|
+
"outcome_proxy_history": [ "...every raw report_outcome_proxy record for this feature_id, oldest first..." ]
|
|
739
|
+
}
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
`outcome_proxy` is `null` (and `outcome_proxy_history` an empty array)
|
|
743
|
+
when no `report_outcome_proxy` calls have been made for this feature yet
|
|
744
|
+
-- see [Outcome proxies](#outcome-proxies).
|
|
745
|
+
|
|
746
|
+
Each entry holds only distilled fields -- `candidates_evaluated` never
|
|
747
|
+
contains raw HTML, full prop tables, or the per-requirement evidence text
|
|
748
|
+
`recommend_component` itself returns. See
|
|
749
|
+
[Data minimization](#data-minimization) below.
|
|
750
|
+
|
|
751
|
+
## Tool: `report_build_cost`
|
|
752
|
+
|
|
753
|
+
Self-reports the end-to-end build cost for one feature. Pattern only ever
|
|
754
|
+
sees the cost of judging *what* to use (`recommend_component`'s own
|
|
755
|
+
`_meta.estimated_cost_usd`); everything past that -- the actual scaffold,
|
|
756
|
+
install, or custom build -- happens outside Pattern entirely and Pattern
|
|
757
|
+
has no way to observe it. Call this once, after the calling agent's build
|
|
758
|
+
for a feature is actually complete (shipped, abandoned, or replaced), not
|
|
759
|
+
on every verdict.
|
|
760
|
+
|
|
761
|
+
### Input
|
|
762
|
+
|
|
763
|
+
```json
|
|
764
|
+
{
|
|
765
|
+
"feature_id": "3f9a21c0",
|
|
766
|
+
"project_id": "my-booking-app",
|
|
767
|
+
"tokens_used": 9000,
|
|
768
|
+
"cost_usd": 1.25,
|
|
769
|
+
"outcome": "shipped"
|
|
770
|
+
}
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
- `feature_id` is required -- either a value you explicitly passed to an
|
|
774
|
+
earlier `recommend_component` call for this feature, or (if you didn't)
|
|
775
|
+
the same value `recommend_component` derives on its own:
|
|
776
|
+
`sha256(project_id + "::" + component_need, lowercased/trimmed)`
|
|
777
|
+
truncated to 8 hex characters. When in doubt, call `read_ledger` with
|
|
778
|
+
just `project_id` and copy the `feature_id` off the relevant entry
|
|
779
|
+
rather than re-deriving it by hand.
|
|
780
|
+
- `project_id` is optional but recommended -- without it, this record
|
|
781
|
+
still joins to a `recommend_component` entry by `feature_id` alone, but
|
|
782
|
+
`read_ledger`'s rollup can't scope it to one project.
|
|
783
|
+
- `tokens_used` is optional.
|
|
784
|
+
- `cost_usd` is required -- your own real number, not Pattern's.
|
|
785
|
+
- `outcome` is required: `"shipped"`, `"abandoned"`, or
|
|
786
|
+
`"replaced_with_existing"`.
|
|
787
|
+
|
|
788
|
+
### Output
|
|
789
|
+
|
|
790
|
+
```json
|
|
791
|
+
{
|
|
792
|
+
"status": "recorded",
|
|
793
|
+
"record": {
|
|
794
|
+
"id": "c5706b47-...",
|
|
795
|
+
"timestamp": "2026-09-02T01:25:29.653Z",
|
|
796
|
+
"project_id": "my-booking-app",
|
|
797
|
+
"feature_id": "3f9a21c0",
|
|
798
|
+
"tokens_used": 9000,
|
|
799
|
+
"cost_usd": 1.25,
|
|
800
|
+
"outcome": "shipped"
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
This only appends a local record to `~/.pattern/build_ledger.jsonl`
|
|
806
|
+
(override with `PATTERN_BUILD_LEDGER_PATH`) -- it never re-runs any
|
|
807
|
+
judgment and never calls the Anthropic API.
|
|
808
|
+
|
|
809
|
+
## Tool: `report_outcome_proxy`
|
|
810
|
+
|
|
811
|
+
Self-reports a value signal for one feature, deliberately independent of
|
|
812
|
+
Pattern's own verdict -- the whole point is a signal that could
|
|
813
|
+
*contradict* the verdict, so nothing on this path ever reads
|
|
814
|
+
`coverage_pct`, `confidence`, or any other Pattern-produced field. Compute
|
|
815
|
+
`reworked`/`days_to_rework` and `time_to_merge_hours` from your own repo's
|
|
816
|
+
real git history (e.g. `git log --follow` against the files this
|
|
817
|
+
feature's build touched) -- Pattern has no `process.cwd()`/repo-path
|
|
818
|
+
concept and no filesystem access to your repo at all, so it can't compute
|
|
819
|
+
these itself. Report `status_at_30d` only once a real ~30-day-post-merge
|
|
820
|
+
horizon has actually passed.
|
|
821
|
+
|
|
822
|
+
Safe to call more than once for the same `feature_id` as more signal
|
|
823
|
+
becomes available over time -- e.g. `time_to_merge_hours` right after
|
|
824
|
+
merge, `reworked` on a later re-check, `status_at_30d` at the 30-day mark.
|
|
825
|
+
`read_ledger`'s `feature_id` rollup merges every report into one
|
|
826
|
+
latest-value-per-field view (a later report only overwrites the specific
|
|
827
|
+
fields it includes, never the others).
|
|
828
|
+
|
|
829
|
+
### Input
|
|
830
|
+
|
|
831
|
+
```json
|
|
832
|
+
{
|
|
833
|
+
"feature_id": "3f9a21c0",
|
|
834
|
+
"project_id": "my-booking-app",
|
|
835
|
+
"reworked": true,
|
|
836
|
+
"days_to_rework": 12
|
|
837
|
+
}
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
- `feature_id` is required.
|
|
841
|
+
- `project_id` is optional but recommended, same reasoning as
|
|
842
|
+
`report_build_cost`.
|
|
843
|
+
- `reworked`, `days_to_rework`, `time_to_merge_hours`, `status_at_30d` are
|
|
844
|
+
all individually optional, but **at least one is required** -- an empty
|
|
845
|
+
report is rejected rather than silently recording nothing.
|
|
846
|
+
|
|
847
|
+
### Output
|
|
848
|
+
|
|
849
|
+
```json
|
|
850
|
+
{
|
|
851
|
+
"status": "recorded",
|
|
852
|
+
"record": {
|
|
853
|
+
"id": "8a2f1e0c-...",
|
|
854
|
+
"timestamp": "2026-09-16T18:04:12.881Z",
|
|
855
|
+
"project_id": "my-booking-app",
|
|
856
|
+
"feature_id": "3f9a21c0",
|
|
857
|
+
"reworked": true,
|
|
858
|
+
"days_to_rework": 12
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
This only appends a local record to `~/.pattern/outcome_proxies.jsonl`
|
|
864
|
+
(override with `PATTERN_OUTCOME_PROXY_PATH`) -- it never calls the
|
|
865
|
+
Anthropic API.
|
|
866
|
+
|
|
867
|
+
## Feature cost attribution
|
|
868
|
+
|
|
869
|
+
Every `recommend_component` call that writes to the ledger -- a fresh
|
|
870
|
+
judgment *or* a $0 [ledger cache hit](#the-cache-hit-exception) -- now
|
|
871
|
+
carries a `feature_id`, plus its own `cost_usd` and `cache_hit`. Pair that
|
|
872
|
+
with `report_build_cost`'s build-time record and `read_ledger`'s
|
|
873
|
+
`feature_id` rollup, and total spend on a feature (judgment + build,
|
|
874
|
+
across however many calls) is queryable end to end, not just the cost of
|
|
875
|
+
one verdict call.
|
|
876
|
+
|
|
877
|
+
`feature_id` defaults to a deterministic derivation --
|
|
878
|
+
`sha256(project_id + "::" + component_need)` truncated to 8 hex chars --
|
|
879
|
+
so repeat calls for the same feature land under the same id automatically,
|
|
880
|
+
with no coordination needed between `recommend_component` and
|
|
881
|
+
`report_build_cost` calls. Pass your own `feature_id` explicitly (e.g. a
|
|
882
|
+
ticket id or branch name) if you'd rather key on something stable on your
|
|
883
|
+
own side.
|
|
884
|
+
|
|
885
|
+
## Outcome proxies
|
|
886
|
+
|
|
887
|
+
Cost data alone (`feature cost attribution` above) can't answer whether a
|
|
888
|
+
cheaper build was actually *worth it* -- comparing it against Pattern's
|
|
889
|
+
own verdict/`coverage_pct` would be circular, since that's the very thing
|
|
890
|
+
being evaluated. `report_outcome_proxy` attaches a cheap, non-circular
|
|
891
|
+
value signal per `feature_id` instead:
|
|
892
|
+
|
|
893
|
+
- **`reworked` / `days_to_rework`** (primary proxy) -- was any file this
|
|
894
|
+
feature's build touched modified again after the original merge, and if
|
|
895
|
+
so, how soon? Computed from real git history, not Pattern's own data.
|
|
896
|
+
- **`time_to_merge_hours`** (secondary proxy) -- how long the feature
|
|
897
|
+
took from first commit to merge.
|
|
898
|
+
- **`status_at_30d`** (tertiary, longer-horizon proxy) -- at a ~30-day
|
|
899
|
+
horizon, does the component Pattern recommended still exist in the
|
|
900
|
+
codebase, unchanged in kind (`"kept"`), was it swapped for a different
|
|
901
|
+
approach (`"replaced"`), or removed entirely (`"removed"`)?
|
|
902
|
+
|
|
903
|
+
`read_ledger`'s `feature_id` rollup returns both `outcome_proxy` (the
|
|
904
|
+
merged latest-value-per-field view) and `outcome_proxy_history` (every
|
|
905
|
+
raw report, in case the timeline itself matters) alongside the cost
|
|
906
|
+
figures from [Feature cost attribution](#feature-cost-attribution) above
|
|
907
|
+
-- so "what did this feature cost end to end, and did it hold up?" is
|
|
908
|
+
answerable from one `read_ledger` call.
|
|
909
|
+
|
|
910
|
+
## Per-project judgment ledger
|
|
911
|
+
|
|
912
|
+
Distinct from [per-project decision memory](#per-project-decision-memory)
|
|
913
|
+
below -- that file only gains an entry when `record_component_decision` is
|
|
914
|
+
explicitly called. The ledger instead gains one entry automatically for
|
|
915
|
+
**every** `recommend_component` call with a `project_id` that lands on
|
|
916
|
+
reason `"scored"` or `"no_candidates_found"` -- whether that's a fresh
|
|
917
|
+
call that reached the API, or a $0 [ledger cache
|
|
918
|
+
hit](#the-cache-hit-exception) served without one (`cache_hit: true`,
|
|
919
|
+
`cost_usd: 0`), so a feature's total cost still rolls up correctly even
|
|
920
|
+
once most of its later calls are free. See [Feature cost
|
|
921
|
+
attribution](#feature-cost-attribution).
|
|
922
|
+
|
|
923
|
+
Pattern stores it locally in:
|
|
924
|
+
|
|
925
|
+
```
|
|
926
|
+
~/.pattern/ledger.jsonl
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
Change the location with `PATTERN_LEDGER_PATH`. One JSON object per line
|
|
930
|
+
(append-only, JSONL).
|
|
931
|
+
|
|
932
|
+
### The cache-hit exception
|
|
933
|
+
|
|
934
|
+
Every other part of Pattern scores fresh every time (see
|
|
935
|
+
[No caching, by design](#no-caching-by-design)). The ledger is the one
|
|
936
|
+
deliberate exception: a later `recommend_component` call with a matching
|
|
937
|
+
`project_id` **can** be served directly from a prior entry, skipping
|
|
938
|
+
search+score entirely, when **all** of the following hold:
|
|
939
|
+
|
|
940
|
+
- `component_need` matches exactly (case-insensitive).
|
|
941
|
+
- `domain` and `framework` match exactly.
|
|
942
|
+
- `existing_stack` hashes to the same value as the stored entry's
|
|
943
|
+
(both omitted counts as a match).
|
|
944
|
+
- The stored entry's `confidence` is `"high"`.
|
|
945
|
+
- The stored entry's `reason` is `"scored"` or `"no_candidates_found"`.
|
|
946
|
+
- The stored entry is no older than `PATTERN_LEDGER_TTL_DAYS` (default
|
|
947
|
+
**30** days, configurable).
|
|
948
|
+
|
|
949
|
+
When served this way, the response has `reason: "ledger_cache_hit"`,
|
|
950
|
+
`served_from_ledger: true`, `ledger_entry_id`, and
|
|
951
|
+
`original_verdict_timestamp` -- so nothing is ever silently passed off as
|
|
952
|
+
freshly verified. `_meta.estimated_cost_usd` and `tokens_used` are
|
|
953
|
+
genuinely `0`: no API call happened. `requirements_checked` is `null` on
|
|
954
|
+
this path -- the ledger never stores per-requirement evidence text (see
|
|
955
|
+
[Data minimization](#data-minimization)), so a cache hit can only replay
|
|
956
|
+
the verdict/confidence/coverage/chosen-candidate, not the original
|
|
957
|
+
per-requirement reasoning.
|
|
958
|
+
|
|
959
|
+
Any mismatch on the criteria above -- a different `domain`, a changed
|
|
960
|
+
`existing_stack`, an entry that's gone stale, or one that wasn't
|
|
961
|
+
high-confidence -- falls through to a normal, fresh search+score call.
|
|
962
|
+
|
|
963
|
+
### Turning the cache-hit exception off
|
|
964
|
+
|
|
965
|
+
Set `PATTERN_NO_LEDGER_CACHE_HIT` (any truthy value) to restore
|
|
966
|
+
"every `recommend_component` call always scores fresh" without removing
|
|
967
|
+
any ledger code. This disables only the cache-hit short-circuit --
|
|
968
|
+
entries are still written to `ledger.jsonl` and `read_ledger` still works
|
|
969
|
+
either way, so the audit trail keeps growing even with the switch on.
|
|
970
|
+
Unset the variable to re-enable cache hits again at any time.
|
|
971
|
+
|
|
972
|
+
### Data minimization
|
|
973
|
+
|
|
974
|
+
Nothing written to the ledger ever contains raw search/fetch content.
|
|
975
|
+
Every candidate is reduced to exactly four fields before it's written --
|
|
976
|
+
`source`, `name`, `url`, `coverage_pct` -- enforced at the type level
|
|
977
|
+
(`assertDistilledCandidateShape` in `src/index.ts`), not just by
|
|
978
|
+
convention: a raw or extended object throws rather than silently
|
|
979
|
+
persisting. Run `node scripts/verify-ledger-boundary.mjs` (after
|
|
980
|
+
`npm run build`) to check this boundary directly.
|
|
981
|
+
|
|
623
982
|
## Per-project decision memory
|
|
624
983
|
|
|
625
984
|
Pattern stores confirmed decisions locally in:
|
|
@@ -644,12 +1003,16 @@ The file is organized by project:
|
|
|
644
1003
|
"domain": "Airbnb-style rental marketplace",
|
|
645
1004
|
"action": "custom_built",
|
|
646
1005
|
"source": "custom",
|
|
647
|
-
"timestamp": "2026-08-25T14:32:00.000Z"
|
|
1006
|
+
"timestamp": "2026-08-25T14:32:00.000Z",
|
|
1007
|
+
"time_saved_minutes": 25
|
|
648
1008
|
}
|
|
649
1009
|
]
|
|
650
1010
|
}
|
|
651
1011
|
```
|
|
652
1012
|
|
|
1013
|
+
`time_saved_minutes` is omitted from an entry entirely when the calling
|
|
1014
|
+
agent didn't provide one -- it's never backfilled or estimated by Pattern.
|
|
1015
|
+
|
|
653
1016
|
Each project keeps its 50 most recent decisions. Older entries are
|
|
654
1017
|
removed as new ones are added.
|
|
655
1018
|
|
|
@@ -668,12 +1031,13 @@ sensitive information in them. See [SECURITY.md](./SECURITY.md).
|
|
|
668
1031
|
A failure to write the decision file is returned as an error from
|
|
669
1032
|
`record_component_decision`.
|
|
670
1033
|
|
|
671
|
-
**No caching, by design.** Project memory
|
|
672
|
-
A previous decision is only additional context
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
1034
|
+
**No caching, by design.** Project memory (this file, `memory.json`) does
|
|
1035
|
+
not cache recommendations. A previous decision is only additional context
|
|
1036
|
+
for a new judgment. This is unrelated to the
|
|
1037
|
+
[judgment ledger](#per-project-judgment-ledger)'s bounded cache-hit
|
|
1038
|
+
exception, which lives in a separate file (`ledger.jsonl`) and is always
|
|
1039
|
+
flagged (`served_from_ledger: true`) when it happens — see
|
|
1040
|
+
[Known limitations](#known-limitations) for more.
|
|
677
1041
|
|
|
678
1042
|
## Security and privacy
|
|
679
1043
|
|
|
@@ -683,28 +1047,103 @@ recommendations.
|
|
|
683
1047
|
Local project memory and the local call log are stored on the machine
|
|
684
1048
|
running Pattern. They are not sent anywhere by Pattern itself.
|
|
685
1049
|
|
|
1050
|
+
The one exception is opt-in telemetry, off by default -- see
|
|
1051
|
+
[Telemetry](#telemetry) below for exactly what it sends and how to turn
|
|
1052
|
+
it on or off.
|
|
1053
|
+
|
|
686
1054
|
Review [SECURITY.md](./SECURITY.md) before putting sensitive information
|
|
687
1055
|
into fields such as `component_need`, `domain`, or project IDs.
|
|
688
1056
|
|
|
1057
|
+
## Telemetry
|
|
1058
|
+
|
|
1059
|
+
Off by default. Nothing is sent anywhere for telemetry purposes unless
|
|
1060
|
+
you explicitly set:
|
|
1061
|
+
|
|
1062
|
+
```
|
|
1063
|
+
PATTERN_TELEMETRY=1
|
|
1064
|
+
```
|
|
1065
|
+
|
|
1066
|
+
**The one-time notice.** The first time you run this version of Pattern
|
|
1067
|
+
-- whether it's a brand-new install or an upgrade from a version before
|
|
1068
|
+
telemetry existed -- it prints a short notice to stderr explaining all of
|
|
1069
|
+
this and how to opt in. It prints exactly once, ever (tracked by a marker
|
|
1070
|
+
file at `~/.pattern/telemetry_notice_shown`), then never again, regardless
|
|
1071
|
+
of whether you act on it. There's no interactive y/n prompt: Pattern's
|
|
1072
|
+
stdin is the MCP JSON-RPC channel the client uses to talk to it, so
|
|
1073
|
+
blocking on stdin for a keypress would fight the protocol handshake
|
|
1074
|
+
instead of showing a dialog -- a stderr notice is the safe equivalent for
|
|
1075
|
+
a stdio MCP server.
|
|
1076
|
+
|
|
1077
|
+
**Why it exists.** Two things about real usage can't be answered from
|
|
1078
|
+
this repo alone: whether people actually come back and use Pattern on a
|
|
1079
|
+
second or third project on their own, and how often a BYO Anthropic key
|
|
1080
|
+
actually hits a rate limit or runs out of credit in real sessions, not
|
|
1081
|
+
just the one time that happened during manual testing (see
|
|
1082
|
+
[Known limitations](#known-limitations)). Telemetry answers both without
|
|
1083
|
+
requiring anyone to fill out a survey.
|
|
1084
|
+
|
|
1085
|
+
**What gets sent, when enabled:**
|
|
1086
|
+
|
|
1087
|
+
- An anonymous, randomly generated install ID -- a UUID created once and
|
|
1088
|
+
stored at `~/.pattern/install_id` (overridable via
|
|
1089
|
+
`PATTERN_INSTALL_ID_PATH`), never derived from your machine, username,
|
|
1090
|
+
or any other identifying information. This is the only thing that ties
|
|
1091
|
+
two events together as "the same install."
|
|
1092
|
+
- A one-way SHA-256 hash of `project_id`, truncated to 16 hex characters
|
|
1093
|
+
-- never the raw `project_id` string. The hash lets Pattern count how
|
|
1094
|
+
many *distinct* projects one install has used, without ever seeing what
|
|
1095
|
+
those projects are named.
|
|
1096
|
+
- On every `recommend_component` call that reaches the API or the ledger
|
|
1097
|
+
cache-hit shortcut: `verdict`, `confidence`, `reason`,
|
|
1098
|
+
`ensemble_triggered`, `estimated_cost_usd`, and `served_from_ledger` --
|
|
1099
|
+
the same distilled shape already written to the
|
|
1100
|
+
[local call log](#local-call-log), not new information.
|
|
1101
|
+
- On a failed Anthropic API call specifically: the HTTP status code and a
|
|
1102
|
+
coarse classification (`rate_limit`, `insufficient_credit`, or `other`)
|
|
1103
|
+
-- never the request or response body.
|
|
1104
|
+
|
|
1105
|
+
**What never gets sent, telemetry on or off:** `component_need`,
|
|
1106
|
+
`domain`, `framework`, `existing_stack`, `requirements_checked` evidence,
|
|
1107
|
+
the raw `project_id`, or your Anthropic API key.
|
|
1108
|
+
|
|
1109
|
+
**Where it goes.** Events go to Pattern's PostHog project via its public,
|
|
1110
|
+
write-only project key (safe to ship in source -- it can send events, it
|
|
1111
|
+
cannot read data back). Set `PATTERN_POSTHOG_KEY` /
|
|
1112
|
+
`PATTERN_POSTHOG_HOST` to point at a different project, e.g. for
|
|
1113
|
+
self-hosting.
|
|
1114
|
+
|
|
1115
|
+
**Turning it off** is the default -- just don't set `PATTERN_TELEMETRY`.
|
|
1116
|
+
If you'd previously enabled it, unset the variable (or set it to `0`) to
|
|
1117
|
+
go back to fully local.
|
|
1118
|
+
|
|
689
1119
|
## Cost
|
|
690
1120
|
|
|
691
1121
|
Pattern uses the Anthropic API, so `recommend_component` has a cost.
|
|
692
1122
|
|
|
693
1123
|
A typical single pass costs about $0.06–$0.10 with Sonnet 5 at current
|
|
694
1124
|
pricing. Skip-listed primitives cost $0 because they're handled locally
|
|
695
|
-
and never reach the API.
|
|
1125
|
+
and never reach the API. A [ledger cache hit](#the-cache-hit-exception)
|
|
1126
|
+
also costs $0, for the same reason -- no API call happens.
|
|
696
1127
|
|
|
697
1128
|
### The `_meta` field
|
|
698
1129
|
|
|
699
1130
|
Every `recommend_component` and `extract_requirements` response includes
|
|
700
|
-
an internal `_meta` block reporting what that call actually spent
|
|
1131
|
+
an internal `_meta` block reporting what that call actually spent. This
|
|
1132
|
+
is not shown to the user automatically -- the calling agent has to
|
|
1133
|
+
surface it, the same way it's separately instructed to show
|
|
1134
|
+
`install_command` before running it (see
|
|
1135
|
+
[above](#installation-commands-are-not-trusted)). Both tool descriptions
|
|
1136
|
+
say so explicitly: surface `_meta.estimated_cost_usd` after the call,
|
|
1137
|
+
since it's real spend against the user's own API key, not internal
|
|
1138
|
+
bookkeeping.
|
|
701
1139
|
|
|
702
1140
|
```json
|
|
703
1141
|
{
|
|
704
1142
|
"total_ms": 41516,
|
|
705
1143
|
"breakdown_ms": { "extract": 5006, "search": 3114, "score": 33396 },
|
|
706
1144
|
"tokens_used": { "input": 8400, "output": 620 },
|
|
707
|
-
"estimated_cost_usd": 0.14
|
|
1145
|
+
"estimated_cost_usd": 0.14,
|
|
1146
|
+
"scoring_fetch": { "attempted": true, "succeeded": true, "url": "https://ui.shadcn.com/docs/components/..." }
|
|
708
1147
|
}
|
|
709
1148
|
```
|
|
710
1149
|
|
|
@@ -719,6 +1158,14 @@ an internal `_meta` block reporting what that call actually spent:
|
|
|
719
1158
|
discounts.
|
|
720
1159
|
- `breakdown_ms` -- how `total_ms` splits across `recommend_component`'s
|
|
721
1160
|
three internal phases.
|
|
1161
|
+
- `scoring_fetch` -- whether step 4's single candidate-verification fetch
|
|
1162
|
+
(see [Fetch-grounded scoring](#fetch-grounded-scoring-and-reference-verification)
|
|
1163
|
+
below) actually happened for this response. `url` is `null` when
|
|
1164
|
+
`attempted` is `false` (no real candidate to verify, e.g. `reason:
|
|
1165
|
+
"no_candidates_found"` or `"skip_list"`). This is a diagnostic only --
|
|
1166
|
+
Pattern never uses it to auto-correct `requirements_checked` after the
|
|
1167
|
+
fact, since there's no safe fallback value for an unverified met/not-met
|
|
1168
|
+
call the way there is for a reference URL.
|
|
722
1169
|
|
|
723
1170
|
**How `breakdown_ms` is measured, and its one real caveat.** The bundled
|
|
724
1171
|
call runs extraction, search, and scoring inside a single model turn
|
|
@@ -746,6 +1193,10 @@ not the wall-clock time you waited. The three ensemble passes run with the
|
|
|
746
1193
|
2nd and 3rd concurrent, so perceived latency is closer to ~2x one pass,
|
|
747
1194
|
not the ~3x `total_ms` will show. Cost and token spend are genuinely
|
|
748
1195
|
additive across reruns, which is what `_meta` is reporting there.
|
|
1196
|
+
`scoring_fetch` is the one exception -- it isn't summed (a fetch either
|
|
1197
|
+
happened for the specific pass whose evidence became the returned
|
|
1198
|
+
`requirements_checked`, or it didn't), so it reports that winning pass's
|
|
1199
|
+
own value, not an aggregate across all three.
|
|
749
1200
|
|
|
750
1201
|
Three things help keep the cost down without changing the decision process.
|
|
751
1202
|
|
|
@@ -770,18 +1221,33 @@ shadcn/ui, 21st.dev, and ReUI are searched in the same turn rather than
|
|
|
770
1221
|
sequentially, which reduces how much conversation context needs to be
|
|
771
1222
|
sent repeatedly.
|
|
772
1223
|
|
|
773
|
-
###
|
|
774
|
-
|
|
775
|
-
Pattern allows up to
|
|
776
|
-
|
|
1224
|
+
### Fetch-grounded scoring and reference verification
|
|
1225
|
+
|
|
1226
|
+
Pattern allows up to 3 `web_fetch` calls per pass: 1 reserved for scoring,
|
|
1227
|
+
2 reserved for reference verification (1 for Mobbin, 1 for Figma
|
|
1228
|
+
Community).
|
|
1229
|
+
|
|
1230
|
+
Before finalizing coverage, Pattern fetches the best-fitting candidate's
|
|
1231
|
+
own real docs/source page once and re-checks the checklist against that
|
|
1232
|
+
page, not just the search-result snippet it started with. This exists
|
|
1233
|
+
because search-result descriptions can both overstate a component's real
|
|
1234
|
+
capabilities and miss real ones it actually has -- both were observed in
|
|
1235
|
+
testing on the same case (an invented feature claim and a missed real
|
|
1236
|
+
one). If the fetch fails, or there's no confirmed URL to fetch, Pattern
|
|
1237
|
+
falls back to search-only evidence and says so in the affected items.
|
|
1238
|
+
|
|
1239
|
+
Each result's `_meta.scoring_fetch` reports whether this fetch actually
|
|
1240
|
+
happened for that response (`{ attempted, succeeded, url }`) -- it's a
|
|
1241
|
+
diagnostic, not something Pattern uses to auto-correct individual
|
|
1242
|
+
requirement judgments. Unlike a reference URL (which has a safe fallback:
|
|
1243
|
+
the category page), there's no safe fallback for an unverified met/not-met
|
|
1244
|
+
call, so nothing is silently corrected -- `scoring_fetch` just tells you
|
|
1245
|
+
whether the grounding actually ran.
|
|
777
1246
|
|
|
778
1247
|
A fetch can read up to 15,000 content tokens. `web_fetch` has no separate
|
|
779
1248
|
per-call fee; the cost comes from the content added to the model's
|
|
780
1249
|
context.
|
|
781
1250
|
|
|
782
|
-
Pattern does not use `web_fetch` during requirement scoring. It's
|
|
783
|
-
reserved for verifying reference links.
|
|
784
|
-
|
|
785
1251
|
### Choosing a cheaper model
|
|
786
1252
|
|
|
787
1253
|
You can change the model with:
|
|
@@ -1047,22 +1513,32 @@ pipeline is still fully bundled; nothing about this evaluation changed.
|
|
|
1047
1513
|
|
|
1048
1514
|
### No caching, by design
|
|
1049
1515
|
|
|
1050
|
-
Every recommendation searches and scores again
|
|
1516
|
+
Every recommendation searches and scores again -- with one bounded
|
|
1517
|
+
exception (see below).
|
|
1051
1518
|
|
|
1052
1519
|
This means a recommendation can change as component libraries change.
|
|
1053
1520
|
For example, a later shadcn/ui release can introduce a component that
|
|
1054
1521
|
changes a previous `custom_build` result.
|
|
1055
1522
|
|
|
1056
|
-
Do not
|
|
1057
|
-
calling-agent layer.
|
|
1058
|
-
|
|
1059
|
-
If you add caching, keep it session-scoped.
|
|
1523
|
+
Do not build a second, unbounded cache of recommendations at the
|
|
1524
|
+
calling-agent layer on top of Pattern's own. If you add caching there,
|
|
1525
|
+
keep it session-scoped.
|
|
1060
1526
|
|
|
1061
1527
|
[Project decision memory](#per-project-decision-memory) does not change
|
|
1062
1528
|
this. It provides context from previous decisions, but every
|
|
1063
1529
|
`recommend_component` call still performs a fresh search and scoring
|
|
1064
1530
|
pass.
|
|
1065
1531
|
|
|
1532
|
+
The one deliberate exception is the
|
|
1533
|
+
[judgment ledger's cache-hit path](#the-cache-hit-exception): a later
|
|
1534
|
+
call matching an exact, recent, high-confidence prior judgment can be
|
|
1535
|
+
served without a fresh search+score. It's bounded (exact
|
|
1536
|
+
component_need/domain/framework/conventions match, a staleness TTL) and
|
|
1537
|
+
always self-identifies via `served_from_ledger: true` and
|
|
1538
|
+
`reason: "ledger_cache_hit"` -- so a calling agent that wants a guaranteed
|
|
1539
|
+
fresh check on every call should look for that flag and treat it the same
|
|
1540
|
+
as any other verdict it wants to double-check.
|
|
1541
|
+
|
|
1066
1542
|
### The skip-list is still evolving
|
|
1067
1543
|
|
|
1068
1544
|
The primitive skip-list is a starting point and has not yet been
|