pattern-mcp 0.4.0 → 0.6.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 +250 -6
- package/dist/index.js +362 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ whether to:
|
|
|
32
32
|
|
|
33
33
|
Pattern is designed for agents to use **while they are building**.
|
|
34
34
|
|
|
35
|
-
It exposes
|
|
35
|
+
It exposes eight tools:
|
|
36
36
|
|
|
37
37
|
- `recommend_component` — evaluates a UI component need and returns a
|
|
38
38
|
structured recommendation.
|
|
@@ -54,6 +54,13 @@ It exposes six tools:
|
|
|
54
54
|
merge, kept-vs-replaced) for one feature, deliberately independent of
|
|
55
55
|
Pattern's own verdict -- see [Outcome
|
|
56
56
|
proxies](#outcome-proxies).
|
|
57
|
+
- `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).
|
|
60
|
+
- `export_ledger_provenance` — formats one ledger entry as a stable
|
|
61
|
+
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).
|
|
57
64
|
|
|
58
65
|
## How it works
|
|
59
66
|
|
|
@@ -428,6 +435,17 @@ Omit it to have one derived deterministically from `project_id` +
|
|
|
428
435
|
`component_need`; only meaningful together with `project_id`. See
|
|
429
436
|
[Feature cost attribution](#feature-cost-attribution).
|
|
430
437
|
|
|
438
|
+
#### `file_path`
|
|
439
|
+
|
|
440
|
+
`file_path` is optional -- path (relative to `PROJECT_ROOT`) where this
|
|
441
|
+
component decision is expected to be implemented, if already known.
|
|
442
|
+
Usually not known yet at call time, since the decision typically precedes
|
|
443
|
+
the file existing. When provided, it's stored on the resulting ledger
|
|
444
|
+
entry and [`check_ledger_liveness`](#tool-check_ledger_liveness) can later
|
|
445
|
+
confirm the file still exists and still references `chosen_candidate`. It
|
|
446
|
+
cannot currently be attached to an entry after the fact -- see [Ledger
|
|
447
|
+
integrity and decision provenance](#ledger-integrity-and-decision-provenance).
|
|
448
|
+
|
|
431
449
|
**Is the checklist actually skipped, not just re-derived?** Checked, not
|
|
432
450
|
assumed. `breakdown_ms.extract` for a `checklist`-provided call is smaller
|
|
433
451
|
than the default path's, but not near-zero -- which raised the question of
|
|
@@ -717,12 +735,24 @@ came back with `served_from_ledger: true`.
|
|
|
717
735
|
"coverage": "5/8 (62.5%)",
|
|
718
736
|
"cost_usd": 0.087,
|
|
719
737
|
"cache_hit": false,
|
|
720
|
-
"project_conventions_snapshot": "9f3a1c7e2b0d4f5a"
|
|
738
|
+
"project_conventions_snapshot": "9f3a1c7e2b0d4f5a",
|
|
739
|
+
"file_path": null,
|
|
740
|
+
"snapshot_ref": "a1b2c3d4e5f6...",
|
|
741
|
+
"last_verified_live": null,
|
|
742
|
+
"live_status": "unknown"
|
|
721
743
|
}
|
|
722
744
|
]
|
|
723
745
|
}
|
|
724
746
|
```
|
|
725
747
|
|
|
748
|
+
`file_path`/`snapshot_ref`/`last_verified_live`/`live_status` are the
|
|
749
|
+
ledger integrity + decision provenance fields -- see [Ledger integrity and
|
|
750
|
+
decision provenance](#ledger-integrity-and-decision-provenance) and [Tool:
|
|
751
|
+
`check_ledger_liveness`](#tool-check_ledger_liveness). Entries written
|
|
752
|
+
before this feature shipped read back with `file_path`/`snapshot_ref`/
|
|
753
|
+
`last_verified_live` as `null` and `live_status` as `"unknown"` rather
|
|
754
|
+
than missing keys.
|
|
755
|
+
|
|
726
756
|
Passing `feature_id` instead returns:
|
|
727
757
|
|
|
728
758
|
```json
|
|
@@ -814,10 +844,12 @@ Pattern's own verdict -- the whole point is a signal that could
|
|
|
814
844
|
`coverage_pct`, `confidence`, or any other Pattern-produced field. Compute
|
|
815
845
|
`reworked`/`days_to_rework` and `time_to_merge_hours` from your own repo's
|
|
816
846
|
real git history (e.g. `git log --follow` against the files this
|
|
817
|
-
feature's build touched)
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
847
|
+
feature's build touched) rather than relying on Pattern -- rework rate and
|
|
848
|
+
time-to-merge need real git *history*, a materially bigger surface than
|
|
849
|
+
the one narrow, read-only exception described in [Ledger integrity and
|
|
850
|
+
decision provenance](#ledger-integrity-and-decision-provenance) below.
|
|
851
|
+
Report `status_at_30d` only once a real ~30-day-post-merge horizon has
|
|
852
|
+
actually passed.
|
|
821
853
|
|
|
822
854
|
Safe to call more than once for the same `feature_id` as more signal
|
|
823
855
|
becomes available over time -- e.g. `time_to_merge_hours` right after
|
|
@@ -864,6 +896,122 @@ This only appends a local record to `~/.pattern/outcome_proxies.jsonl`
|
|
|
864
896
|
(override with `PATTERN_OUTCOME_PROXY_PATH`) -- it never calls the
|
|
865
897
|
Anthropic API.
|
|
866
898
|
|
|
899
|
+
## Tool: `check_ledger_liveness`
|
|
900
|
+
|
|
901
|
+
Checks whether ledger entries for a `project_id` are still **live** --
|
|
902
|
+
does the `file_path` recorded on the entry (if any, see
|
|
903
|
+
[`file_path`](#tool-recommend_component)) still exist, and does it still
|
|
904
|
+
mention `chosen_candidate`. See [Ledger integrity and decision
|
|
905
|
+
provenance](#ledger-integrity-and-decision-provenance) for the full design
|
|
906
|
+
and its deliberate limits.
|
|
907
|
+
|
|
908
|
+
This is the **one exception** to Pattern otherwise having no filesystem
|
|
909
|
+
access to your repo (see [Outcome proxies](#outcome-proxies) above) --
|
|
910
|
+
scoped narrowly to read-only `fs.existsSync`/file-read calls against
|
|
911
|
+
`PROJECT_ROOT` (defaults to this server's own working directory; override
|
|
912
|
+
with `PATTERN_PROJECT_ROOT`). It never writes to your repo and never runs
|
|
913
|
+
an arbitrary shell command.
|
|
914
|
+
|
|
915
|
+
### Input
|
|
916
|
+
|
|
917
|
+
```json
|
|
918
|
+
{
|
|
919
|
+
"project_id": "my-booking-app",
|
|
920
|
+
"ledger_entry_id": "a1b2c3d4-..."
|
|
921
|
+
}
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
- `project_id` is required.
|
|
925
|
+
- `ledger_entry_id` is optional -- check just that one entry instead of
|
|
926
|
+
every entry for `project_id` that has a `file_path` set.
|
|
927
|
+
|
|
928
|
+
### Output
|
|
929
|
+
|
|
930
|
+
```json
|
|
931
|
+
{
|
|
932
|
+
"project_id": "my-booking-app",
|
|
933
|
+
"checked": 1,
|
|
934
|
+
"total_entries": 2,
|
|
935
|
+
"results": [
|
|
936
|
+
{
|
|
937
|
+
"ledger_entry_id": "a1b2c3d4-...",
|
|
938
|
+
"component_need": "cancellation policy display with refund tiers by date",
|
|
939
|
+
"file_path": "src/components/CancellationPolicy.tsx",
|
|
940
|
+
"live_status": "live",
|
|
941
|
+
"checked_at": "2026-09-02T20:11:03.442Z",
|
|
942
|
+
"note": null
|
|
943
|
+
},
|
|
944
|
+
{
|
|
945
|
+
"ledger_entry_id": "e5f6a7b8-...",
|
|
946
|
+
"component_need": "gallery",
|
|
947
|
+
"file_path": null,
|
|
948
|
+
"live_status": "unknown",
|
|
949
|
+
"checked_at": null,
|
|
950
|
+
"note": "no file_path recorded on this entry -- nothing to check"
|
|
951
|
+
}
|
|
952
|
+
]
|
|
953
|
+
}
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
`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"`.
|
|
959
|
+
Entries with no `file_path` are listed but never checked or written to
|
|
960
|
+
`ledger_liveness.jsonl` -- their status is permanently `"unknown"` since
|
|
961
|
+
there's nothing to check. Results here are also layered onto
|
|
962
|
+
`read_ledger`'s `live_status`/`last_verified_live` fields for the same
|
|
963
|
+
entries afterward -- `check_ledger_liveness` is the only thing that
|
|
964
|
+
advances those fields past their write-time defaults.
|
|
965
|
+
|
|
966
|
+
## Tool: `export_ledger_provenance`
|
|
967
|
+
|
|
968
|
+
Formats one ledger entry -- requirements checklist, candidates compared,
|
|
969
|
+
verdict, confidence, `snapshot_ref` -- as a single markdown block: a
|
|
970
|
+
stable, portable record of that decision you can paste into a PR
|
|
971
|
+
description or issue by hand. See [Ledger integrity and decision
|
|
972
|
+
provenance](#ledger-integrity-and-decision-provenance) for the full
|
|
973
|
+
design and its deliberate limits.
|
|
974
|
+
|
|
975
|
+
Pure and deterministic: the same entry always produces byte-identical
|
|
976
|
+
markdown, since the function reads nothing but its input (no live system
|
|
977
|
+
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.
|
|
980
|
+
|
|
981
|
+
### Input
|
|
982
|
+
|
|
983
|
+
```json
|
|
984
|
+
{
|
|
985
|
+
"project_id": "my-booking-app",
|
|
986
|
+
"ledger_entry_id": "a1b2c3d4-..."
|
|
987
|
+
}
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
Both fields are required -- unlike `check_ledger_liveness`, there's no
|
|
991
|
+
"every entry for this project" mode, since a provenance artifact is
|
|
992
|
+
inherently about one specific decision.
|
|
993
|
+
|
|
994
|
+
### Output
|
|
995
|
+
|
|
996
|
+
```json
|
|
997
|
+
{
|
|
998
|
+
"ledger_entry_id": "a1b2c3d4-...",
|
|
999
|
+
"markdown": "## Pattern decision: cancellation policy display with refund tiers by date\n\n- **Verdict:** use_existing (confidence: high)\n- **Reason:** scored\n- **Coverage:** 5/8 (62.5%)\n- **Domain:** Airbnb-style rental marketplace\n- **Framework:** React + Tailwind\n- **Snapshot:** `9f3a1c7e2b0d4f5a6b7c8d9e0f1a2b3c4d5e6f70`\n- **Judged at:** 2026-08-29T19:50:47.073Z\n\n### Requirements checked\n- ...\n\n### Candidates compared\n| Source | Name | Coverage | Chosen |\n| --- | --- | --- | --- |\n| ReUI (reui.io) | Timeline | 62.5 | ✓ |\n\n_Generated by Pattern (`export_ledger_provenance`) from ledger entry `a1b2c3d4-...`._"
|
|
1000
|
+
}
|
|
1001
|
+
```
|
|
1002
|
+
|
|
1003
|
+
Errors (as `isError: true`, not a thrown exception) when `ledger_entry_id`
|
|
1004
|
+
doesn't match any entry for that `project_id` -- including when the id is
|
|
1005
|
+
real but belongs to a different project, since entries are always scoped
|
|
1006
|
+
per `project_id`.
|
|
1007
|
+
|
|
1008
|
+
For a `custom_build` verdict, the candidates section explains that gap in
|
|
1009
|
+
prose instead of an empty table -- Pattern doesn't persist the
|
|
1010
|
+
custom-build reference (Mobbin/Figma) to the ledger (see
|
|
1011
|
+
[`distillCandidate`](#data-minimization)), so it can't reproduce it here.
|
|
1012
|
+
A `null` `snapshot_ref` (project root wasn't a git repository at judgment
|
|
1013
|
+
time) renders as prose too, not the literal word `null`.
|
|
1014
|
+
|
|
867
1015
|
## Feature cost attribution
|
|
868
1016
|
|
|
869
1017
|
Every `recommend_component` call that writes to the ledger -- a fresh
|
|
@@ -979,6 +1127,102 @@ convention: a raw or extended object throws rather than silently
|
|
|
979
1127
|
persisting. Run `node scripts/verify-ledger-boundary.mjs` (after
|
|
980
1128
|
`npm run build`) to check this boundary directly.
|
|
981
1129
|
|
|
1130
|
+
## Ledger integrity and decision provenance
|
|
1131
|
+
|
|
1132
|
+
Two gaps in the ledger, surfaced from user feedback: it tracks that a
|
|
1133
|
+
decision was made, but not whether the thing it decided about is still
|
|
1134
|
+
live in your codebase, and it stores the checklist/verdict but not a
|
|
1135
|
+
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.
|
|
1149
|
+
- `fs.existsSync` plus a plain-text read of one file, only for a
|
|
1150
|
+
`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.
|
|
1155
|
+
|
|
1156
|
+
### `PROJECT_ROOT`
|
|
1157
|
+
|
|
1158
|
+
Defaults to `process.cwd()` -- for a locally-run stdio MCP server, that's
|
|
1159
|
+
normally the consuming repo's root, since MCP hosts typically launch the
|
|
1160
|
+
server with the project directory as its working directory. Override with
|
|
1161
|
+
`PATTERN_PROJECT_ROOT` if that assumption doesn't hold for your setup.
|
|
1162
|
+
|
|
1163
|
+
A `file_path` that's absolute or escapes `PROJECT_ROOT` via `../` resolves
|
|
1164
|
+
to `live_status: "unknown"` rather than being read -- belt-and-suspenders,
|
|
1165
|
+
since the calling agent already has real filesystem access to its own
|
|
1166
|
+
machine regardless.
|
|
1167
|
+
|
|
1168
|
+
### Decision provenance: `snapshot_ref`
|
|
1169
|
+
|
|
1170
|
+
Every ledger entry -- fresh judgment or [ledger cache
|
|
1171
|
+
hit](#the-cache-hit-exception) -- now carries `snapshot_ref`: the commit
|
|
1172
|
+
SHA of `PROJECT_ROOT` at the moment that line was written, or `null` when
|
|
1173
|
+
`PROJECT_ROOT` isn't a git repo (or `git` isn't installed, or the call
|
|
1174
|
+
times out) -- this never fails the underlying `recommend_component` call.
|
|
1175
|
+
Entries written before this shipped read back with `snapshot_ref: null`.
|
|
1176
|
+
|
|
1177
|
+
A cache-hit entry's `snapshot_ref` reflects the codebase state *when that
|
|
1178
|
+
cache-hit line was written*, not the original judgment's -- to see the
|
|
1179
|
+
original judgment's snapshot, look up the entry named in its
|
|
1180
|
+
`ledger_entry_id`/`original_verdict_timestamp` fields instead.
|
|
1181
|
+
|
|
1182
|
+
[`export_ledger_provenance`](#tool-export_ledger_provenance) packages one
|
|
1183
|
+
entry's full record -- checklist, candidates, verdict, `snapshot_ref` --
|
|
1184
|
+
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`.
|
|
1190
|
+
|
|
1191
|
+
### Referential integrity: `file_path` / `live_status`
|
|
1192
|
+
|
|
1193
|
+
`recommend_component` optionally accepts `file_path` (see [Tool:
|
|
1194
|
+
`recommend_component`](#tool-recommend_component)) -- usually not known at
|
|
1195
|
+
call time, since the decision typically precedes the file existing. When
|
|
1196
|
+
set, [`check_ledger_liveness`](#tool-check_ledger_liveness) can later
|
|
1197
|
+
check whether that file still exists and still mentions
|
|
1198
|
+
`chosen_candidate`:
|
|
1199
|
+
|
|
1200
|
+
- **`live`** -- the file exists and mentions `chosen_candidate`.
|
|
1201
|
+
- **`orphaned`** -- `file_path` is set but the file no longer exists.
|
|
1202
|
+
- **`unknown`** -- no `file_path` was ever recorded, the path escapes
|
|
1203
|
+
`PROJECT_ROOT`, or the file exists but `chosen_candidate` can't be
|
|
1204
|
+
confirmed in it. Deliberately the default outcome for anything
|
|
1205
|
+
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
|
|
1219
|
+
`outcome_proxies.jsonl`, see [Outcome proxies](#outcome-proxies)) and
|
|
1220
|
+
layered onto `ledger.jsonl`'s own entries at read time -- the ledger line
|
|
1221
|
+
itself is never rewritten.
|
|
1222
|
+
|
|
1223
|
+
Not yet built (P2-P3 of Feature 1): a scheduled/batch sweep across an
|
|
1224
|
+
entire ledger, and dangling-cluster detection.
|
|
1225
|
+
|
|
982
1226
|
## Per-project decision memory
|
|
983
1227
|
|
|
984
1228
|
Pattern stores confirmed decisions locally in:
|
package/dist/index.js
CHANGED
|
@@ -30,10 +30,11 @@
|
|
|
30
30
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
31
31
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
32
32
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
33
|
+
import { execFileSync } from "node:child_process";
|
|
33
34
|
import { createHash, randomUUID } from "node:crypto";
|
|
34
|
-
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
35
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
35
36
|
import { homedir } from "node:os";
|
|
36
|
-
import { dirname, join } from "node:path";
|
|
37
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
37
38
|
import { captureApiError, captureRecommendation, printTelemetryNoticeOnce, shutdownTelemetry, } from "./telemetry.js";
|
|
38
39
|
export const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
|
39
40
|
// Configurable so Sonnet vs. Haiku can be A/B tested without a code change.
|
|
@@ -129,6 +130,64 @@ const MAX_DECISIONS_PER_PROJECT = 50;
|
|
|
129
130
|
// call is about, only the caller-supplied project_id string.
|
|
130
131
|
const LEDGER_PATH = process.env.PATTERN_LEDGER_PATH ?? join(homedir(), ".pattern", "ledger.jsonl");
|
|
131
132
|
const LEDGER_TTL_DAYS = Number(process.env.PATTERN_LEDGER_TTL_DAYS ?? 30);
|
|
133
|
+
// Ledger integrity + decision provenance
|
|
134
|
+
// (pattern-ledger-integrity-and-provenance-spec.md). This deliberately
|
|
135
|
+
// reverses the principle stated above report_outcome_proxy elsewhere in
|
|
136
|
+
// this file ("Pattern has no process.cwd()/repo-path concept and no
|
|
137
|
+
// filesystem access to a caller's repo at all") -- but narrowly: the only
|
|
138
|
+
// two things this grants are (1) checking whether one caller-supplied
|
|
139
|
+
// file_path still exists / still mentions a chosen_candidate
|
|
140
|
+
// (checkFileLiveStatus) and (2) reading the current commit SHA via
|
|
141
|
+
// `git rev-parse HEAD` (computeSnapshotRef). Both are read-only, both are
|
|
142
|
+
// scoped to PROJECT_ROOT (see resolveWithinRoot's traversal guard), and
|
|
143
|
+
// neither ever runs an arbitrary shell command. report_build_cost/
|
|
144
|
+
// report_outcome_proxy remain self-reported by design -- rework rate and
|
|
145
|
+
// time-to-merge need real git *history*, a materially bigger and more
|
|
146
|
+
// failure-prone surface than "does this one file exist right now" or
|
|
147
|
+
// "what commit is HEAD."
|
|
148
|
+
//
|
|
149
|
+
// Defaults to process.cwd() -- for a locally-run stdio MCP server, that's
|
|
150
|
+
// normally the consuming repo's root, since MCP hosts typically launch
|
|
151
|
+
// the server with the project directory as its working directory. When
|
|
152
|
+
// that assumption doesn't hold (or for tests), override with
|
|
153
|
+
// PATTERN_PROJECT_ROOT.
|
|
154
|
+
const PROJECT_ROOT = process.env.PATTERN_PROJECT_ROOT ?? process.cwd();
|
|
155
|
+
// Belt-and-suspenders guard against a file_path (ultimately caller-
|
|
156
|
+
// supplied, see recommend_component's input schema) that's absolute or
|
|
157
|
+
// escapes PROJECT_ROOT via "../" -- the calling agent already has real fs
|
|
158
|
+
// access to its own machine regardless, but a stray path should degrade
|
|
159
|
+
// to "unknown" rather than silently stat-ing something outside the
|
|
160
|
+
// project. Returns null (never throws) on anything that doesn't resolve
|
|
161
|
+
// cleanly inside root.
|
|
162
|
+
function resolveWithinRoot(root, relPath) {
|
|
163
|
+
if (!relPath || isAbsolute(relPath))
|
|
164
|
+
return null;
|
|
165
|
+
const resolved = resolve(root, relPath);
|
|
166
|
+
const rel = relative(root, resolved);
|
|
167
|
+
if (rel.startsWith("..") || isAbsolute(rel))
|
|
168
|
+
return null;
|
|
169
|
+
return resolved;
|
|
170
|
+
}
|
|
171
|
+
// Feature 2 / Decision Provenance, P0: best-effort commit SHA at
|
|
172
|
+
// ledger-write time. Never throws -- not being in a git repo, git not
|
|
173
|
+
// being installed, or the call simply timing out all degrade to null
|
|
174
|
+
// rather than failing the judgment call that triggered this write (see
|
|
175
|
+
// buildLedgerEntry). Read-only: `git rev-parse HEAD` never touches repo
|
|
176
|
+
// state.
|
|
177
|
+
function computeSnapshotRef(root) {
|
|
178
|
+
try {
|
|
179
|
+
const sha = execFileSync("git", ["rev-parse", "HEAD"], {
|
|
180
|
+
cwd: root,
|
|
181
|
+
encoding: "utf8",
|
|
182
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
183
|
+
timeout: 2000,
|
|
184
|
+
}).trim();
|
|
185
|
+
return /^[0-9a-f]{7,40}$/i.test(sha) ? sha : null;
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
132
191
|
// Kill switch for the cache-hit short-circuit specifically -- does NOT
|
|
133
192
|
// disable the ledger itself. Entries still get written and read_ledger
|
|
134
193
|
// still works either way; this only controls whether judgeComponent is
|
|
@@ -382,6 +441,8 @@ const EXTRACT_REQUIREMENTS_TOOL_NAME = "extract_requirements";
|
|
|
382
441
|
const READ_LEDGER_TOOL_NAME = "read_ledger";
|
|
383
442
|
const REPORT_BUILD_COST_TOOL_NAME = "report_build_cost";
|
|
384
443
|
const REPORT_OUTCOME_PROXY_TOOL_NAME = "report_outcome_proxy";
|
|
444
|
+
const CHECK_LEDGER_LIVENESS_TOOL_NAME = "check_ledger_liveness";
|
|
445
|
+
const EXPORT_LEDGER_PROVENANCE_TOOL_NAME = "export_ledger_provenance";
|
|
385
446
|
const INPUT_SCHEMA = {
|
|
386
447
|
type: "object",
|
|
387
448
|
properties: {
|
|
@@ -441,6 +502,16 @@ const INPUT_SCHEMA = {
|
|
|
441
502
|
"then land under the same id automatically, with no coordination " +
|
|
442
503
|
"needed between calls. Only meaningful together with project_id.",
|
|
443
504
|
},
|
|
505
|
+
file_path: {
|
|
506
|
+
type: "string",
|
|
507
|
+
description: "Optional. Path (relative to the project root) where this component " +
|
|
508
|
+
"decision is expected to be implemented, if already known -- usually " +
|
|
509
|
+
"not known yet at this call, since the decision typically precedes " +
|
|
510
|
+
"the file existing. When provided, it's stored on the resulting " +
|
|
511
|
+
"ledger entry and check_ledger_liveness can later confirm the file " +
|
|
512
|
+
"still exists and still references chosen_candidate. Omit if unknown; " +
|
|
513
|
+
"it cannot currently be attached to an entry after the fact.",
|
|
514
|
+
},
|
|
444
515
|
},
|
|
445
516
|
required: ["component_need", "domain", "framework"],
|
|
446
517
|
};
|
|
@@ -592,6 +663,35 @@ const REPORT_OUTCOME_PROXY_INPUT_SCHEMA = {
|
|
|
592
663
|
},
|
|
593
664
|
required: ["feature_id"],
|
|
594
665
|
};
|
|
666
|
+
const CHECK_LEDGER_LIVENESS_INPUT_SCHEMA = {
|
|
667
|
+
type: "object",
|
|
668
|
+
properties: {
|
|
669
|
+
project_id: {
|
|
670
|
+
type: "string",
|
|
671
|
+
description: "The project_id used in the recommend_component call(s) whose ledger entries you want live-checked.",
|
|
672
|
+
},
|
|
673
|
+
ledger_entry_id: {
|
|
674
|
+
type: "string",
|
|
675
|
+
description: "Optional. Check just this one entry (its id, from read_ledger) " +
|
|
676
|
+
"instead of every entry for project_id that has a file_path set.",
|
|
677
|
+
},
|
|
678
|
+
},
|
|
679
|
+
required: ["project_id"],
|
|
680
|
+
};
|
|
681
|
+
const EXPORT_LEDGER_PROVENANCE_INPUT_SCHEMA = {
|
|
682
|
+
type: "object",
|
|
683
|
+
properties: {
|
|
684
|
+
project_id: {
|
|
685
|
+
type: "string",
|
|
686
|
+
description: "The project_id used in the recommend_component call that produced this ledger entry.",
|
|
687
|
+
},
|
|
688
|
+
ledger_entry_id: {
|
|
689
|
+
type: "string",
|
|
690
|
+
description: "The specific entry to export, from read_ledger or check_ledger_liveness.",
|
|
691
|
+
},
|
|
692
|
+
},
|
|
693
|
+
required: ["project_id", "ledger_entry_id"],
|
|
694
|
+
};
|
|
595
695
|
// Shared between buildSystemPrompt's own step 2 and
|
|
596
696
|
// buildExtractionSystemPrompt (the extract_requirements tool's standalone
|
|
597
697
|
// prompt) -- the extraction *instructions* are one piece of text reused
|
|
@@ -1243,6 +1343,125 @@ function deriveFeatureId(componentNeed, projectId, provided) {
|
|
|
1243
1343
|
.digest("hex")
|
|
1244
1344
|
.slice(0, 8);
|
|
1245
1345
|
}
|
|
1346
|
+
// Overlay store for live-check results, same "append-only, latest-value-
|
|
1347
|
+
// per-key wins at read time, never mutate the source-of-truth line"
|
|
1348
|
+
// convention as outcome_proxies.jsonl/latestOutcomeProxy above -- a check
|
|
1349
|
+
// is a new observation, not a correction of the original ledger entry, so
|
|
1350
|
+
// ledger.jsonl itself stays untouched by it.
|
|
1351
|
+
const LEDGER_LIVENESS_PATH = process.env.PATTERN_LEDGER_LIVENESS_PATH ?? join(homedir(), ".pattern", "ledger_liveness.jsonl");
|
|
1352
|
+
function appendLedgerLivenessRecord(record) {
|
|
1353
|
+
mkdirSync(dirname(LEDGER_LIVENESS_PATH), { recursive: true });
|
|
1354
|
+
appendFileSync(LEDGER_LIVENESS_PATH, JSON.stringify(record) + "\n", "utf8");
|
|
1355
|
+
}
|
|
1356
|
+
function readLedgerLivenessRecords(ledgerEntryId) {
|
|
1357
|
+
let raw;
|
|
1358
|
+
try {
|
|
1359
|
+
raw = readFileSync(LEDGER_LIVENESS_PATH, "utf8");
|
|
1360
|
+
}
|
|
1361
|
+
catch {
|
|
1362
|
+
return [];
|
|
1363
|
+
}
|
|
1364
|
+
const records = [];
|
|
1365
|
+
for (const line of raw.split("\n")) {
|
|
1366
|
+
if (!line.trim())
|
|
1367
|
+
continue;
|
|
1368
|
+
try {
|
|
1369
|
+
const parsed = JSON.parse(line);
|
|
1370
|
+
if (parsed && typeof parsed === "object" && parsed.ledger_entry_id === ledgerEntryId) {
|
|
1371
|
+
records.push(parsed);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
catch {
|
|
1375
|
+
// skip malformed line
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
return records;
|
|
1379
|
+
}
|
|
1380
|
+
function latestLiveness(ledgerEntryId) {
|
|
1381
|
+
const records = readLedgerLivenessRecords(ledgerEntryId).sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
1382
|
+
return records[0] ?? null;
|
|
1383
|
+
}
|
|
1384
|
+
function withLatestLiveness(entry) {
|
|
1385
|
+
const latest = latestLiveness(entry.id);
|
|
1386
|
+
if (!latest)
|
|
1387
|
+
return entry;
|
|
1388
|
+
return { ...entry, live_status: latest.live_status, last_verified_live: latest.timestamp };
|
|
1389
|
+
}
|
|
1390
|
+
// Feature 1 / Referential Integrity, P1: the single-entry live-check.
|
|
1391
|
+
// Orphaned when file_path is set but the file no longer exists; live when
|
|
1392
|
+
// the file exists and (best-effort) still mentions chosen_candidate;
|
|
1393
|
+
// unknown when file_path was never supplied, escapes PROJECT_ROOT (see
|
|
1394
|
+
// resolveWithinRoot), or exists but the candidate name can't be confirmed
|
|
1395
|
+
// in its content -- conservative on purpose, per the spec's own risk
|
|
1396
|
+
// mitigation (a false "orphaned" is worse than a lingering "unknown").
|
|
1397
|
+
// "dangling" (an entry only cross-referenced by other ledger entries, no
|
|
1398
|
+
// live anchor anywhere) is graph-level analysis across the whole ledger,
|
|
1399
|
+
// not a single-entry check -- Feature 1 P3, not built here.
|
|
1400
|
+
function checkFileLiveStatus(entry) {
|
|
1401
|
+
if (!entry.file_path)
|
|
1402
|
+
return "unknown";
|
|
1403
|
+
const abs = resolveWithinRoot(PROJECT_ROOT, entry.file_path);
|
|
1404
|
+
if (!abs)
|
|
1405
|
+
return "unknown";
|
|
1406
|
+
if (!existsSync(abs))
|
|
1407
|
+
return "orphaned";
|
|
1408
|
+
if (!entry.chosen_candidate)
|
|
1409
|
+
return "live";
|
|
1410
|
+
try {
|
|
1411
|
+
const content = readFileSync(abs, "utf8");
|
|
1412
|
+
return content.toLowerCase().includes(entry.chosen_candidate.toLowerCase()) ? "live" : "unknown";
|
|
1413
|
+
}
|
|
1414
|
+
catch {
|
|
1415
|
+
return "unknown";
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
function checkLedgerEntryLiveness(entry) {
|
|
1419
|
+
const record = {
|
|
1420
|
+
id: randomUUID(),
|
|
1421
|
+
timestamp: new Date().toISOString(),
|
|
1422
|
+
ledger_entry_id: entry.id,
|
|
1423
|
+
project_id: entry.project_id,
|
|
1424
|
+
live_status: checkFileLiveStatus(entry),
|
|
1425
|
+
checked_file_path: entry.file_path,
|
|
1426
|
+
};
|
|
1427
|
+
appendLedgerLivenessRecord(record);
|
|
1428
|
+
return record;
|
|
1429
|
+
}
|
|
1430
|
+
// check_ledger_liveness tool: on-demand invocation of the live-check above
|
|
1431
|
+
// (the design's "on demand via an MCP call" case -- a scheduled/batch
|
|
1432
|
+
// sweep is Feature 1 P2, not built here). Entries with no file_path are
|
|
1433
|
+
// reported but never checked/recorded -- their status is permanently
|
|
1434
|
+
// "unknown" by construction, so re-checking them on every call would only
|
|
1435
|
+
// grow ledger_liveness.jsonl without ever learning anything new.
|
|
1436
|
+
function checkLedgerLiveness(input) {
|
|
1437
|
+
const entries = readLedgerEntries(input.project_id).filter((e) => !input.ledger_entry_id || e.id === input.ledger_entry_id);
|
|
1438
|
+
const results = entries.map((e) => {
|
|
1439
|
+
if (!e.file_path) {
|
|
1440
|
+
return {
|
|
1441
|
+
ledger_entry_id: e.id,
|
|
1442
|
+
component_need: e.component_need,
|
|
1443
|
+
file_path: null,
|
|
1444
|
+
live_status: "unknown",
|
|
1445
|
+
checked_at: null,
|
|
1446
|
+
note: "no file_path recorded on this entry -- nothing to check",
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
const record = checkLedgerEntryLiveness(e);
|
|
1450
|
+
return {
|
|
1451
|
+
ledger_entry_id: e.id,
|
|
1452
|
+
component_need: e.component_need,
|
|
1453
|
+
file_path: e.file_path,
|
|
1454
|
+
live_status: record.live_status,
|
|
1455
|
+
checked_at: record.timestamp,
|
|
1456
|
+
note: null,
|
|
1457
|
+
};
|
|
1458
|
+
});
|
|
1459
|
+
return {
|
|
1460
|
+
checked: results.filter((r) => r.checked_at !== null).length,
|
|
1461
|
+
total_entries: results.length,
|
|
1462
|
+
results,
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1246
1465
|
// Same "missing/malformed collapses to empty" philosophy as readMemory,
|
|
1247
1466
|
// but line-oriented (JSONL) rather than whole-file JSON -- a single
|
|
1248
1467
|
// corrupted line (e.g. a hand-edited file, or a write that got cut off)
|
|
@@ -1262,7 +1481,19 @@ function readLedgerEntries(projectId) {
|
|
|
1262
1481
|
try {
|
|
1263
1482
|
const parsed = JSON.parse(line);
|
|
1264
1483
|
if (parsed && typeof parsed === "object" && parsed.project_id === projectId) {
|
|
1265
|
-
entries
|
|
1484
|
+
// Backward-compatible defaults for entries written before the
|
|
1485
|
+
// ledger integrity/provenance fields existed -- a missing key
|
|
1486
|
+
// (not merely a null one) falls back to these rather than
|
|
1487
|
+
// `undefined` leaking into the returned shape.
|
|
1488
|
+
const rawEntry = parsed;
|
|
1489
|
+
const normalized = {
|
|
1490
|
+
...rawEntry,
|
|
1491
|
+
file_path: rawEntry.file_path ?? null,
|
|
1492
|
+
snapshot_ref: rawEntry.snapshot_ref ?? null,
|
|
1493
|
+
last_verified_live: rawEntry.last_verified_live ?? null,
|
|
1494
|
+
live_status: rawEntry.live_status ?? "unknown",
|
|
1495
|
+
};
|
|
1496
|
+
entries.push(withLatestLiveness(normalized));
|
|
1266
1497
|
}
|
|
1267
1498
|
}
|
|
1268
1499
|
catch {
|
|
@@ -1325,6 +1556,54 @@ function findLedgerMatches(projectId, componentNeed, limit = 20) {
|
|
|
1325
1556
|
entries.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
1326
1557
|
return entries.slice(0, limit);
|
|
1327
1558
|
}
|
|
1559
|
+
// Feature 2 / Decision Provenance, P1: renders one ledger entry as a
|
|
1560
|
+
// stable markdown block -- "stable" meaning a pure function of the entry
|
|
1561
|
+
// alone (never Date.now(), never anything read live off disk), so the
|
|
1562
|
+
// same entry always produces byte-identical markdown. That determinism is
|
|
1563
|
+
// what makes verify-provenance-artifact.mjs's snapshot test meaningful:
|
|
1564
|
+
// a diff in the generated markdown for a fixed fixture means the format
|
|
1565
|
+
// changed, not that time passed. Markdown, not JSON, per the spec --
|
|
1566
|
+
// PRs/issues render it natively (P2, not built here, attaches this to
|
|
1567
|
+
// one).
|
|
1568
|
+
export function formatProvenanceArtifact(entry) {
|
|
1569
|
+
const lines = [];
|
|
1570
|
+
lines.push(`## Pattern decision: ${entry.component_need}`);
|
|
1571
|
+
lines.push("");
|
|
1572
|
+
lines.push(`- **Verdict:** ${entry.verdict} (confidence: ${entry.confidence})`);
|
|
1573
|
+
lines.push(`- **Reason:** ${entry.reason}`);
|
|
1574
|
+
lines.push(`- **Coverage:** ${entry.coverage ?? "n/a"}`);
|
|
1575
|
+
lines.push(`- **Domain:** ${entry.domain}`);
|
|
1576
|
+
lines.push(`- **Framework:** ${entry.framework}`);
|
|
1577
|
+
lines.push(`- **Snapshot:** ${entry.snapshot_ref ? "`" + entry.snapshot_ref + "`" : "not available (project root wasn't a git repository at judgment time)"}`);
|
|
1578
|
+
lines.push(`- **Judged at:** ${entry.timestamp}${entry.cache_hit ? " (served from ledger cache hit)" : ""}`);
|
|
1579
|
+
lines.push("");
|
|
1580
|
+
lines.push("### Requirements checked");
|
|
1581
|
+
if (entry.checklist.length === 0) {
|
|
1582
|
+
lines.push("_No checklist recorded for this entry._");
|
|
1583
|
+
}
|
|
1584
|
+
else {
|
|
1585
|
+
for (const item of entry.checklist)
|
|
1586
|
+
lines.push(`- ${item}`);
|
|
1587
|
+
}
|
|
1588
|
+
lines.push("");
|
|
1589
|
+
lines.push("### Candidates compared");
|
|
1590
|
+
if (entry.candidates_evaluated.length === 0) {
|
|
1591
|
+
lines.push(entry.verdict === "custom_build"
|
|
1592
|
+
? "_No existing candidate met the bar -- Pattern recommended a custom build. Pattern doesn't persist the custom-build reference (Mobbin/Figma) to the ledger, so it isn't reproducible here._"
|
|
1593
|
+
: "_No candidates recorded for this entry._");
|
|
1594
|
+
}
|
|
1595
|
+
else {
|
|
1596
|
+
lines.push("| Source | Name | Coverage | Chosen |");
|
|
1597
|
+
lines.push("| --- | --- | --- | --- |");
|
|
1598
|
+
for (const c of entry.candidates_evaluated) {
|
|
1599
|
+
const chosen = c.name !== null && c.name === entry.chosen_candidate ? "✓" : "";
|
|
1600
|
+
lines.push(`| ${c.source ?? "n/a"} | ${c.name ?? "n/a"} | ${c.coverage_pct ?? "n/a"} | ${chosen} |`);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
lines.push("");
|
|
1604
|
+
lines.push(`_Generated by Pattern (\`export_ledger_provenance\`) from ledger entry \`${entry.id}\`._`);
|
|
1605
|
+
return lines.join("\n");
|
|
1606
|
+
}
|
|
1328
1607
|
// report_build_cost (cost-attribution build plan, 1.3) -- self-reported
|
|
1329
1608
|
// build cost, cheapest option first, since Pattern has no visibility into
|
|
1330
1609
|
// what happens after judgeComponent returns a verdict (1.4's
|
|
@@ -1562,6 +1841,19 @@ function buildLedgerEntry(input, projectId, result, opts) {
|
|
|
1562
1841
|
cost_usd: opts.costUsd,
|
|
1563
1842
|
cache_hit: opts.cacheHit,
|
|
1564
1843
|
project_conventions_snapshot: hashConventions(input.existing_stack),
|
|
1844
|
+
// Feature 2 P0: captured fresh for every entry (cache hits included),
|
|
1845
|
+
// not inherited from a matched ledger_cache_hit -- this reflects the
|
|
1846
|
+
// codebase state at the moment *this line* was written, not the
|
|
1847
|
+
// moment the original judgment ran (see PROJECT_ROOT above).
|
|
1848
|
+
snapshot_ref: computeSnapshotRef(PROJECT_ROOT),
|
|
1849
|
+
// Feature 1 P0: caller-supplied at write time (recommend_component's
|
|
1850
|
+
// optional file_path), null when not yet known -- typically the case,
|
|
1851
|
+
// since the decision is usually made before the file exists. Always
|
|
1852
|
+
// starts "unknown"/unchecked; check_ledger_liveness fills these in
|
|
1853
|
+
// later via the ledger_liveness.jsonl overlay (see withLatestLiveness).
|
|
1854
|
+
file_path: input.file_path ?? null,
|
|
1855
|
+
last_verified_live: null,
|
|
1856
|
+
live_status: "unknown",
|
|
1565
1857
|
};
|
|
1566
1858
|
}
|
|
1567
1859
|
async function judgeComponent(input) {
|
|
@@ -2230,6 +2522,36 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2230
2522
|
"This only appends a local record; it never calls the Anthropic API.",
|
|
2231
2523
|
inputSchema: REPORT_OUTCOME_PROXY_INPUT_SCHEMA,
|
|
2232
2524
|
},
|
|
2525
|
+
{
|
|
2526
|
+
name: CHECK_LEDGER_LIVENESS_TOOL_NAME,
|
|
2527
|
+
description: "Checks whether recommend_component ledger entries for a project_id " +
|
|
2528
|
+
"are still 'live' -- the file_path recorded on the entry (if any) " +
|
|
2529
|
+
"still exists and still mentions chosen_candidate. Requires real, " +
|
|
2530
|
+
"read-only filesystem access to PROJECT_ROOT (defaults to this " +
|
|
2531
|
+
"server's working directory; override with PATTERN_PROJECT_ROOT) -- " +
|
|
2532
|
+
"this is the one exception to Pattern otherwise having no " +
|
|
2533
|
+
"filesystem access to a caller's repo (see report_build_cost/" +
|
|
2534
|
+
"report_outcome_proxy above). Entries with no file_path are listed " +
|
|
2535
|
+
"but not checked -- their status is permanently 'unknown' since " +
|
|
2536
|
+
"there's nothing to check. Never writes to your repo, never runs " +
|
|
2537
|
+
"an arbitrary git/shell command beyond `git rev-parse HEAD` " +
|
|
2538
|
+
"elsewhere in this server. Results are also layered onto " +
|
|
2539
|
+
"read_ledger's live_status/last_verified_live fields for the same " +
|
|
2540
|
+
"entries afterward.",
|
|
2541
|
+
inputSchema: CHECK_LEDGER_LIVENESS_INPUT_SCHEMA,
|
|
2542
|
+
},
|
|
2543
|
+
{
|
|
2544
|
+
name: EXPORT_LEDGER_PROVENANCE_TOOL_NAME,
|
|
2545
|
+
description: "Formats one ledger entry (requirements checklist, candidates " +
|
|
2546
|
+
"compared, verdict, confidence, snapshot_ref) as a single markdown " +
|
|
2547
|
+
"block -- a stable, portable record of that decision you can paste " +
|
|
2548
|
+
"into a PR description or issue by hand. Pure and deterministic: " +
|
|
2549
|
+
"the same entry always produces the same markdown, nothing here " +
|
|
2550
|
+
"reads live system time or disk state. This only formats and " +
|
|
2551
|
+
"returns text; it does not post anything to GitHub or anywhere " +
|
|
2552
|
+
"else -- that's a separate, not-yet-built action.",
|
|
2553
|
+
inputSchema: EXPORT_LEDGER_PROVENANCE_INPUT_SCHEMA,
|
|
2554
|
+
},
|
|
2233
2555
|
],
|
|
2234
2556
|
}));
|
|
2235
2557
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -2358,6 +2680,43 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2358
2680
|
};
|
|
2359
2681
|
}
|
|
2360
2682
|
}
|
|
2683
|
+
if (request.params.name === CHECK_LEDGER_LIVENESS_TOOL_NAME) {
|
|
2684
|
+
const args = request.params.arguments;
|
|
2685
|
+
try {
|
|
2686
|
+
const result = checkLedgerLiveness(args);
|
|
2687
|
+
return {
|
|
2688
|
+
content: [{ type: "text", text: JSON.stringify({ project_id: args.project_id, ...result }) }],
|
|
2689
|
+
};
|
|
2690
|
+
}
|
|
2691
|
+
catch (err) {
|
|
2692
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2693
|
+
return {
|
|
2694
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
2695
|
+
isError: true,
|
|
2696
|
+
};
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
if (request.params.name === EXPORT_LEDGER_PROVENANCE_TOOL_NAME) {
|
|
2700
|
+
const args = request.params.arguments;
|
|
2701
|
+
try {
|
|
2702
|
+
const entry = readLedgerEntries(args.project_id).find((e) => e.id === args.ledger_entry_id);
|
|
2703
|
+
if (!entry) {
|
|
2704
|
+
throw new Error(`No ledger entry with id "${args.ledger_entry_id}" found for project_id "${args.project_id}". Use read_ledger to list entries and their ids.`);
|
|
2705
|
+
}
|
|
2706
|
+
return {
|
|
2707
|
+
content: [
|
|
2708
|
+
{ type: "text", text: JSON.stringify({ ledger_entry_id: entry.id, markdown: formatProvenanceArtifact(entry) }) },
|
|
2709
|
+
],
|
|
2710
|
+
};
|
|
2711
|
+
}
|
|
2712
|
+
catch (err) {
|
|
2713
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2714
|
+
return {
|
|
2715
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
2716
|
+
isError: true,
|
|
2717
|
+
};
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2361
2720
|
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
2362
2721
|
});
|
|
2363
2722
|
async function main() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pattern-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "MCP tool that judges whether a UI component need should be met with an existing shadcn/ui, 21st.dev, or ReUI component or requires a custom build, using field/requirement coverage scored against real component code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|