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.
Files changed (3) hide show
  1. package/README.md +191 -6
  2. package/dist/index.js +266 -3
  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 six tools:
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) -- 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.
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.push(parsed);
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.4.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",