@productbrain/mcp 0.0.1-beta.158 → 0.0.1-beta.159

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.
@@ -7,6 +7,7 @@ import {
7
7
  trackCaptureQualityHints,
8
8
  trackCaptureRelationSuggestions,
9
9
  trackChainEntryCommitted,
10
+ trackClassifierDivergence,
10
11
  trackCollectionClassified,
11
12
  trackCommitErrorByCode,
12
13
  trackFieldGuidanceApplied,
@@ -18,7 +19,7 @@ import {
18
19
  trackToolCall,
19
20
  trackWriteBackHintServed,
20
21
  trackZeroCaptureAuditFired
21
- } from "./chunk-KGGSEIZ3.js";
22
+ } from "./chunk-YMF3IQ5E.js";
22
23
 
23
24
  // src/auth.ts
24
25
  import { AsyncLocalStorage } from "async_hooks";
@@ -1835,7 +1836,9 @@ var captureSchema = z2.object({
1835
1836
  "If true, commits the entry immediately after capture + linking. If omitted, Open mode workspaces auto-commit by default and consensus/role modes stay draft-first."
1836
1837
  ),
1837
1838
  sourceRef: z2.string().optional().describe("URI or path of the source document backing this entry (e.g. 'meeting-2026-03-28.md', 'import://batch-5'). Stored as top-level entry field, not in data."),
1838
- sourceExcerpt: z2.string().optional().describe("Verbatim excerpt from the source that backs this entry's claims. Stored as top-level entry field, not in data.")
1839
+ sourceExcerpt: z2.string().optional().describe("Verbatim excerpt from the source that backs this entry's claims. Stored as top-level entry field, not in data."),
1840
+ // WP-316 S3: Preview gate — dry-run mode. Returns what would happen, no DB writes.
1841
+ preview: z2.boolean().optional().describe("If true, validates the capture without writing. Returns what would happen. Default false.")
1839
1842
  });
1840
1843
  var batchCaptureSchema = z2.object({
1841
1844
  entries: z2.array(z2.object({
@@ -1850,7 +1853,9 @@ var batchCaptureSchema = z2.object({
1850
1853
  })).min(1).max(50).describe("Array of entries to capture"),
1851
1854
  autoCommit: z2.boolean().optional().describe(
1852
1855
  "If true, commits created entries immediately after linking. If omitted, Open mode workspaces commit by default and consensus/role modes stay draft-first."
1853
- )
1856
+ ),
1857
+ // WP-316 S3: Preview gate — dry-run mode. Returns what would happen, no DB writes.
1858
+ preview: z2.boolean().optional().describe("If true, validates all captures without writing. Returns what would happen. Default false.")
1854
1859
  });
1855
1860
  var captureClassifierSchema = z2.object({
1856
1861
  enabled: z2.boolean(),
@@ -1864,7 +1869,9 @@ var captureClassifierSchema = z2.object({
1864
1869
  z2.object({
1865
1870
  collection: z2.string(),
1866
1871
  signalScore: z2.number().optional(),
1867
- confidence: z2.number()
1872
+ confidence: z2.number(),
1873
+ /** WP-316 S2: required-field cost — how many fields the agent must supply for this collection. */
1874
+ requiredFieldCount: z2.number().optional()
1868
1875
  })
1869
1876
  ),
1870
1877
  agentProvidedCollection: z2.string().optional(),
@@ -1922,7 +1929,17 @@ function buildLowConfidenceResult(resolved, classifierMeta) {
1922
1929
  { collection: resolved.collection, confidence: resolved.confidence },
1923
1930
  ...resolved.alternatives
1924
1931
  ];
1925
- const suggestions = allCandidates.map((c) => `- \`${c.collection}\` (${c.confidence}% confidence)`).join("\n");
1932
+ const candidateFieldCounts = /* @__PURE__ */ new Map();
1933
+ for (const c of classifierMeta.candidates) {
1934
+ if (c.requiredFieldCount !== void 0) {
1935
+ candidateFieldCounts.set(c.collection, c.requiredFieldCount);
1936
+ }
1937
+ }
1938
+ const suggestions = allCandidates.map((c) => {
1939
+ const fieldCost = candidateFieldCounts.get(c.collection);
1940
+ const fieldNote = fieldCost !== void 0 ? `, ${fieldCost} required field${fieldCost === 1 ? "" : "s"}` : "";
1941
+ return `- \`${c.collection}\` (${c.confidence}% confidence${fieldNote})`;
1942
+ }).join("\n");
1926
1943
  const textBody = `Low confidence classification (${resolved.confidence}%, classified by ${resolved.classifiedBy}).
1927
1944
 
1928
1945
  Predicted: \`${resolved.collection}\`
@@ -1966,6 +1983,19 @@ function buildClassifierMeta(resolved, overrides = {}) {
1966
1983
  ...overrides
1967
1984
  };
1968
1985
  }
1986
+ async function enrichCandidatesWithFieldCounts(meta) {
1987
+ try {
1988
+ const cols = await getCollections();
1989
+ const reqBySlug = new Map(
1990
+ cols.map((c) => [c.slug, (c.fields ?? []).filter((f) => f.required === true).length])
1991
+ );
1992
+ meta.candidates = meta.candidates.map((c) => ({
1993
+ ...c,
1994
+ requiredFieldCount: reqBySlug.get(c.collection) ?? 0
1995
+ }));
1996
+ } catch {
1997
+ }
1998
+ }
1969
1999
  function emitClassificationTelemetry(resolved, workspaceId, opts) {
1970
2000
  const topAlt = resolved.alternatives?.[0] ?? null;
1971
2001
  trackCollectionClassified(workspaceId, {
@@ -2001,6 +2031,7 @@ async function resolveCaptureCollection(params) {
2001
2031
  overrideCommand: `capture collection="${resolved2.collection}"`
2002
2032
  }
2003
2033
  });
2034
+ await enrichCandidatesWithFieldCounts(classifierMeta2);
2004
2035
  emitClassificationTelemetry(resolved2, workspaceId, {
2005
2036
  explicitCollectionProvided: true,
2006
2037
  autoRouted: false
@@ -2015,6 +2046,14 @@ async function resolveCaptureCollection(params) {
2015
2046
  explicitCollectionProvided: true,
2016
2047
  outcome: "fallback"
2017
2048
  });
2049
+ const agentInCandidates = resolved2.alternatives.some((a) => a.collection === collection) || resolved2.collection === collection;
2050
+ trackClassifierDivergence(workspaceId, {
2051
+ classifier_collection: resolved2.collection,
2052
+ agent_collection: collection,
2053
+ classifier_confidence: resolved2.confidence,
2054
+ classifier_tier: resolved2.tier,
2055
+ agent_in_candidates: agentInCandidates
2056
+ });
2018
2057
  }
2019
2058
  return { resolvedCollection: collection, classifierMeta: classifierMeta2 };
2020
2059
  }
@@ -2051,6 +2090,7 @@ async function resolveCaptureCollection(params) {
2051
2090
  }
2052
2091
  const autoRoute = resolved.tier !== "low";
2053
2092
  const classifierMeta = buildClassifierMeta(resolved, { autoRouted: autoRoute });
2093
+ await enrichCandidatesWithFieldCounts(classifierMeta);
2054
2094
  emitClassificationTelemetry(resolved, workspaceId, {
2055
2095
  explicitCollectionProvided,
2056
2096
  autoRouted: autoRoute
@@ -2226,7 +2266,7 @@ function registerSmartCaptureTools(server) {
2226
2266
  inputSchema: captureSchema.shape,
2227
2267
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
2228
2268
  },
2229
- withEnvelope(async ({ collection, name, description, context, entryId, canonicalKey, data: userData, links, autoCommit, sourceRef, sourceExcerpt }) => {
2269
+ withEnvelope(async ({ collection, name, description, context, entryId, canonicalKey, data: userData, links, autoCommit, sourceRef, sourceExcerpt, preview }) => {
2230
2270
  requireWriteAccess();
2231
2271
  const timingStart = Date.now();
2232
2272
  const wsCtx = await getWorkspaceContext();
@@ -2385,8 +2425,33 @@ Or use \`collections action=list\` to see available collections.`
2385
2425
  createdBy: agentId ? `agent:${agentId}` : "capture",
2386
2426
  sessionId: agentId ?? void 0,
2387
2427
  ...sourceRef ? { sourceRef } : {},
2388
- ...sourceExcerpt ? { sourceExcerpt } : {}
2428
+ ...sourceExcerpt ? { sourceExcerpt } : {},
2429
+ ...preview ? { preview: true } : {}
2389
2430
  });
2431
+ if (result?.preview) {
2432
+ const previewEnvelope = success(
2433
+ `Preview: would capture "${name}" \u2014 no DB writes`,
2434
+ {
2435
+ entryId: result.entryId,
2436
+ name,
2437
+ collection: resolvedCollection,
2438
+ outcome: "preview",
2439
+ warnings: result.warnings ?? []
2440
+ },
2441
+ [{ tool: "capture", description: "Capture for real", parameters: { collection: resolvedCollection, name, description } }]
2442
+ );
2443
+ if (result.contract) {
2444
+ previewEnvelope.contract = result.contract;
2445
+ }
2446
+ return {
2447
+ content: [{ type: "text", text: `# Preview: would capture "${name}" [${resolvedCollection}]
2448
+
2449
+ No DB writes \u2014 call without \`preview:true\` to capture for real.${result.warnings?.length ? `
2450
+
2451
+ **Warnings:** ${result.warnings.join("; ")}` : ""}` }],
2452
+ structuredContent: previewEnvelope
2453
+ };
2454
+ }
2390
2455
  internalId = result.docId;
2391
2456
  finalEntryId = result.entryId;
2392
2457
  entryWarnings.push(...result.warnings ?? []);
@@ -2468,9 +2533,9 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2468
2533
  if (linksSuggested.length >= MAX_SUGGESTIONS) break;
2469
2534
  if (autoTargetIds.has(c.entryId)) continue;
2470
2535
  if (c.confidence < 10) continue;
2471
- const preview = extractPreview(c.data, 80);
2536
+ const preview2 = extractPreview(c.data, 80);
2472
2537
  const reason = c.confidence >= AUTO_LINK_CONFIDENCE_THRESHOLD ? "high relevance (already linked)" : `"${c.name.toLowerCase().split(/\s+/).filter((w) => `${name} ${description}`.toLowerCase().includes(w) && w.length > 3).slice(0, 2).join('", "')}" appears in content`;
2473
- linksSuggested.push({ entryId: c.entryId, name: c.name, collection: c.collSlug, reason, preview });
2538
+ linksSuggested.push({ entryId: c.entryId, name: c.name, collection: c.collSlug, reason, preview: preview2 });
2474
2539
  }
2475
2540
  conflictCandidates = candidates.filter((c) => c.entryId && c.entryId !== finalEntryId).slice(0, 3).map((c) => ({ entryId: c.entryId, name: c.name, collection: c.collSlug }));
2476
2541
  }
@@ -2701,8 +2766,8 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2701
2766
  lines.push("## Suggested links (review and use `relations action=create`)");
2702
2767
  for (let i = 0; i < linksSuggested.length; i++) {
2703
2768
  const s = linksSuggested[i];
2704
- const preview = s.preview ? ` \u2014 ${s.preview}` : "";
2705
- lines.push(`${i + 1}. **${s.entryId ?? "(no ID)"}**: ${s.name} [${s.collection}]${preview}`);
2769
+ const preview2 = s.preview ? ` \u2014 ${s.preview}` : "";
2770
+ lines.push(`${i + 1}. **${s.entryId ?? "(no ID)"}**: ${s.name} [${s.collection}]${preview2}`);
2706
2771
  }
2707
2772
  }
2708
2773
  lines.push("");
@@ -2902,7 +2967,7 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2902
2967
  inputSchema: batchCaptureSchema.shape,
2903
2968
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
2904
2969
  },
2905
- withEnvelope(async ({ entries, autoCommit }) => {
2970
+ withEnvelope(async ({ entries, autoCommit, preview }) => {
2906
2971
  requireWriteAccess();
2907
2972
  const batchTimingStart = Date.now();
2908
2973
  const agentId = getAgentSessionId();
@@ -3082,7 +3147,8 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
3082
3147
  sessionId: agentId ?? void 0,
3083
3148
  canonicalKey: entry.canonicalKey ?? void 0,
3084
3149
  ...entry.sourceRef ? { sourceRef: entry.sourceRef } : {},
3085
- ...entry.sourceExcerpt ? { sourceExcerpt: entry.sourceExcerpt } : {}
3150
+ ...entry.sourceExcerpt ? { sourceExcerpt: entry.sourceExcerpt } : {},
3151
+ ...preview ? { preview: true } : {}
3086
3152
  });
3087
3153
  const internalId = result.docId;
3088
3154
  const finalEntryId = result.entryId;
@@ -3669,7 +3735,9 @@ var getHistorySchema = z3.object({
3669
3735
  entryId: z3.string().describe("Entry ID, e.g. 'T-SUPPLIER', 'BR-001'")
3670
3736
  });
3671
3737
  var commitEntrySchema = z3.object({
3672
- entryId: z3.string().describe("Entry ID to commit, e.g. 'TEN-abc123', 'GT-019'")
3738
+ entryId: z3.string().describe("Entry ID to commit, e.g. 'TEN-abc123', 'GT-019'"),
3739
+ // WP-316 S3: Preview gate — dry-run mode. Returns would-succeed result, no DB writes.
3740
+ preview: z3.boolean().optional().describe("If true, validates the commit without writing. Returns what would happen. Default false.")
3673
3741
  });
3674
3742
  function registerKnowledgeTools(server) {
3675
3743
  const updateTool = server.registerTool(
@@ -3838,7 +3906,7 @@ ${formatted}` }],
3838
3906
  inputSchema: commitEntrySchema,
3839
3907
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
3840
3908
  },
3841
- withEnvelope(async ({ entryId }) => {
3909
+ withEnvelope(async ({ entryId, preview }) => {
3842
3910
  requireWriteAccess();
3843
3911
  const entry = await mcpQuery("chain.getEntry", { entryId });
3844
3912
  if (!entry) {
@@ -3901,7 +3969,8 @@ ${formatted}` }],
3901
3969
  result = await mcpMutation("chain.commitEntry", {
3902
3970
  entryId,
3903
3971
  author: getAgentSessionId() ? `agent:${getAgentSessionId()}` : void 0,
3904
- sessionId: getAgentSessionId() ?? void 0
3972
+ sessionId: getAgentSessionId() ?? void 0,
3973
+ ...preview ? { preview: true } : {}
3905
3974
  });
3906
3975
  } catch (commitErr) {
3907
3976
  const errCode = commitErr?.code;
@@ -3920,6 +3989,30 @@ ${formatted}` }],
3920
3989
  }
3921
3990
  throw commitErr;
3922
3991
  }
3992
+ if (result?.preview) {
3993
+ const previewEnvelope = success(
3994
+ `Preview: would commit ${entryId} \u2014 no DB writes`,
3995
+ {
3996
+ entryId,
3997
+ name: entry.name,
3998
+ outcome: "preview",
3999
+ currentStatus: result.currentStatus,
4000
+ wouldSetStatus: result.wouldSetStatus
4001
+ },
4002
+ [{ tool: "commit-entry", description: "Commit for real", parameters: { entryId } }]
4003
+ );
4004
+ if (result.contract) {
4005
+ previewEnvelope.contract = result.contract;
4006
+ }
4007
+ return {
4008
+ content: [{ type: "text", text: `# Preview: would commit ${entryId}
4009
+
4010
+ Current status: \`${result.currentStatus}\` \u2192 would become \`${result.wouldSetStatus}\`.
4011
+
4012
+ No DB writes \u2014 call without \`preview:true\` to commit for real.` }],
4013
+ structuredContent: previewEnvelope
4014
+ };
4015
+ }
3923
4016
  const docId = result?._id ?? entry._id;
3924
4017
  const wsCtx = await getWorkspaceContext();
3925
4018
  const isProposal = result?.status === "proposal_created";
@@ -4794,7 +4887,9 @@ var relationsSchema = z6.object({
4794
4887
  from: z6.string(),
4795
4888
  to: z6.string(),
4796
4889
  type: z6.string()
4797
- })).min(1).max(20).optional().describe("Array of relations for batch-create")
4890
+ })).min(1).max(20).optional().describe("Array of relations for batch-create"),
4891
+ // WP-316 S3: Preview gate — dry-run mode for action=create.
4892
+ preview: z6.boolean().optional().describe("If true, validates the relation without writing. Returns what would happen. Default false. Only applies to action=create.")
4798
4893
  });
4799
4894
  function registerRelationsTools(server) {
4800
4895
  const tool = server.registerTool(
@@ -4808,13 +4903,13 @@ function registerRelationsTools(server) {
4808
4903
  withEnvelope(async (args) => {
4809
4904
  const parsed = parseOrFail(relationsSchema, args);
4810
4905
  if (!parsed.ok) return parsed.result;
4811
- const { action, from, to, type, score, relations } = parsed.data;
4906
+ const { action, from, to, type, score, relations, preview } = parsed.data;
4812
4907
  return runWithToolContext({ tool: "relations", action }, async () => {
4813
4908
  if (action === "create") {
4814
4909
  if (!from || !to || !type) {
4815
4910
  return validationResult("from, to, and type are required when action is 'create'.");
4816
4911
  }
4817
- return handleCreate(from, to, type, score);
4912
+ return handleCreate(from, to, type, score, preview);
4818
4913
  }
4819
4914
  if (action === "batch-create") {
4820
4915
  if (!relations || relations.length === 0) {
@@ -4840,7 +4935,7 @@ function registerRelationsTools(server) {
4840
4935
  );
4841
4936
  trackWriteTool(tool);
4842
4937
  }
4843
- async function handleCreate(from, to, type, score) {
4938
+ async function handleCreate(from, to, type, score, preview) {
4844
4939
  requireWriteAccess();
4845
4940
  const agentSessionId = getAgentSessionId();
4846
4941
  const result = await mcpMutation("chain.createEntryRelation", {
@@ -4848,8 +4943,31 @@ async function handleCreate(from, to, type, score) {
4848
4943
  toEntryId: to,
4849
4944
  type,
4850
4945
  suggestionScore: score ?? void 0,
4851
- sessionId: agentSessionId ?? void 0
4946
+ sessionId: agentSessionId ?? void 0,
4947
+ ...preview ? { preview: true } : {}
4852
4948
  });
4949
+ if (result?.preview) {
4950
+ const suggested2 = result.suggestedType;
4951
+ const suggestedWithPosture2 = suggested2 ? { ...suggested2, governancePosture: suggested2.type === "governs" ? "requires_consent" : "direct" } : void 0;
4952
+ return {
4953
+ content: [{ type: "text", text: `# Preview: would relate ${from} \u2192 ${to} (${type})
4954
+
4955
+ No DB writes \u2014 call without \`preview:true\` to create for real.${suggestedWithPosture2 ? `
4956
+
4957
+ **Type suggestion:** \`${suggestedWithPosture2.type}\` may be more precise (${suggestedWithPosture2.confidence}% confidence). Governance: ${suggestedWithPosture2.governancePosture}.` : ""}` }],
4958
+ structuredContent: success(
4959
+ `Preview: would relate ${from} \u2192 ${to} (${type}) \u2014 no DB writes`,
4960
+ {
4961
+ status: "preview",
4962
+ from,
4963
+ to,
4964
+ type,
4965
+ ...suggestedWithPosture2 && { suggestedType: suggestedWithPosture2 }
4966
+ },
4967
+ [{ tool: "relations", description: "Create for real", parameters: { action: "create", from, to, type } }]
4968
+ )
4969
+ };
4970
+ }
4853
4971
  const wsCtx = await getWorkspaceContext();
4854
4972
  if (result?.status === "proposal_created") {
4855
4973
  const existingNote = result.existing ? " (existing proposal reused)" : "";
@@ -4880,11 +4998,12 @@ The relation was **not applied** \u2014 it will be created when the proposal is
4880
4998
  `**Workspace:** ${wsCtx.workspaceSlug} (${wsCtx.workspaceId})`
4881
4999
  ];
4882
5000
  const suggested = result?.suggestedType;
4883
- if (suggested) {
5001
+ const suggestedWithPosture = suggested ? { ...suggested, governancePosture: suggested.type === "governs" ? "requires_consent" : "direct" } : void 0;
5002
+ if (suggestedWithPosture) {
4884
5003
  lines.push("");
4885
- lines.push(`**Type suggestion:** \`${suggested.type}\` may be more precise (${suggested.confidence}% confidence).`);
4886
- lines.push(`_${suggested.reasoning}_`);
4887
- lines.push(`To use instead: \`relations action=create from="${from}" to="${to}" type="${suggested.type}"\``);
5004
+ lines.push(`**Type suggestion:** \`${suggestedWithPosture.type}\` may be more precise (${suggestedWithPosture.confidence}% confidence). Governance: ${suggestedWithPosture.governancePosture}.`);
5005
+ lines.push(`_${suggestedWithPosture.reasoning}_`);
5006
+ lines.push(`To use instead: \`relations action=create from="${from}" to="${to}" type="${suggestedWithPosture.type}"\``);
4888
5007
  }
4889
5008
  return {
4890
5009
  content: [{ type: "text", text: lines.join("\n") }],
@@ -4895,7 +5014,7 @@ The relation was **not applied** \u2014 it will be created when the proposal is
4895
5014
  from,
4896
5015
  to,
4897
5016
  type,
4898
- ...suggested && { suggestedType: suggested }
5017
+ ...suggestedWithPosture && { suggestedType: suggestedWithPosture }
4899
5018
  },
4900
5019
  [{ tool: "graph", description: "See connections", parameters: { action: "find", entryId: from } }]
4901
5020
  )
@@ -15520,4 +15639,4 @@ export {
15520
15639
  SERVER_VERSION,
15521
15640
  createProductBrainServer
15522
15641
  };
15523
- //# sourceMappingURL=chunk-ZZHGJKLF.js.map
15642
+ //# sourceMappingURL=chunk-LKUFRQUO.js.map