pattern-mcp 0.5.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.
Files changed (3) hide show
  1. package/README.md +62 -3
  2. package/dist/index.js +96 -0
  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 seven tools:
35
+ It exposes eight tools:
36
36
 
37
37
  - `recommend_component` — evaluates a UI component need and returns a
38
38
  structured recommendation.
@@ -57,6 +57,10 @@ It exposes seven tools:
57
57
  - `check_ledger_liveness` — checks whether a ledger entry's recorded
58
58
  `file_path` still exists and still references its `chosen_candidate` --
59
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).
60
64
 
61
65
  ## How it works
62
66
 
@@ -959,6 +963,55 @@ there's nothing to check. Results here are also layered onto
959
963
  entries afterward -- `check_ledger_liveness` is the only thing that
960
964
  advances those fields past their write-time defaults.
961
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
+
962
1015
  ## Feature cost attribution
963
1016
 
964
1017
  Every `recommend_component` call that writes to the ledger -- a fresh
@@ -1126,8 +1179,14 @@ cache-hit line was written*, not the original judgment's -- to see the
1126
1179
  original judgment's snapshot, look up the entry named in its
1127
1180
  `ledger_entry_id`/`original_verdict_timestamp` fields instead.
1128
1181
 
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.
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`.
1131
1190
 
1132
1191
  ### Referential integrity: `file_path` / `live_status`
1133
1192
 
package/dist/index.js CHANGED
@@ -442,6 +442,7 @@ const READ_LEDGER_TOOL_NAME = "read_ledger";
442
442
  const REPORT_BUILD_COST_TOOL_NAME = "report_build_cost";
443
443
  const REPORT_OUTCOME_PROXY_TOOL_NAME = "report_outcome_proxy";
444
444
  const CHECK_LEDGER_LIVENESS_TOOL_NAME = "check_ledger_liveness";
445
+ const EXPORT_LEDGER_PROVENANCE_TOOL_NAME = "export_ledger_provenance";
445
446
  const INPUT_SCHEMA = {
446
447
  type: "object",
447
448
  properties: {
@@ -677,6 +678,20 @@ const CHECK_LEDGER_LIVENESS_INPUT_SCHEMA = {
677
678
  },
678
679
  required: ["project_id"],
679
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
+ };
680
695
  // Shared between buildSystemPrompt's own step 2 and
681
696
  // buildExtractionSystemPrompt (the extract_requirements tool's standalone
682
697
  // prompt) -- the extraction *instructions* are one piece of text reused
@@ -1541,6 +1556,54 @@ function findLedgerMatches(projectId, componentNeed, limit = 20) {
1541
1556
  entries.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
1542
1557
  return entries.slice(0, limit);
1543
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
+ }
1544
1607
  // report_build_cost (cost-attribution build plan, 1.3) -- self-reported
1545
1608
  // build cost, cheapest option first, since Pattern has no visibility into
1546
1609
  // what happens after judgeComponent returns a verdict (1.4's
@@ -2477,6 +2540,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2477
2540
  "entries afterward.",
2478
2541
  inputSchema: CHECK_LEDGER_LIVENESS_INPUT_SCHEMA,
2479
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
+ },
2480
2555
  ],
2481
2556
  }));
2482
2557
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -2621,6 +2696,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2621
2696
  };
2622
2697
  }
2623
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
+ }
2624
2720
  throw new Error(`Unknown tool: ${request.params.name}`);
2625
2721
  });
2626
2722
  async function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pattern-mcp",
3
- "version": "0.5.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",