pattern-mcp 0.4.0 → 0.5.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 +191 -6
- package/dist/index.js +266 -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 seven tools:
|
|
36
36
|
|
|
37
37
|
- `recommend_component` — evaluates a UI component need and returns a
|
|
38
38
|
structured recommendation.
|
|
@@ -54,6 +54,9 @@ 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).
|
|
57
60
|
|
|
58
61
|
## How it works
|
|
59
62
|
|
|
@@ -428,6 +431,17 @@ Omit it to have one derived deterministically from `project_id` +
|
|
|
428
431
|
`component_need`; only meaningful together with `project_id`. See
|
|
429
432
|
[Feature cost attribution](#feature-cost-attribution).
|
|
430
433
|
|
|
434
|
+
#### `file_path`
|
|
435
|
+
|
|
436
|
+
`file_path` is optional -- path (relative to `PROJECT_ROOT`) where this
|
|
437
|
+
component decision is expected to be implemented, if already known.
|
|
438
|
+
Usually not known yet at call time, since the decision typically precedes
|
|
439
|
+
the file existing. When provided, it's stored on the resulting ledger
|
|
440
|
+
entry and [`check_ledger_liveness`](#tool-check_ledger_liveness) can later
|
|
441
|
+
confirm the file still exists and still references `chosen_candidate`. It
|
|
442
|
+
cannot currently be attached to an entry after the fact -- see [Ledger
|
|
443
|
+
integrity and decision provenance](#ledger-integrity-and-decision-provenance).
|
|
444
|
+
|
|
431
445
|
**Is the checklist actually skipped, not just re-derived?** Checked, not
|
|
432
446
|
assumed. `breakdown_ms.extract` for a `checklist`-provided call is smaller
|
|
433
447
|
than the default path's, but not near-zero -- which raised the question of
|
|
@@ -717,12 +731,24 @@ came back with `served_from_ledger: true`.
|
|
|
717
731
|
"coverage": "5/8 (62.5%)",
|
|
718
732
|
"cost_usd": 0.087,
|
|
719
733
|
"cache_hit": false,
|
|
720
|
-
"project_conventions_snapshot": "9f3a1c7e2b0d4f5a"
|
|
734
|
+
"project_conventions_snapshot": "9f3a1c7e2b0d4f5a",
|
|
735
|
+
"file_path": null,
|
|
736
|
+
"snapshot_ref": "a1b2c3d4e5f6...",
|
|
737
|
+
"last_verified_live": null,
|
|
738
|
+
"live_status": "unknown"
|
|
721
739
|
}
|
|
722
740
|
]
|
|
723
741
|
}
|
|
724
742
|
```
|
|
725
743
|
|
|
744
|
+
`file_path`/`snapshot_ref`/`last_verified_live`/`live_status` are the
|
|
745
|
+
ledger integrity + decision provenance fields -- see [Ledger integrity and
|
|
746
|
+
decision provenance](#ledger-integrity-and-decision-provenance) and [Tool:
|
|
747
|
+
`check_ledger_liveness`](#tool-check_ledger_liveness). Entries written
|
|
748
|
+
before this feature shipped read back with `file_path`/`snapshot_ref`/
|
|
749
|
+
`last_verified_live` as `null` and `live_status` as `"unknown"` rather
|
|
750
|
+
than missing keys.
|
|
751
|
+
|
|
726
752
|
Passing `feature_id` instead returns:
|
|
727
753
|
|
|
728
754
|
```json
|
|
@@ -814,10 +840,12 @@ Pattern's own verdict -- the whole point is a signal that could
|
|
|
814
840
|
`coverage_pct`, `confidence`, or any other Pattern-produced field. Compute
|
|
815
841
|
`reworked`/`days_to_rework` and `time_to_merge_hours` from your own repo's
|
|
816
842
|
real git history (e.g. `git log --follow` against the files this
|
|
817
|
-
feature's build touched)
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
843
|
+
feature's build touched) rather than relying on Pattern -- rework rate and
|
|
844
|
+
time-to-merge need real git *history*, a materially bigger surface than
|
|
845
|
+
the one narrow, read-only exception described in [Ledger integrity and
|
|
846
|
+
decision provenance](#ledger-integrity-and-decision-provenance) below.
|
|
847
|
+
Report `status_at_30d` only once a real ~30-day-post-merge horizon has
|
|
848
|
+
actually passed.
|
|
821
849
|
|
|
822
850
|
Safe to call more than once for the same `feature_id` as more signal
|
|
823
851
|
becomes available over time -- e.g. `time_to_merge_hours` right after
|
|
@@ -864,6 +892,73 @@ This only appends a local record to `~/.pattern/outcome_proxies.jsonl`
|
|
|
864
892
|
(override with `PATTERN_OUTCOME_PROXY_PATH`) -- it never calls the
|
|
865
893
|
Anthropic API.
|
|
866
894
|
|
|
895
|
+
## Tool: `check_ledger_liveness`
|
|
896
|
+
|
|
897
|
+
Checks whether ledger entries for a `project_id` are still **live** --
|
|
898
|
+
does the `file_path` recorded on the entry (if any, see
|
|
899
|
+
[`file_path`](#tool-recommend_component)) still exist, and does it still
|
|
900
|
+
mention `chosen_candidate`. See [Ledger integrity and decision
|
|
901
|
+
provenance](#ledger-integrity-and-decision-provenance) for the full design
|
|
902
|
+
and its deliberate limits.
|
|
903
|
+
|
|
904
|
+
This is the **one exception** to Pattern otherwise having no filesystem
|
|
905
|
+
access to your repo (see [Outcome proxies](#outcome-proxies) above) --
|
|
906
|
+
scoped narrowly to read-only `fs.existsSync`/file-read calls against
|
|
907
|
+
`PROJECT_ROOT` (defaults to this server's own working directory; override
|
|
908
|
+
with `PATTERN_PROJECT_ROOT`). It never writes to your repo and never runs
|
|
909
|
+
an arbitrary shell command.
|
|
910
|
+
|
|
911
|
+
### Input
|
|
912
|
+
|
|
913
|
+
```json
|
|
914
|
+
{
|
|
915
|
+
"project_id": "my-booking-app",
|
|
916
|
+
"ledger_entry_id": "a1b2c3d4-..."
|
|
917
|
+
}
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
- `project_id` is required.
|
|
921
|
+
- `ledger_entry_id` is optional -- check just that one entry instead of
|
|
922
|
+
every entry for `project_id` that has a `file_path` set.
|
|
923
|
+
|
|
924
|
+
### Output
|
|
925
|
+
|
|
926
|
+
```json
|
|
927
|
+
{
|
|
928
|
+
"project_id": "my-booking-app",
|
|
929
|
+
"checked": 1,
|
|
930
|
+
"total_entries": 2,
|
|
931
|
+
"results": [
|
|
932
|
+
{
|
|
933
|
+
"ledger_entry_id": "a1b2c3d4-...",
|
|
934
|
+
"component_need": "cancellation policy display with refund tiers by date",
|
|
935
|
+
"file_path": "src/components/CancellationPolicy.tsx",
|
|
936
|
+
"live_status": "live",
|
|
937
|
+
"checked_at": "2026-09-02T20:11:03.442Z",
|
|
938
|
+
"note": null
|
|
939
|
+
},
|
|
940
|
+
{
|
|
941
|
+
"ledger_entry_id": "e5f6a7b8-...",
|
|
942
|
+
"component_need": "gallery",
|
|
943
|
+
"file_path": null,
|
|
944
|
+
"live_status": "unknown",
|
|
945
|
+
"checked_at": null,
|
|
946
|
+
"note": "no file_path recorded on this entry -- nothing to check"
|
|
947
|
+
}
|
|
948
|
+
]
|
|
949
|
+
}
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
`live_status` is one of `"live"`, `"orphaned"`, `"unknown"`, or
|
|
953
|
+
(reserved, not yet produced -- see [Ledger integrity and decision
|
|
954
|
+
provenance](#ledger-integrity-and-decision-provenance)) `"dangling"`.
|
|
955
|
+
Entries with no `file_path` are listed but never checked or written to
|
|
956
|
+
`ledger_liveness.jsonl` -- their status is permanently `"unknown"` since
|
|
957
|
+
there's nothing to check. Results here are also layered onto
|
|
958
|
+
`read_ledger`'s `live_status`/`last_verified_live` fields for the same
|
|
959
|
+
entries afterward -- `check_ledger_liveness` is the only thing that
|
|
960
|
+
advances those fields past their write-time defaults.
|
|
961
|
+
|
|
867
962
|
## Feature cost attribution
|
|
868
963
|
|
|
869
964
|
Every `recommend_component` call that writes to the ledger -- a fresh
|
|
@@ -979,6 +1074,96 @@ convention: a raw or extended object throws rather than silently
|
|
|
979
1074
|
persisting. Run `node scripts/verify-ledger-boundary.mjs` (after
|
|
980
1075
|
`npm run build`) to check this boundary directly.
|
|
981
1076
|
|
|
1077
|
+
## Ledger integrity and decision provenance
|
|
1078
|
+
|
|
1079
|
+
Two gaps in the ledger, surfaced from user feedback: it tracks that a
|
|
1080
|
+
decision was made, but not whether the thing it decided about is still
|
|
1081
|
+
live in your codebase, and it stores the checklist/verdict but not a
|
|
1082
|
+
version pin or an exportable artifact you can attach to a PR or issue.
|
|
1083
|
+
This section covers what's shipped so far -- **P0/P1 of both halves**, not
|
|
1084
|
+
the full spec. See `pattern-ledger-integrity-and-provenance-spec.md` for
|
|
1085
|
+
the complete phased plan; P2/P3 (a scheduled/batch sweep, dangling-cluster
|
|
1086
|
+
detection, the provenance-artifact exporter, and GitHub PR/issue posting)
|
|
1087
|
+
are not built yet.
|
|
1088
|
+
|
|
1089
|
+
**This is the one deliberate exception** to Pattern otherwise having [no
|
|
1090
|
+
filesystem/git access to your repo](#per-project-judgment-ledger) at all
|
|
1091
|
+
(the principle `report_build_cost`/`report_outcome_proxy` are built
|
|
1092
|
+
around). It's narrow on purpose:
|
|
1093
|
+
|
|
1094
|
+
- `git rev-parse HEAD` (read-only, never touches repo state) to capture
|
|
1095
|
+
`snapshot_ref` on every ledger write.
|
|
1096
|
+
- `fs.existsSync` plus a plain-text read of one file, only for a
|
|
1097
|
+
`file_path` you explicitly passed to `recommend_component`, only inside
|
|
1098
|
+
`PROJECT_ROOT` (see below), to answer `check_ledger_liveness`.
|
|
1099
|
+
|
|
1100
|
+
Nothing here runs an arbitrary git or shell command, and nothing writes to
|
|
1101
|
+
your repo.
|
|
1102
|
+
|
|
1103
|
+
### `PROJECT_ROOT`
|
|
1104
|
+
|
|
1105
|
+
Defaults to `process.cwd()` -- for a locally-run stdio MCP server, that's
|
|
1106
|
+
normally the consuming repo's root, since MCP hosts typically launch the
|
|
1107
|
+
server with the project directory as its working directory. Override with
|
|
1108
|
+
`PATTERN_PROJECT_ROOT` if that assumption doesn't hold for your setup.
|
|
1109
|
+
|
|
1110
|
+
A `file_path` that's absolute or escapes `PROJECT_ROOT` via `../` resolves
|
|
1111
|
+
to `live_status: "unknown"` rather than being read -- belt-and-suspenders,
|
|
1112
|
+
since the calling agent already has real filesystem access to its own
|
|
1113
|
+
machine regardless.
|
|
1114
|
+
|
|
1115
|
+
### Decision provenance: `snapshot_ref`
|
|
1116
|
+
|
|
1117
|
+
Every ledger entry -- fresh judgment or [ledger cache
|
|
1118
|
+
hit](#the-cache-hit-exception) -- now carries `snapshot_ref`: the commit
|
|
1119
|
+
SHA of `PROJECT_ROOT` at the moment that line was written, or `null` when
|
|
1120
|
+
`PROJECT_ROOT` isn't a git repo (or `git` isn't installed, or the call
|
|
1121
|
+
times out) -- this never fails the underlying `recommend_component` call.
|
|
1122
|
+
Entries written before this shipped read back with `snapshot_ref: null`.
|
|
1123
|
+
|
|
1124
|
+
A cache-hit entry's `snapshot_ref` reflects the codebase state *when that
|
|
1125
|
+
cache-hit line was written*, not the original judgment's -- to see the
|
|
1126
|
+
original judgment's snapshot, look up the entry named in its
|
|
1127
|
+
`ledger_entry_id`/`original_verdict_timestamp` fields instead.
|
|
1128
|
+
|
|
1129
|
+
Not yet built (P1-P3 of Feature 2): a markdown export of one entry's full
|
|
1130
|
+
record for attaching to a PR/issue, and the GitHub posting action itself.
|
|
1131
|
+
|
|
1132
|
+
### Referential integrity: `file_path` / `live_status`
|
|
1133
|
+
|
|
1134
|
+
`recommend_component` optionally accepts `file_path` (see [Tool:
|
|
1135
|
+
`recommend_component`](#tool-recommend_component)) -- usually not known at
|
|
1136
|
+
call time, since the decision typically precedes the file existing. When
|
|
1137
|
+
set, [`check_ledger_liveness`](#tool-check_ledger_liveness) can later
|
|
1138
|
+
check whether that file still exists and still mentions
|
|
1139
|
+
`chosen_candidate`:
|
|
1140
|
+
|
|
1141
|
+
- **`live`** -- the file exists and mentions `chosen_candidate`.
|
|
1142
|
+
- **`orphaned`** -- `file_path` is set but the file no longer exists.
|
|
1143
|
+
- **`unknown`** -- no `file_path` was ever recorded, the path escapes
|
|
1144
|
+
`PROJECT_ROOT`, or the file exists but `chosen_candidate` can't be
|
|
1145
|
+
confirmed in it. Deliberately the default outcome for anything
|
|
1146
|
+
ambiguous: a false `"orphaned"` is worse than a lingering `"unknown"`.
|
|
1147
|
+
- **`dangling`** -- reserved, not yet produced. Feature 1's second
|
|
1148
|
+
staleness type (a cluster of entries that only reference each other,
|
|
1149
|
+
with no live anchor anywhere) is graph-level analysis across the whole
|
|
1150
|
+
ledger, not a single-entry check -- P3, not built here.
|
|
1151
|
+
|
|
1152
|
+
Checks are on-demand only right now (call `check_ledger_liveness`
|
|
1153
|
+
yourself, or on whatever schedule you want) -- there's no automatic
|
|
1154
|
+
sweep. `live_status`/`last_verified_live` start `"unknown"`/`null` on
|
|
1155
|
+
every entry at write time and only ever advance via a
|
|
1156
|
+
`check_ledger_liveness` call; results are stored append-only in
|
|
1157
|
+
`~/.pattern/ledger_liveness.jsonl` (override with
|
|
1158
|
+
`PATTERN_LEDGER_LIVENESS_PATH`, same "append, never mutate the source
|
|
1159
|
+
line, most recent record wins at read time" convention as
|
|
1160
|
+
`outcome_proxies.jsonl`, see [Outcome proxies](#outcome-proxies)) and
|
|
1161
|
+
layered onto `ledger.jsonl`'s own entries at read time -- the ledger line
|
|
1162
|
+
itself is never rewritten.
|
|
1163
|
+
|
|
1164
|
+
Not yet built (P2-P3 of Feature 1): a scheduled/batch sweep across an
|
|
1165
|
+
entire ledger, and dangling-cluster detection.
|
|
1166
|
+
|
|
982
1167
|
## Per-project decision memory
|
|
983
1168
|
|
|
984
1169
|
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,7 @@ 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";
|
|
385
445
|
const INPUT_SCHEMA = {
|
|
386
446
|
type: "object",
|
|
387
447
|
properties: {
|
|
@@ -441,6 +501,16 @@ const INPUT_SCHEMA = {
|
|
|
441
501
|
"then land under the same id automatically, with no coordination " +
|
|
442
502
|
"needed between calls. Only meaningful together with project_id.",
|
|
443
503
|
},
|
|
504
|
+
file_path: {
|
|
505
|
+
type: "string",
|
|
506
|
+
description: "Optional. Path (relative to the project root) where this component " +
|
|
507
|
+
"decision is expected to be implemented, if already known -- usually " +
|
|
508
|
+
"not known yet at this call, since the decision typically precedes " +
|
|
509
|
+
"the file existing. When provided, it's stored on the resulting " +
|
|
510
|
+
"ledger entry and check_ledger_liveness can later confirm the file " +
|
|
511
|
+
"still exists and still references chosen_candidate. Omit if unknown; " +
|
|
512
|
+
"it cannot currently be attached to an entry after the fact.",
|
|
513
|
+
},
|
|
444
514
|
},
|
|
445
515
|
required: ["component_need", "domain", "framework"],
|
|
446
516
|
};
|
|
@@ -592,6 +662,21 @@ const REPORT_OUTCOME_PROXY_INPUT_SCHEMA = {
|
|
|
592
662
|
},
|
|
593
663
|
required: ["feature_id"],
|
|
594
664
|
};
|
|
665
|
+
const CHECK_LEDGER_LIVENESS_INPUT_SCHEMA = {
|
|
666
|
+
type: "object",
|
|
667
|
+
properties: {
|
|
668
|
+
project_id: {
|
|
669
|
+
type: "string",
|
|
670
|
+
description: "The project_id used in the recommend_component call(s) whose ledger entries you want live-checked.",
|
|
671
|
+
},
|
|
672
|
+
ledger_entry_id: {
|
|
673
|
+
type: "string",
|
|
674
|
+
description: "Optional. Check just this one entry (its id, from read_ledger) " +
|
|
675
|
+
"instead of every entry for project_id that has a file_path set.",
|
|
676
|
+
},
|
|
677
|
+
},
|
|
678
|
+
required: ["project_id"],
|
|
679
|
+
};
|
|
595
680
|
// Shared between buildSystemPrompt's own step 2 and
|
|
596
681
|
// buildExtractionSystemPrompt (the extract_requirements tool's standalone
|
|
597
682
|
// prompt) -- the extraction *instructions* are one piece of text reused
|
|
@@ -1243,6 +1328,125 @@ function deriveFeatureId(componentNeed, projectId, provided) {
|
|
|
1243
1328
|
.digest("hex")
|
|
1244
1329
|
.slice(0, 8);
|
|
1245
1330
|
}
|
|
1331
|
+
// Overlay store for live-check results, same "append-only, latest-value-
|
|
1332
|
+
// per-key wins at read time, never mutate the source-of-truth line"
|
|
1333
|
+
// convention as outcome_proxies.jsonl/latestOutcomeProxy above -- a check
|
|
1334
|
+
// is a new observation, not a correction of the original ledger entry, so
|
|
1335
|
+
// ledger.jsonl itself stays untouched by it.
|
|
1336
|
+
const LEDGER_LIVENESS_PATH = process.env.PATTERN_LEDGER_LIVENESS_PATH ?? join(homedir(), ".pattern", "ledger_liveness.jsonl");
|
|
1337
|
+
function appendLedgerLivenessRecord(record) {
|
|
1338
|
+
mkdirSync(dirname(LEDGER_LIVENESS_PATH), { recursive: true });
|
|
1339
|
+
appendFileSync(LEDGER_LIVENESS_PATH, JSON.stringify(record) + "\n", "utf8");
|
|
1340
|
+
}
|
|
1341
|
+
function readLedgerLivenessRecords(ledgerEntryId) {
|
|
1342
|
+
let raw;
|
|
1343
|
+
try {
|
|
1344
|
+
raw = readFileSync(LEDGER_LIVENESS_PATH, "utf8");
|
|
1345
|
+
}
|
|
1346
|
+
catch {
|
|
1347
|
+
return [];
|
|
1348
|
+
}
|
|
1349
|
+
const records = [];
|
|
1350
|
+
for (const line of raw.split("\n")) {
|
|
1351
|
+
if (!line.trim())
|
|
1352
|
+
continue;
|
|
1353
|
+
try {
|
|
1354
|
+
const parsed = JSON.parse(line);
|
|
1355
|
+
if (parsed && typeof parsed === "object" && parsed.ledger_entry_id === ledgerEntryId) {
|
|
1356
|
+
records.push(parsed);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
catch {
|
|
1360
|
+
// skip malformed line
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
return records;
|
|
1364
|
+
}
|
|
1365
|
+
function latestLiveness(ledgerEntryId) {
|
|
1366
|
+
const records = readLedgerLivenessRecords(ledgerEntryId).sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
1367
|
+
return records[0] ?? null;
|
|
1368
|
+
}
|
|
1369
|
+
function withLatestLiveness(entry) {
|
|
1370
|
+
const latest = latestLiveness(entry.id);
|
|
1371
|
+
if (!latest)
|
|
1372
|
+
return entry;
|
|
1373
|
+
return { ...entry, live_status: latest.live_status, last_verified_live: latest.timestamp };
|
|
1374
|
+
}
|
|
1375
|
+
// Feature 1 / Referential Integrity, P1: the single-entry live-check.
|
|
1376
|
+
// Orphaned when file_path is set but the file no longer exists; live when
|
|
1377
|
+
// the file exists and (best-effort) still mentions chosen_candidate;
|
|
1378
|
+
// unknown when file_path was never supplied, escapes PROJECT_ROOT (see
|
|
1379
|
+
// resolveWithinRoot), or exists but the candidate name can't be confirmed
|
|
1380
|
+
// in its content -- conservative on purpose, per the spec's own risk
|
|
1381
|
+
// mitigation (a false "orphaned" is worse than a lingering "unknown").
|
|
1382
|
+
// "dangling" (an entry only cross-referenced by other ledger entries, no
|
|
1383
|
+
// live anchor anywhere) is graph-level analysis across the whole ledger,
|
|
1384
|
+
// not a single-entry check -- Feature 1 P3, not built here.
|
|
1385
|
+
function checkFileLiveStatus(entry) {
|
|
1386
|
+
if (!entry.file_path)
|
|
1387
|
+
return "unknown";
|
|
1388
|
+
const abs = resolveWithinRoot(PROJECT_ROOT, entry.file_path);
|
|
1389
|
+
if (!abs)
|
|
1390
|
+
return "unknown";
|
|
1391
|
+
if (!existsSync(abs))
|
|
1392
|
+
return "orphaned";
|
|
1393
|
+
if (!entry.chosen_candidate)
|
|
1394
|
+
return "live";
|
|
1395
|
+
try {
|
|
1396
|
+
const content = readFileSync(abs, "utf8");
|
|
1397
|
+
return content.toLowerCase().includes(entry.chosen_candidate.toLowerCase()) ? "live" : "unknown";
|
|
1398
|
+
}
|
|
1399
|
+
catch {
|
|
1400
|
+
return "unknown";
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
function checkLedgerEntryLiveness(entry) {
|
|
1404
|
+
const record = {
|
|
1405
|
+
id: randomUUID(),
|
|
1406
|
+
timestamp: new Date().toISOString(),
|
|
1407
|
+
ledger_entry_id: entry.id,
|
|
1408
|
+
project_id: entry.project_id,
|
|
1409
|
+
live_status: checkFileLiveStatus(entry),
|
|
1410
|
+
checked_file_path: entry.file_path,
|
|
1411
|
+
};
|
|
1412
|
+
appendLedgerLivenessRecord(record);
|
|
1413
|
+
return record;
|
|
1414
|
+
}
|
|
1415
|
+
// check_ledger_liveness tool: on-demand invocation of the live-check above
|
|
1416
|
+
// (the design's "on demand via an MCP call" case -- a scheduled/batch
|
|
1417
|
+
// sweep is Feature 1 P2, not built here). Entries with no file_path are
|
|
1418
|
+
// reported but never checked/recorded -- their status is permanently
|
|
1419
|
+
// "unknown" by construction, so re-checking them on every call would only
|
|
1420
|
+
// grow ledger_liveness.jsonl without ever learning anything new.
|
|
1421
|
+
function checkLedgerLiveness(input) {
|
|
1422
|
+
const entries = readLedgerEntries(input.project_id).filter((e) => !input.ledger_entry_id || e.id === input.ledger_entry_id);
|
|
1423
|
+
const results = entries.map((e) => {
|
|
1424
|
+
if (!e.file_path) {
|
|
1425
|
+
return {
|
|
1426
|
+
ledger_entry_id: e.id,
|
|
1427
|
+
component_need: e.component_need,
|
|
1428
|
+
file_path: null,
|
|
1429
|
+
live_status: "unknown",
|
|
1430
|
+
checked_at: null,
|
|
1431
|
+
note: "no file_path recorded on this entry -- nothing to check",
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
const record = checkLedgerEntryLiveness(e);
|
|
1435
|
+
return {
|
|
1436
|
+
ledger_entry_id: e.id,
|
|
1437
|
+
component_need: e.component_need,
|
|
1438
|
+
file_path: e.file_path,
|
|
1439
|
+
live_status: record.live_status,
|
|
1440
|
+
checked_at: record.timestamp,
|
|
1441
|
+
note: null,
|
|
1442
|
+
};
|
|
1443
|
+
});
|
|
1444
|
+
return {
|
|
1445
|
+
checked: results.filter((r) => r.checked_at !== null).length,
|
|
1446
|
+
total_entries: results.length,
|
|
1447
|
+
results,
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1246
1450
|
// Same "missing/malformed collapses to empty" philosophy as readMemory,
|
|
1247
1451
|
// but line-oriented (JSONL) rather than whole-file JSON -- a single
|
|
1248
1452
|
// corrupted line (e.g. a hand-edited file, or a write that got cut off)
|
|
@@ -1262,7 +1466,19 @@ function readLedgerEntries(projectId) {
|
|
|
1262
1466
|
try {
|
|
1263
1467
|
const parsed = JSON.parse(line);
|
|
1264
1468
|
if (parsed && typeof parsed === "object" && parsed.project_id === projectId) {
|
|
1265
|
-
entries
|
|
1469
|
+
// Backward-compatible defaults for entries written before the
|
|
1470
|
+
// ledger integrity/provenance fields existed -- a missing key
|
|
1471
|
+
// (not merely a null one) falls back to these rather than
|
|
1472
|
+
// `undefined` leaking into the returned shape.
|
|
1473
|
+
const rawEntry = parsed;
|
|
1474
|
+
const normalized = {
|
|
1475
|
+
...rawEntry,
|
|
1476
|
+
file_path: rawEntry.file_path ?? null,
|
|
1477
|
+
snapshot_ref: rawEntry.snapshot_ref ?? null,
|
|
1478
|
+
last_verified_live: rawEntry.last_verified_live ?? null,
|
|
1479
|
+
live_status: rawEntry.live_status ?? "unknown",
|
|
1480
|
+
};
|
|
1481
|
+
entries.push(withLatestLiveness(normalized));
|
|
1266
1482
|
}
|
|
1267
1483
|
}
|
|
1268
1484
|
catch {
|
|
@@ -1562,6 +1778,19 @@ function buildLedgerEntry(input, projectId, result, opts) {
|
|
|
1562
1778
|
cost_usd: opts.costUsd,
|
|
1563
1779
|
cache_hit: opts.cacheHit,
|
|
1564
1780
|
project_conventions_snapshot: hashConventions(input.existing_stack),
|
|
1781
|
+
// Feature 2 P0: captured fresh for every entry (cache hits included),
|
|
1782
|
+
// not inherited from a matched ledger_cache_hit -- this reflects the
|
|
1783
|
+
// codebase state at the moment *this line* was written, not the
|
|
1784
|
+
// moment the original judgment ran (see PROJECT_ROOT above).
|
|
1785
|
+
snapshot_ref: computeSnapshotRef(PROJECT_ROOT),
|
|
1786
|
+
// Feature 1 P0: caller-supplied at write time (recommend_component's
|
|
1787
|
+
// optional file_path), null when not yet known -- typically the case,
|
|
1788
|
+
// since the decision is usually made before the file exists. Always
|
|
1789
|
+
// starts "unknown"/unchecked; check_ledger_liveness fills these in
|
|
1790
|
+
// later via the ledger_liveness.jsonl overlay (see withLatestLiveness).
|
|
1791
|
+
file_path: input.file_path ?? null,
|
|
1792
|
+
last_verified_live: null,
|
|
1793
|
+
live_status: "unknown",
|
|
1565
1794
|
};
|
|
1566
1795
|
}
|
|
1567
1796
|
async function judgeComponent(input) {
|
|
@@ -2230,6 +2459,24 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2230
2459
|
"This only appends a local record; it never calls the Anthropic API.",
|
|
2231
2460
|
inputSchema: REPORT_OUTCOME_PROXY_INPUT_SCHEMA,
|
|
2232
2461
|
},
|
|
2462
|
+
{
|
|
2463
|
+
name: CHECK_LEDGER_LIVENESS_TOOL_NAME,
|
|
2464
|
+
description: "Checks whether recommend_component ledger entries for a project_id " +
|
|
2465
|
+
"are still 'live' -- the file_path recorded on the entry (if any) " +
|
|
2466
|
+
"still exists and still mentions chosen_candidate. Requires real, " +
|
|
2467
|
+
"read-only filesystem access to PROJECT_ROOT (defaults to this " +
|
|
2468
|
+
"server's working directory; override with PATTERN_PROJECT_ROOT) -- " +
|
|
2469
|
+
"this is the one exception to Pattern otherwise having no " +
|
|
2470
|
+
"filesystem access to a caller's repo (see report_build_cost/" +
|
|
2471
|
+
"report_outcome_proxy above). Entries with no file_path are listed " +
|
|
2472
|
+
"but not checked -- their status is permanently 'unknown' since " +
|
|
2473
|
+
"there's nothing to check. Never writes to your repo, never runs " +
|
|
2474
|
+
"an arbitrary git/shell command beyond `git rev-parse HEAD` " +
|
|
2475
|
+
"elsewhere in this server. Results are also layered onto " +
|
|
2476
|
+
"read_ledger's live_status/last_verified_live fields for the same " +
|
|
2477
|
+
"entries afterward.",
|
|
2478
|
+
inputSchema: CHECK_LEDGER_LIVENESS_INPUT_SCHEMA,
|
|
2479
|
+
},
|
|
2233
2480
|
],
|
|
2234
2481
|
}));
|
|
2235
2482
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -2358,6 +2605,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2358
2605
|
};
|
|
2359
2606
|
}
|
|
2360
2607
|
}
|
|
2608
|
+
if (request.params.name === CHECK_LEDGER_LIVENESS_TOOL_NAME) {
|
|
2609
|
+
const args = request.params.arguments;
|
|
2610
|
+
try {
|
|
2611
|
+
const result = checkLedgerLiveness(args);
|
|
2612
|
+
return {
|
|
2613
|
+
content: [{ type: "text", text: JSON.stringify({ project_id: args.project_id, ...result }) }],
|
|
2614
|
+
};
|
|
2615
|
+
}
|
|
2616
|
+
catch (err) {
|
|
2617
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2618
|
+
return {
|
|
2619
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
2620
|
+
isError: true,
|
|
2621
|
+
};
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2361
2624
|
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
2362
2625
|
});
|
|
2363
2626
|
async function main() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pattern-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|