@productbrain/mcp 0.0.1-beta.157 → 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.
@@ -3,10 +3,13 @@ import {
3
3
  trackCaptureClassifierAutoRouted,
4
4
  trackCaptureClassifierEvaluated,
5
5
  trackCaptureClassifierFallback,
6
+ trackCaptureContractMiss,
6
7
  trackCaptureQualityHints,
7
8
  trackCaptureRelationSuggestions,
8
9
  trackChainEntryCommitted,
10
+ trackClassifierDivergence,
9
11
  trackCollectionClassified,
12
+ trackCommitErrorByCode,
10
13
  trackFieldGuidanceApplied,
11
14
  trackFieldQualityWarning,
12
15
  trackKnowledgeGap,
@@ -16,7 +19,7 @@ import {
16
19
  trackToolCall,
17
20
  trackWriteBackHintServed,
18
21
  trackZeroCaptureAuditFired
19
- } from "./chunk-WYVQARYT.js";
22
+ } from "./chunk-YMF3IQ5E.js";
20
23
 
21
24
  // src/auth.ts
22
25
  import { AsyncLocalStorage } from "async_hooks";
@@ -86,11 +89,17 @@ var DEFAULT_CLOUD_URL = "https://gateway.productbrain.io";
86
89
  var McpCallError = class extends Error {
87
90
  status;
88
91
  code;
89
- constructor(message, status, code) {
92
+ /** WP-316 S1a: Structured commit validation — required field keys missing from entry.data. */
93
+ missingRequiredFields;
94
+ /** WP-316 S1a: Structured commit validation — field-level data errors. */
95
+ fieldErrors;
96
+ constructor(message, status, code, missingRequiredFields, fieldErrors) {
90
97
  super(message);
91
98
  this.name = "McpCallError";
92
99
  this.status = status;
93
100
  this.code = code;
101
+ this.missingRequiredFields = missingRequiredFields;
102
+ this.fieldErrors = fieldErrors;
94
103
  }
95
104
  };
96
105
  var CACHE_TTL_MS = 6e4;
@@ -327,7 +336,13 @@ async function mcpCall(fn, args = {}) {
327
336
  if (!res.ok || json.error) {
328
337
  const msg = json.error ?? "unknown error";
329
338
  audit(fn, "error", Date.now() - start, json.code ? `${msg} [${json.code}]` : msg);
330
- throw new McpCallError(`MCP call "${fn}" failed (${res.status}): ${msg}`, res.status, json.code);
339
+ throw new McpCallError(
340
+ `MCP call "${fn}" failed (${res.status}): ${msg}`,
341
+ res.status,
342
+ json.code,
343
+ Array.isArray(json.missingRequiredFields) ? json.missingRequiredFields : void 0,
344
+ Array.isArray(json.fieldErrors) ? json.fieldErrors : void 0
345
+ );
331
346
  }
332
347
  audit(fn, "ok", Date.now() - start);
333
348
  const data = json.data;
@@ -751,7 +766,13 @@ function asMcpCallError(err) {
751
766
  const code = err.code;
752
767
  const match = MCP_CALL_ERROR_RE.exec(err.message);
753
768
  if (match) {
754
- return { code, cleanMessage: match[1] };
769
+ const e = err;
770
+ return {
771
+ code,
772
+ cleanMessage: match[1],
773
+ missingRequiredFields: Array.isArray(e.missingRequiredFields) ? e.missingRequiredFields : void 0,
774
+ fieldErrors: Array.isArray(e.fieldErrors) ? e.fieldErrors : void 0
775
+ };
755
776
  }
756
777
  }
757
778
  return void 0;
@@ -818,9 +839,19 @@ function classifyError(err) {
818
839
  }
819
840
  const mcpErr = asMcpCallError(err);
820
841
  if (mcpErr) {
821
- const { code: convexCode, cleanMessage } = mcpErr;
842
+ const { code: convexCode, cleanMessage, missingRequiredFields, fieldErrors } = mcpErr;
822
843
  if (convexCode === "VALIDATION_FAILED") {
823
- return { code: "VALIDATION_ERROR", message: cleanMessage };
844
+ const hasDiagnostics = missingRequiredFields?.length || fieldErrors?.length;
845
+ return {
846
+ code: "VALIDATION_ERROR",
847
+ message: cleanMessage,
848
+ ...hasDiagnostics ? {
849
+ diagnostics: {
850
+ ...missingRequiredFields?.length ? { missingRequiredFields } : {},
851
+ ...fieldErrors?.length ? { fieldErrors } : {}
852
+ }
853
+ } : {}
854
+ };
824
855
  }
825
856
  if (convexCode === "NOT_FOUND") {
826
857
  return {
@@ -979,7 +1010,9 @@ function withEnvelope(handler) {
979
1010
  classified.code,
980
1011
  classified.message,
981
1012
  classified.recovery,
982
- classified.availableActions
1013
+ classified.availableActions,
1014
+ // WP-316 S1a: pass structured diagnostics (e.g. missingRequiredFields, fieldErrors)
1015
+ classified.diagnostics
983
1016
  ),
984
1017
  _meta: { durationMs }
985
1018
  };
@@ -1803,7 +1836,9 @@ var captureSchema = z2.object({
1803
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."
1804
1837
  ),
1805
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."),
1806
- 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.")
1807
1842
  });
1808
1843
  var batchCaptureSchema = z2.object({
1809
1844
  entries: z2.array(z2.object({
@@ -1818,7 +1853,9 @@ var batchCaptureSchema = z2.object({
1818
1853
  })).min(1).max(50).describe("Array of entries to capture"),
1819
1854
  autoCommit: z2.boolean().optional().describe(
1820
1855
  "If true, commits created entries immediately after linking. If omitted, Open mode workspaces commit by default and consensus/role modes stay draft-first."
1821
- )
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.")
1822
1859
  });
1823
1860
  var captureClassifierSchema = z2.object({
1824
1861
  enabled: z2.boolean(),
@@ -1832,7 +1869,9 @@ var captureClassifierSchema = z2.object({
1832
1869
  z2.object({
1833
1870
  collection: z2.string(),
1834
1871
  signalScore: z2.number().optional(),
1835
- 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()
1836
1875
  })
1837
1876
  ),
1838
1877
  agentProvidedCollection: z2.string().optional(),
@@ -1890,7 +1929,17 @@ function buildLowConfidenceResult(resolved, classifierMeta) {
1890
1929
  { collection: resolved.collection, confidence: resolved.confidence },
1891
1930
  ...resolved.alternatives
1892
1931
  ];
1893
- 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");
1894
1943
  const textBody = `Low confidence classification (${resolved.confidence}%, classified by ${resolved.classifiedBy}).
1895
1944
 
1896
1945
  Predicted: \`${resolved.collection}\`
@@ -1934,6 +1983,19 @@ function buildClassifierMeta(resolved, overrides = {}) {
1934
1983
  ...overrides
1935
1984
  };
1936
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
+ }
1937
1999
  function emitClassificationTelemetry(resolved, workspaceId, opts) {
1938
2000
  const topAlt = resolved.alternatives?.[0] ?? null;
1939
2001
  trackCollectionClassified(workspaceId, {
@@ -1969,6 +2031,7 @@ async function resolveCaptureCollection(params) {
1969
2031
  overrideCommand: `capture collection="${resolved2.collection}"`
1970
2032
  }
1971
2033
  });
2034
+ await enrichCandidatesWithFieldCounts(classifierMeta2);
1972
2035
  emitClassificationTelemetry(resolved2, workspaceId, {
1973
2036
  explicitCollectionProvided: true,
1974
2037
  autoRouted: false
@@ -1983,6 +2046,14 @@ async function resolveCaptureCollection(params) {
1983
2046
  explicitCollectionProvided: true,
1984
2047
  outcome: "fallback"
1985
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
+ });
1986
2057
  }
1987
2058
  return { resolvedCollection: collection, classifierMeta: classifierMeta2 };
1988
2059
  }
@@ -2019,6 +2090,7 @@ async function resolveCaptureCollection(params) {
2019
2090
  }
2020
2091
  const autoRoute = resolved.tier !== "low";
2021
2092
  const classifierMeta = buildClassifierMeta(resolved, { autoRouted: autoRoute });
2093
+ await enrichCandidatesWithFieldCounts(classifierMeta);
2022
2094
  emitClassificationTelemetry(resolved, workspaceId, {
2023
2095
  explicitCollectionProvided,
2024
2096
  autoRouted: autoRoute
@@ -2194,7 +2266,7 @@ function registerSmartCaptureTools(server) {
2194
2266
  inputSchema: captureSchema.shape,
2195
2267
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
2196
2268
  },
2197
- 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 }) => {
2198
2270
  requireWriteAccess();
2199
2271
  const timingStart = Date.now();
2200
2272
  const wsCtx = await getWorkspaceContext();
@@ -2353,8 +2425,33 @@ Or use \`collections action=list\` to see available collections.`
2353
2425
  createdBy: agentId ? `agent:${agentId}` : "capture",
2354
2426
  sessionId: agentId ?? void 0,
2355
2427
  ...sourceRef ? { sourceRef } : {},
2356
- ...sourceExcerpt ? { sourceExcerpt } : {}
2428
+ ...sourceExcerpt ? { sourceExcerpt } : {},
2429
+ ...preview ? { preview: true } : {}
2357
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
+ }
2358
2455
  internalId = result.docId;
2359
2456
  finalEntryId = result.entryId;
2360
2457
  entryWarnings.push(...result.warnings ?? []);
@@ -2436,9 +2533,9 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2436
2533
  if (linksSuggested.length >= MAX_SUGGESTIONS) break;
2437
2534
  if (autoTargetIds.has(c.entryId)) continue;
2438
2535
  if (c.confidence < 10) continue;
2439
- const preview = extractPreview(c.data, 80);
2536
+ const preview2 = extractPreview(c.data, 80);
2440
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`;
2441
- 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 });
2442
2539
  }
2443
2540
  conflictCandidates = candidates.filter((c) => c.entryId && c.entryId !== finalEntryId).slice(0, 3).map((c) => ({ entryId: c.entryId, name: c.name, collection: c.collSlug }));
2444
2541
  }
@@ -2669,8 +2766,8 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2669
2766
  lines.push("## Suggested links (review and use `relations action=create`)");
2670
2767
  for (let i = 0; i < linksSuggested.length; i++) {
2671
2768
  const s = linksSuggested[i];
2672
- const preview = s.preview ? ` \u2014 ${s.preview}` : "";
2673
- 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}`);
2674
2771
  }
2675
2772
  }
2676
2773
  lines.push("");
@@ -2806,6 +2903,16 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2806
2903
  type: f.type,
2807
2904
  ...f.required && { required: true }
2808
2905
  }));
2906
+ const captureContractForEnvelope = {
2907
+ requiredFields: (col.fields ?? []).filter((f) => f.required === true).map((f) => f.key),
2908
+ allFields: (col.fields ?? []).map((f) => ({
2909
+ key: f.key,
2910
+ required: f.required === true,
2911
+ type: f.type,
2912
+ ...f.helpText ? { helpText: f.helpText } : {}
2913
+ })),
2914
+ ...col.captureExpectation ? { captureExpectation: col.captureExpectation } : {}
2915
+ };
2809
2916
  const totalMs = Date.now() - timingStart;
2810
2917
  const timing = {
2811
2918
  classifyMs: tAfterClassify - timingStart,
@@ -2843,6 +2950,8 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2843
2950
  },
2844
2951
  next
2845
2952
  ),
2953
+ // WP-316 S1a: Populate contract field so agents know required fields upfront.
2954
+ contract: captureContractForEnvelope,
2846
2955
  _meta: { timing }
2847
2956
  }
2848
2957
  };
@@ -2858,7 +2967,7 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2858
2967
  inputSchema: batchCaptureSchema.shape,
2859
2968
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
2860
2969
  },
2861
- withEnvelope(async ({ entries, autoCommit }) => {
2970
+ withEnvelope(async ({ entries, autoCommit, preview }) => {
2862
2971
  requireWriteAccess();
2863
2972
  const batchTimingStart = Date.now();
2864
2973
  const agentId = getAgentSessionId();
@@ -3038,7 +3147,8 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
3038
3147
  sessionId: agentId ?? void 0,
3039
3148
  canonicalKey: entry.canonicalKey ?? void 0,
3040
3149
  ...entry.sourceRef ? { sourceRef: entry.sourceRef } : {},
3041
- ...entry.sourceExcerpt ? { sourceExcerpt: entry.sourceExcerpt } : {}
3150
+ ...entry.sourceExcerpt ? { sourceExcerpt: entry.sourceExcerpt } : {},
3151
+ ...preview ? { preview: true } : {}
3042
3152
  });
3043
3153
  const internalId = result.docId;
3044
3154
  const finalEntryId = result.entryId;
@@ -3625,7 +3735,9 @@ var getHistorySchema = z3.object({
3625
3735
  entryId: z3.string().describe("Entry ID, e.g. 'T-SUPPLIER', 'BR-001'")
3626
3736
  });
3627
3737
  var commitEntrySchema = z3.object({
3628
- 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.")
3629
3741
  });
3630
3742
  function registerKnowledgeTools(server) {
3631
3743
  const updateTool = server.registerTool(
@@ -3794,7 +3906,7 @@ ${formatted}` }],
3794
3906
  inputSchema: commitEntrySchema,
3795
3907
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
3796
3908
  },
3797
- withEnvelope(async ({ entryId }) => {
3909
+ withEnvelope(async ({ entryId, preview }) => {
3798
3910
  requireWriteAccess();
3799
3911
  const entry = await mcpQuery("chain.getEntry", { entryId });
3800
3912
  if (!entry) {
@@ -3852,11 +3964,55 @@ ${formatted}` }],
3852
3964
  });
3853
3965
  } catch {
3854
3966
  }
3855
- const result = await mcpMutation("chain.commitEntry", {
3856
- entryId,
3857
- author: getAgentSessionId() ? `agent:${getAgentSessionId()}` : void 0,
3858
- sessionId: getAgentSessionId() ?? void 0
3859
- });
3967
+ let result;
3968
+ try {
3969
+ result = await mcpMutation("chain.commitEntry", {
3970
+ entryId,
3971
+ author: getAgentSessionId() ? `agent:${getAgentSessionId()}` : void 0,
3972
+ sessionId: getAgentSessionId() ?? void 0,
3973
+ ...preview ? { preview: true } : {}
3974
+ });
3975
+ } catch (commitErr) {
3976
+ const errCode = commitErr?.code;
3977
+ if (errCode === "VALIDATION_FAILED") {
3978
+ try {
3979
+ const wsCtx2 = await getWorkspaceContext();
3980
+ const e = commitErr;
3981
+ trackCommitErrorByCode(wsCtx2.workspaceId, {
3982
+ error_code: errCode,
3983
+ missing_field_count: e.missingRequiredFields?.length ?? 0,
3984
+ field_error_count: e.fieldErrors?.length ?? 0,
3985
+ entry_id: entryId
3986
+ });
3987
+ } catch {
3988
+ }
3989
+ }
3990
+ throw commitErr;
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
+ }
3860
4016
  const docId = result?._id ?? entry._id;
3861
4017
  const wsCtx = await getWorkspaceContext();
3862
4018
  const isProposal = result?.status === "proposal_created";
@@ -3945,6 +4101,17 @@ ${formatted}` }],
3945
4101
  lines.push(` \u2192 ${epistemic.action}`);
3946
4102
  }
3947
4103
  }
4104
+ let captureContract;
4105
+ try {
4106
+ const collSlug = entry.collectionSlug ?? entry.collection?.slug;
4107
+ if (collSlug) {
4108
+ captureContract = await mcpQuery(
4109
+ "chain.getCaptureContract",
4110
+ { collectionSlug: collSlug }
4111
+ ) ?? void 0;
4112
+ }
4113
+ } catch {
4114
+ }
3948
4115
  const next = isProposal ? [
3949
4116
  { tool: "entries", description: "View entry", parameters: { action: "get", entryId } }
3950
4117
  ] : [
@@ -3952,21 +4119,25 @@ ${formatted}` }],
3952
4119
  { tool: "entries", description: "View committed entry", parameters: { action: "get", entryId } }
3953
4120
  ];
3954
4121
  const summary = isProposal ? `Proposal created for ${entryId} (${entry.name}). Awaiting consent.` : `Committed ${entryId} (${entry.name}) to the Chain.`;
4122
+ const commitSuccessEnvelope = success(
4123
+ summary,
4124
+ {
4125
+ entryId,
4126
+ name: entry.name,
4127
+ outcome: isProposal ? "proposal_created" : "committed",
4128
+ qualityVerdict: coachingResult?.verdict ?? void 0,
4129
+ source: coachingResult?.source ?? void 0,
4130
+ contradictions: advisoryWarnings.length,
4131
+ ...epistemic ? { epistemicStatus: epistemic } : {}
4132
+ },
4133
+ next
4134
+ );
4135
+ if (captureContract) {
4136
+ commitSuccessEnvelope.contract = captureContract;
4137
+ }
3955
4138
  return {
3956
4139
  content: [{ type: "text", text: lines.join("\n") }],
3957
- structuredContent: success(
3958
- summary,
3959
- {
3960
- entryId,
3961
- name: entry.name,
3962
- outcome: isProposal ? "proposal_created" : "committed",
3963
- qualityVerdict: coachingResult?.verdict ?? void 0,
3964
- source: coachingResult?.source ?? void 0,
3965
- contradictions: advisoryWarnings.length,
3966
- ...epistemic ? { epistemicStatus: epistemic } : {}
3967
- },
3968
- next
3969
- )
4140
+ structuredContent: commitSuccessEnvelope
3970
4141
  };
3971
4142
  })
3972
4143
  );
@@ -4716,7 +4887,9 @@ var relationsSchema = z6.object({
4716
4887
  from: z6.string(),
4717
4888
  to: z6.string(),
4718
4889
  type: z6.string()
4719
- })).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.")
4720
4893
  });
4721
4894
  function registerRelationsTools(server) {
4722
4895
  const tool = server.registerTool(
@@ -4730,13 +4903,13 @@ function registerRelationsTools(server) {
4730
4903
  withEnvelope(async (args) => {
4731
4904
  const parsed = parseOrFail(relationsSchema, args);
4732
4905
  if (!parsed.ok) return parsed.result;
4733
- const { action, from, to, type, score, relations } = parsed.data;
4906
+ const { action, from, to, type, score, relations, preview } = parsed.data;
4734
4907
  return runWithToolContext({ tool: "relations", action }, async () => {
4735
4908
  if (action === "create") {
4736
4909
  if (!from || !to || !type) {
4737
4910
  return validationResult("from, to, and type are required when action is 'create'.");
4738
4911
  }
4739
- return handleCreate(from, to, type, score);
4912
+ return handleCreate(from, to, type, score, preview);
4740
4913
  }
4741
4914
  if (action === "batch-create") {
4742
4915
  if (!relations || relations.length === 0) {
@@ -4762,7 +4935,7 @@ function registerRelationsTools(server) {
4762
4935
  );
4763
4936
  trackWriteTool(tool);
4764
4937
  }
4765
- async function handleCreate(from, to, type, score) {
4938
+ async function handleCreate(from, to, type, score, preview) {
4766
4939
  requireWriteAccess();
4767
4940
  const agentSessionId = getAgentSessionId();
4768
4941
  const result = await mcpMutation("chain.createEntryRelation", {
@@ -4770,8 +4943,31 @@ async function handleCreate(from, to, type, score) {
4770
4943
  toEntryId: to,
4771
4944
  type,
4772
4945
  suggestionScore: score ?? void 0,
4773
- sessionId: agentSessionId ?? void 0
4946
+ sessionId: agentSessionId ?? void 0,
4947
+ ...preview ? { preview: true } : {}
4774
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
+ }
4775
4971
  const wsCtx = await getWorkspaceContext();
4776
4972
  if (result?.status === "proposal_created") {
4777
4973
  const existingNote = result.existing ? " (existing proposal reused)" : "";
@@ -4802,11 +4998,12 @@ The relation was **not applied** \u2014 it will be created when the proposal is
4802
4998
  `**Workspace:** ${wsCtx.workspaceSlug} (${wsCtx.workspaceId})`
4803
4999
  ];
4804
5000
  const suggested = result?.suggestedType;
4805
- if (suggested) {
5001
+ const suggestedWithPosture = suggested ? { ...suggested, governancePosture: suggested.type === "governs" ? "requires_consent" : "direct" } : void 0;
5002
+ if (suggestedWithPosture) {
4806
5003
  lines.push("");
4807
- lines.push(`**Type suggestion:** \`${suggested.type}\` may be more precise (${suggested.confidence}% confidence).`);
4808
- lines.push(`_${suggested.reasoning}_`);
4809
- 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}"\``);
4810
5007
  }
4811
5008
  return {
4812
5009
  content: [{ type: "text", text: lines.join("\n") }],
@@ -4817,7 +5014,7 @@ The relation was **not applied** \u2014 it will be created when the proposal is
4817
5014
  from,
4818
5015
  to,
4819
5016
  type,
4820
- ...suggested && { suggestedType: suggested }
5017
+ ...suggestedWithPosture && { suggestedType: suggestedWithPosture }
4821
5018
  },
4822
5019
  [{ tool: "graph", description: "See connections", parameters: { action: "find", entryId: from } }]
4823
5020
  )
@@ -15052,6 +15249,67 @@ ${entry.labels.map((l) => `- ${l.name ?? l.slug}`).join("\n")}`);
15052
15249
  };
15053
15250
  }
15054
15251
  );
15252
+ server.resource(
15253
+ "chain-collection-capture-contract",
15254
+ new ResourceTemplate("productbrain://collections/{slug}/capture-contract", {
15255
+ list: async () => {
15256
+ const collections = await mcpQuery("chain.listCollections") ?? [];
15257
+ return {
15258
+ resources: collections.map((c) => ({
15259
+ uri: `productbrain://collections/${c.slug}/capture-contract`,
15260
+ name: `${c.icon ?? ""} ${c.name} \u2014 capture contract`.trim()
15261
+ }))
15262
+ };
15263
+ }
15264
+ }),
15265
+ async (uri, { slug }) => {
15266
+ const collectionSlug = slug;
15267
+ let workspaceId = null;
15268
+ try {
15269
+ const ctx = await getWorkspaceContext();
15270
+ workspaceId = ctx.workspaceId;
15271
+ } catch {
15272
+ }
15273
+ let contract = null;
15274
+ try {
15275
+ contract = await mcpQuery(
15276
+ "chain.getCaptureContract",
15277
+ { collectionSlug }
15278
+ );
15279
+ } catch {
15280
+ contract = null;
15281
+ }
15282
+ if (!contract) {
15283
+ if (workspaceId) {
15284
+ trackCaptureContractMiss(workspaceId, { slug: collectionSlug });
15285
+ }
15286
+ return {
15287
+ contents: [{
15288
+ uri: uri.href,
15289
+ mimeType: "application/json",
15290
+ text: JSON.stringify({
15291
+ ok: false,
15292
+ error: `Collection '${collectionSlug}' not found in this workspace.`,
15293
+ hint: "Use `collections action=list` to see available collections and their slugs."
15294
+ })
15295
+ }]
15296
+ };
15297
+ }
15298
+ const result = {
15299
+ collectionSlug,
15300
+ requiredFields: contract.requiredFields,
15301
+ allFields: contract.allFields,
15302
+ captureExpectation: contract.captureExpectation
15303
+ };
15304
+ return {
15305
+ contents: [{
15306
+ uri: uri.href,
15307
+ mimeType: "application/json",
15308
+ text: JSON.stringify(result)
15309
+ }]
15310
+ };
15311
+ }
15312
+ );
15055
15313
  server.resource(
15056
15314
  "chain-search",
15057
15315
  new ResourceTemplate("productbrain://search/{query}", {
@@ -15381,4 +15639,4 @@ export {
15381
15639
  SERVER_VERSION,
15382
15640
  createProductBrainServer
15383
15641
  };
15384
- //# sourceMappingURL=chunk-NRP3DF6E.js.map
15642
+ //# sourceMappingURL=chunk-LKUFRQUO.js.map