@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.
- package/dist/{chunk-NRP3DF6E.js → chunk-LKUFRQUO.js} +308 -50
- package/dist/chunk-LKUFRQUO.js.map +1 -0
- package/dist/{chunk-WYVQARYT.js → chunk-YMF3IQ5E.js} +52 -1
- package/dist/chunk-YMF3IQ5E.js.map +1 -0
- package/dist/cli/index.js +1 -1
- package/dist/http.js +2 -2
- package/dist/index.js +2 -2
- package/dist/{setup-CTBG5GQM.js → setup-BPZMFI56.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-NRP3DF6E.js.map +0 -1
- package/dist/chunk-WYVQARYT.js.map +0 -1
- /package/dist/{setup-CTBG5GQM.js.map → setup-BPZMFI56.js.map} +0 -0
|
@@ -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-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
2673
|
-
lines.push(`${i + 1}. **${s.entryId ?? "(no ID)"}**: ${s.name} [${s.collection}]${
|
|
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
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
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:
|
|
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
|
-
|
|
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:** \`${
|
|
4808
|
-
lines.push(`_${
|
|
4809
|
-
lines.push(`To use instead: \`relations action=create from="${from}" to="${to}" 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
|
-
...
|
|
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-
|
|
15642
|
+
//# sourceMappingURL=chunk-LKUFRQUO.js.map
|