@productbrain/mcp 0.0.1-beta.8 → 0.0.1-beta.81
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-G4JJNINW.js +3441 -0
- package/dist/chunk-G4JJNINW.js.map +1 -0
- package/dist/chunk-RQXM3TCI.js +261 -0
- package/dist/chunk-RQXM3TCI.js.map +1 -0
- package/dist/chunk-WXT35272.js +10144 -0
- package/dist/chunk-WXT35272.js.map +1 -0
- package/dist/cli/index.js +1 -1
- package/dist/http.js +443 -0
- package/dist/http.js.map +1 -0
- package/dist/index.js +62 -4374
- package/dist/index.js.map +1 -1
- package/dist/setup-GQ3LQS2L.js +297 -0
- package/dist/setup-GQ3LQS2L.js.map +1 -0
- package/dist/smart-capture-HRJL7SGD.js +41 -0
- package/dist/views/src/entry-cards/index.html +227 -0
- package/dist/views/src/graph-constellation/index.html +254 -0
- package/package.json +9 -3
- package/dist/chunk-6ZSCQINU.js +0 -1202
- package/dist/chunk-6ZSCQINU.js.map +0 -1
- package/dist/setup-42R6TBPH.js +0 -234
- package/dist/setup-42R6TBPH.js.map +0 -1
- package/dist/smart-capture-SEINMTTR.js +0 -13
- /package/dist/{smart-capture-SEINMTTR.js.map → smart-capture-HRJL7SGD.js.map} +0 -0
|
@@ -0,0 +1,3441 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MCP_NPX_PACKAGE,
|
|
3
|
+
trackCaptureClassifierAutoRouted,
|
|
4
|
+
trackCaptureClassifierEvaluated,
|
|
5
|
+
trackCaptureClassifierFallback,
|
|
6
|
+
trackQualityVerdict,
|
|
7
|
+
trackToolCall
|
|
8
|
+
} from "./chunk-RQXM3TCI.js";
|
|
9
|
+
|
|
10
|
+
// src/tools/smart-capture.ts
|
|
11
|
+
import { z as z2 } from "zod";
|
|
12
|
+
|
|
13
|
+
// src/client.ts
|
|
14
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
15
|
+
|
|
16
|
+
// src/auth.ts
|
|
17
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
18
|
+
var requestStore = new AsyncLocalStorage();
|
|
19
|
+
function runWithAuth(auth, fn) {
|
|
20
|
+
return requestStore.run(auth, fn);
|
|
21
|
+
}
|
|
22
|
+
function getRequestApiKey() {
|
|
23
|
+
return requestStore.getStore()?.apiKey;
|
|
24
|
+
}
|
|
25
|
+
var SESSION_TTL_MS = 30 * 60 * 1e3;
|
|
26
|
+
var MAX_KEYS = 100;
|
|
27
|
+
var keyStateMap = /* @__PURE__ */ new Map();
|
|
28
|
+
function newKeyState() {
|
|
29
|
+
return {
|
|
30
|
+
workspaceId: null,
|
|
31
|
+
workspaceSlug: null,
|
|
32
|
+
workspaceName: null,
|
|
33
|
+
workspaceCreatedAt: null,
|
|
34
|
+
workspaceGovernanceMode: null,
|
|
35
|
+
agentSessionId: null,
|
|
36
|
+
apiKeyId: null,
|
|
37
|
+
apiKeyScope: "readwrite",
|
|
38
|
+
sessionOriented: false,
|
|
39
|
+
sessionClosed: false,
|
|
40
|
+
lastAccess: Date.now()
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function getKeyState(apiKey) {
|
|
44
|
+
let s = keyStateMap.get(apiKey);
|
|
45
|
+
if (!s) {
|
|
46
|
+
s = newKeyState();
|
|
47
|
+
keyStateMap.set(apiKey, s);
|
|
48
|
+
evictStale();
|
|
49
|
+
}
|
|
50
|
+
s.lastAccess = Date.now();
|
|
51
|
+
return s;
|
|
52
|
+
}
|
|
53
|
+
function evictStale() {
|
|
54
|
+
if (keyStateMap.size <= MAX_KEYS) return;
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
for (const [key, s] of keyStateMap) {
|
|
57
|
+
if (now - s.lastAccess > SESSION_TTL_MS) keyStateMap.delete(key);
|
|
58
|
+
}
|
|
59
|
+
if (keyStateMap.size > MAX_KEYS) {
|
|
60
|
+
const sorted = [...keyStateMap.entries()].sort((a, b) => a[1].lastAccess - b[1].lastAccess);
|
|
61
|
+
for (let i = 0; i < sorted.length - MAX_KEYS; i++) {
|
|
62
|
+
keyStateMap.delete(sorted[i][0]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/client.ts
|
|
68
|
+
var toolContextStore = new AsyncLocalStorage2();
|
|
69
|
+
function runWithToolContext(ctx, fn) {
|
|
70
|
+
return toolContextStore.run(ctx, fn);
|
|
71
|
+
}
|
|
72
|
+
function getToolContext() {
|
|
73
|
+
return toolContextStore.getStore() ?? null;
|
|
74
|
+
}
|
|
75
|
+
var DEFAULT_CLOUD_URL = "https://trustworthy-kangaroo-277.convex.site";
|
|
76
|
+
var CACHE_TTL_MS = 6e4;
|
|
77
|
+
var CACHEABLE_FNS = [
|
|
78
|
+
"chain.getOrientEntries",
|
|
79
|
+
"chain.gatherContext",
|
|
80
|
+
"chain.graphGatherContext",
|
|
81
|
+
"chain.taskAwareGatherContext",
|
|
82
|
+
"chain.journeyAwareGatherContext",
|
|
83
|
+
"chain.assembleBuildContext"
|
|
84
|
+
];
|
|
85
|
+
function isCacheable(fn) {
|
|
86
|
+
return CACHEABLE_FNS.includes(fn);
|
|
87
|
+
}
|
|
88
|
+
var READ_PATTERN = /^(chain\.(get|list|search|batchGet|gather|graph|task|journey|assemble|workspace|score|absence)|chainwork\.(get|list|score)|maps\.(get|list)|gitchain\.(get|list|diff|history|runGate))/i;
|
|
89
|
+
function isWrite(fn) {
|
|
90
|
+
if (fn.startsWith("agent.")) return false;
|
|
91
|
+
return !READ_PATTERN.test(fn);
|
|
92
|
+
}
|
|
93
|
+
var readCache = /* @__PURE__ */ new Map();
|
|
94
|
+
function cacheKey(fn, args) {
|
|
95
|
+
return `${fn}:${JSON.stringify(args)}`;
|
|
96
|
+
}
|
|
97
|
+
function getCached(fn, args) {
|
|
98
|
+
if (!isCacheable(fn)) return void 0;
|
|
99
|
+
const key = cacheKey(fn, args);
|
|
100
|
+
const entry = readCache.get(key);
|
|
101
|
+
if (!entry || Date.now() > entry.expiresAt) {
|
|
102
|
+
if (entry) readCache.delete(key);
|
|
103
|
+
return void 0;
|
|
104
|
+
}
|
|
105
|
+
return entry.data;
|
|
106
|
+
}
|
|
107
|
+
function setCached(fn, args, data) {
|
|
108
|
+
if (!isCacheable(fn)) return;
|
|
109
|
+
const key = cacheKey(fn, args);
|
|
110
|
+
readCache.set(key, { data, expiresAt: Date.now() + CACHE_TTL_MS });
|
|
111
|
+
}
|
|
112
|
+
function invalidateReadCache() {
|
|
113
|
+
readCache.clear();
|
|
114
|
+
}
|
|
115
|
+
var _stdioState = {
|
|
116
|
+
workspaceId: null,
|
|
117
|
+
workspaceSlug: null,
|
|
118
|
+
workspaceName: null,
|
|
119
|
+
workspaceCreatedAt: null,
|
|
120
|
+
workspaceGovernanceMode: null,
|
|
121
|
+
agentSessionId: null,
|
|
122
|
+
apiKeyId: null,
|
|
123
|
+
apiKeyScope: "readwrite",
|
|
124
|
+
sessionOriented: false,
|
|
125
|
+
sessionClosed: false,
|
|
126
|
+
lastAccess: 0
|
|
127
|
+
};
|
|
128
|
+
function state() {
|
|
129
|
+
const reqKey = getRequestApiKey();
|
|
130
|
+
if (reqKey) return getKeyState(reqKey);
|
|
131
|
+
return _stdioState;
|
|
132
|
+
}
|
|
133
|
+
function getActiveApiKey() {
|
|
134
|
+
const fromRequest = getRequestApiKey();
|
|
135
|
+
if (fromRequest) return fromRequest;
|
|
136
|
+
const fromEnv = process.env.PRODUCTBRAIN_API_KEY;
|
|
137
|
+
if (!fromEnv) throw new Error("No API key available \u2014 set PRODUCTBRAIN_API_KEY or provide Bearer token");
|
|
138
|
+
return fromEnv;
|
|
139
|
+
}
|
|
140
|
+
function getAgentSessionId() {
|
|
141
|
+
return state().agentSessionId;
|
|
142
|
+
}
|
|
143
|
+
function isSessionOriented() {
|
|
144
|
+
return state().sessionOriented;
|
|
145
|
+
}
|
|
146
|
+
function setSessionOriented(value) {
|
|
147
|
+
state().sessionOriented = value;
|
|
148
|
+
}
|
|
149
|
+
function getApiKeyScope() {
|
|
150
|
+
return state().apiKeyScope;
|
|
151
|
+
}
|
|
152
|
+
async function startAgentSession() {
|
|
153
|
+
const workspaceId = await getWorkspaceId();
|
|
154
|
+
const s = state();
|
|
155
|
+
if (!s.apiKeyId) {
|
|
156
|
+
throw new Error("Cannot start session: API key ID not resolved. Ensure workspace resolution completed.");
|
|
157
|
+
}
|
|
158
|
+
const result = await mcpCall("agent.startSession", {
|
|
159
|
+
workspaceId,
|
|
160
|
+
apiKeyId: s.apiKeyId
|
|
161
|
+
});
|
|
162
|
+
if (s.agentSessionId) {
|
|
163
|
+
resetTouchThrottle(s.agentSessionId);
|
|
164
|
+
}
|
|
165
|
+
s.agentSessionId = result.sessionId;
|
|
166
|
+
s.apiKeyScope = result.toolsScope;
|
|
167
|
+
s.sessionOriented = false;
|
|
168
|
+
s.sessionClosed = false;
|
|
169
|
+
resetTouchThrottle(result.sessionId);
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
async function closeAgentSession() {
|
|
173
|
+
const s = state();
|
|
174
|
+
if (!s.agentSessionId) return;
|
|
175
|
+
const sessionId = s.agentSessionId;
|
|
176
|
+
try {
|
|
177
|
+
await mcpCall("agent.closeSession", {
|
|
178
|
+
sessionId,
|
|
179
|
+
status: "closed"
|
|
180
|
+
});
|
|
181
|
+
} finally {
|
|
182
|
+
resetTouchThrottle(sessionId);
|
|
183
|
+
s.sessionClosed = true;
|
|
184
|
+
s.agentSessionId = null;
|
|
185
|
+
s.sessionOriented = false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function orphanAgentSession() {
|
|
189
|
+
const s = state();
|
|
190
|
+
if (!s.agentSessionId) return;
|
|
191
|
+
const sessionId = s.agentSessionId;
|
|
192
|
+
try {
|
|
193
|
+
await mcpCall("agent.closeSession", {
|
|
194
|
+
sessionId,
|
|
195
|
+
status: "orphaned"
|
|
196
|
+
});
|
|
197
|
+
} catch {
|
|
198
|
+
} finally {
|
|
199
|
+
resetTouchThrottle(sessionId);
|
|
200
|
+
s.agentSessionId = null;
|
|
201
|
+
s.sessionOriented = false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
var _lastTouchAtBySession = /* @__PURE__ */ new Map();
|
|
205
|
+
var TOUCH_THROTTLE_MS = 5e3;
|
|
206
|
+
function touchSessionActivity() {
|
|
207
|
+
const s = state();
|
|
208
|
+
const sessionId = s.agentSessionId;
|
|
209
|
+
if (!sessionId) return;
|
|
210
|
+
const now = Date.now();
|
|
211
|
+
const lastTouchAt = _lastTouchAtBySession.get(sessionId) ?? 0;
|
|
212
|
+
if (now - lastTouchAt < TOUCH_THROTTLE_MS) return;
|
|
213
|
+
_lastTouchAtBySession.set(sessionId, now);
|
|
214
|
+
mcpCall("agent.touchSession", {
|
|
215
|
+
sessionId
|
|
216
|
+
}).catch(() => {
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
function resetTouchThrottle(sessionId) {
|
|
220
|
+
if (sessionId) {
|
|
221
|
+
_lastTouchAtBySession.delete(sessionId);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
_lastTouchAtBySession.clear();
|
|
225
|
+
}
|
|
226
|
+
async function recordSessionActivity(activity) {
|
|
227
|
+
const s = state();
|
|
228
|
+
if (!s.agentSessionId) return;
|
|
229
|
+
try {
|
|
230
|
+
await mcpCall("agent.recordActivity", {
|
|
231
|
+
sessionId: s.agentSessionId,
|
|
232
|
+
...activity
|
|
233
|
+
});
|
|
234
|
+
} catch {
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
var AUDIT_BUFFER_SIZE = 50;
|
|
238
|
+
var auditBuffer = [];
|
|
239
|
+
function bootstrap() {
|
|
240
|
+
process.env.CONVEX_SITE_URL ??= process.env.PRODUCTBRAIN_URL ?? DEFAULT_CLOUD_URL;
|
|
241
|
+
const pbKey = process.env.PRODUCTBRAIN_API_KEY;
|
|
242
|
+
if (!pbKey?.startsWith("pb_sk_")) {
|
|
243
|
+
process.stderr.write(
|
|
244
|
+
"[MCP] Warning: PRODUCTBRAIN_API_KEY is not set or invalid. Tool calls will fail until a valid key is provided.\n"
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function bootstrapHttp() {
|
|
249
|
+
process.env.CONVEX_SITE_URL ??= process.env.PRODUCTBRAIN_URL ?? DEFAULT_CLOUD_URL;
|
|
250
|
+
}
|
|
251
|
+
function getEnv(key) {
|
|
252
|
+
const value = process.env[key];
|
|
253
|
+
if (!value) throw new Error(`${key} environment variable is required`);
|
|
254
|
+
return value;
|
|
255
|
+
}
|
|
256
|
+
function shouldLogAudit(status) {
|
|
257
|
+
return status === "error" || process.env.MCP_DEBUG === "1";
|
|
258
|
+
}
|
|
259
|
+
function audit(fn, status, durationMs, errorMsg) {
|
|
260
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
261
|
+
const workspace = state().workspaceId ?? "unresolved";
|
|
262
|
+
const toolCtx = getToolContext();
|
|
263
|
+
const entry = { ts, fn, workspace, status, durationMs };
|
|
264
|
+
if (errorMsg) entry.error = errorMsg;
|
|
265
|
+
if (toolCtx) entry.toolContext = toolCtx;
|
|
266
|
+
auditBuffer.push(entry);
|
|
267
|
+
if (auditBuffer.length > AUDIT_BUFFER_SIZE) auditBuffer.shift();
|
|
268
|
+
trackToolCall(fn, status, durationMs, workspace, errorMsg);
|
|
269
|
+
if (!shouldLogAudit(status)) return;
|
|
270
|
+
const base = `[MCP-AUDIT] ${ts} fn=${fn} workspace=${workspace} status=${status} duration=${durationMs}ms`;
|
|
271
|
+
if (status === "error" && errorMsg) {
|
|
272
|
+
process.stderr.write(`${base} error=${JSON.stringify(errorMsg)}
|
|
273
|
+
`);
|
|
274
|
+
} else {
|
|
275
|
+
process.stderr.write(`${base}
|
|
276
|
+
`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function getAuditLog() {
|
|
280
|
+
return auditBuffer;
|
|
281
|
+
}
|
|
282
|
+
async function mcpCall(fn, args = {}) {
|
|
283
|
+
const cached = getCached(fn, args);
|
|
284
|
+
if (cached !== void 0) {
|
|
285
|
+
return cached;
|
|
286
|
+
}
|
|
287
|
+
const siteUrl = getEnv("CONVEX_SITE_URL").replace(/\/$/, "");
|
|
288
|
+
const apiKey = getActiveApiKey();
|
|
289
|
+
const start = Date.now();
|
|
290
|
+
let res;
|
|
291
|
+
try {
|
|
292
|
+
res = await fetch(`${siteUrl}/api/mcp`, {
|
|
293
|
+
method: "POST",
|
|
294
|
+
signal: AbortSignal.timeout(1e4),
|
|
295
|
+
headers: {
|
|
296
|
+
"Content-Type": "application/json",
|
|
297
|
+
Authorization: `Bearer ${apiKey}`
|
|
298
|
+
},
|
|
299
|
+
body: JSON.stringify({ fn, args })
|
|
300
|
+
});
|
|
301
|
+
} catch (err) {
|
|
302
|
+
audit(fn, "error", Date.now() - start, err.message);
|
|
303
|
+
throw new Error(`MCP call "${fn}" network error: ${err.message}`);
|
|
304
|
+
}
|
|
305
|
+
const json = await res.json();
|
|
306
|
+
if (!res.ok || json.error) {
|
|
307
|
+
audit(fn, "error", Date.now() - start, json.error);
|
|
308
|
+
throw new Error(`MCP call "${fn}" failed (${res.status}): ${json.error ?? "unknown error"}`);
|
|
309
|
+
}
|
|
310
|
+
audit(fn, "ok", Date.now() - start);
|
|
311
|
+
const data = json.data;
|
|
312
|
+
if (isWrite(fn)) {
|
|
313
|
+
invalidateReadCache();
|
|
314
|
+
} else {
|
|
315
|
+
setCached(fn, args, data);
|
|
316
|
+
}
|
|
317
|
+
const s = state();
|
|
318
|
+
const TOUCH_EXCLUDED = /* @__PURE__ */ new Set([
|
|
319
|
+
"agent.touchSession",
|
|
320
|
+
"agent.startSession",
|
|
321
|
+
"agent.markOriented",
|
|
322
|
+
"agent.recordActivity",
|
|
323
|
+
"agent.recordWrapup",
|
|
324
|
+
"agent.closeSession"
|
|
325
|
+
]);
|
|
326
|
+
if (s.agentSessionId && !TOUCH_EXCLUDED.has(fn)) {
|
|
327
|
+
touchSessionActivity();
|
|
328
|
+
}
|
|
329
|
+
return data;
|
|
330
|
+
}
|
|
331
|
+
var resolveInFlightMap = /* @__PURE__ */ new Map();
|
|
332
|
+
async function getWorkspaceId() {
|
|
333
|
+
const s = state();
|
|
334
|
+
if (s.workspaceId) return s.workspaceId;
|
|
335
|
+
const apiKey = getActiveApiKey();
|
|
336
|
+
const existing = resolveInFlightMap.get(apiKey);
|
|
337
|
+
if (existing) return existing;
|
|
338
|
+
const promise = resolveWorkspaceWithRetry().finally(() => resolveInFlightMap.delete(apiKey));
|
|
339
|
+
resolveInFlightMap.set(apiKey, promise);
|
|
340
|
+
return promise;
|
|
341
|
+
}
|
|
342
|
+
async function resolveWorkspaceWithRetry(maxRetries = 2) {
|
|
343
|
+
let lastError = null;
|
|
344
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
345
|
+
try {
|
|
346
|
+
const workspace = await mcpCall("resolveWorkspace", {});
|
|
347
|
+
if (!workspace) {
|
|
348
|
+
throw new Error(
|
|
349
|
+
`API key is valid but no workspace is associated. Run \`npx ${MCP_NPX_PACKAGE} setup\` or regenerate your key.`
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
const s = state();
|
|
353
|
+
s.workspaceId = workspace._id;
|
|
354
|
+
s.workspaceSlug = workspace.slug;
|
|
355
|
+
s.workspaceName = workspace.name;
|
|
356
|
+
s.workspaceCreatedAt = workspace.createdAt ?? null;
|
|
357
|
+
s.workspaceGovernanceMode = workspace.governanceMode ?? "open";
|
|
358
|
+
if (workspace.keyScope) s.apiKeyScope = workspace.keyScope;
|
|
359
|
+
if (workspace.keyId) s.apiKeyId = workspace.keyId;
|
|
360
|
+
return s.workspaceId;
|
|
361
|
+
} catch (err) {
|
|
362
|
+
lastError = err;
|
|
363
|
+
const isTransient = /network error|fetch failed|ECONNREFUSED|ETIMEDOUT/i.test(err.message);
|
|
364
|
+
if (!isTransient || attempt === maxRetries) break;
|
|
365
|
+
const delay = 1e3 * (attempt + 1);
|
|
366
|
+
process.stderr.write(
|
|
367
|
+
`[MCP] Workspace resolution failed (attempt ${attempt + 1}/${maxRetries + 1}), retrying in ${delay}ms...
|
|
368
|
+
`
|
|
369
|
+
);
|
|
370
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
throw lastError;
|
|
374
|
+
}
|
|
375
|
+
async function getWorkspaceContext() {
|
|
376
|
+
const workspaceId = await getWorkspaceId();
|
|
377
|
+
const s = state();
|
|
378
|
+
return {
|
|
379
|
+
workspaceId,
|
|
380
|
+
workspaceSlug: s.workspaceSlug ?? "unknown",
|
|
381
|
+
workspaceName: s.workspaceName ?? "unknown",
|
|
382
|
+
createdAt: s.workspaceCreatedAt,
|
|
383
|
+
governanceMode: s.workspaceGovernanceMode ?? "open"
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
async function mcpQuery(fn, args = {}) {
|
|
387
|
+
const workspaceId = await getWorkspaceId();
|
|
388
|
+
return mcpCall(fn, { ...args, workspaceId });
|
|
389
|
+
}
|
|
390
|
+
async function mcpMutation(fn, args = {}) {
|
|
391
|
+
const workspaceId = await getWorkspaceId();
|
|
392
|
+
return mcpCall(fn, { ...args, workspaceId });
|
|
393
|
+
}
|
|
394
|
+
function requireActiveSession() {
|
|
395
|
+
const s = state();
|
|
396
|
+
if (!s.agentSessionId) {
|
|
397
|
+
throw new Error(
|
|
398
|
+
"Active session required (SOS-iszqu7). Call `session action=start` then `orient` first."
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
if (s.sessionClosed) {
|
|
402
|
+
throw new Error(
|
|
403
|
+
"Session has been closed (SOS-iszqu7). Start a new session with `session action=start`."
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
if (!s.sessionOriented) {
|
|
407
|
+
throw new Error(
|
|
408
|
+
"Orientation required before accessing build context (SOS-iszqu7). Call `orient` first."
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function requireWriteAccess() {
|
|
413
|
+
const s = state();
|
|
414
|
+
if (!s.agentSessionId) {
|
|
415
|
+
throw new Error(
|
|
416
|
+
"Agent session required for write operations. Call `session action=start` first."
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
if (s.sessionClosed) {
|
|
420
|
+
throw new Error(
|
|
421
|
+
"Agent session has been closed. Write tools are no longer available."
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
if (!s.sessionOriented) {
|
|
425
|
+
throw new Error(
|
|
426
|
+
"Orientation required before writing to the Chain. Call 'orient' first."
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
if (s.apiKeyScope === "read") {
|
|
430
|
+
throw new Error(
|
|
431
|
+
"This API key has read-only scope. Write tools are not available."
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
async function recoverSessionState() {
|
|
436
|
+
const s = state();
|
|
437
|
+
if (!s.workspaceId) return;
|
|
438
|
+
try {
|
|
439
|
+
const session = await mcpCall("agent.getActiveSession", { workspaceId: s.workspaceId });
|
|
440
|
+
if (session && session.status === "active") {
|
|
441
|
+
s.agentSessionId = session._id;
|
|
442
|
+
s.sessionOriented = session.oriented;
|
|
443
|
+
s.apiKeyScope = session.toolsScope;
|
|
444
|
+
s.sessionClosed = false;
|
|
445
|
+
}
|
|
446
|
+
} catch {
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/tools/knowledge-helpers.ts
|
|
451
|
+
var EPISTEMIC_COLLECTIONS = /* @__PURE__ */ new Set(["insights", "assumptions"]);
|
|
452
|
+
function deriveEpistemicStatus(entry) {
|
|
453
|
+
const coll = entry.collectionName?.toLowerCase();
|
|
454
|
+
if (!coll || !EPISTEMIC_COLLECTIONS.has(coll)) return null;
|
|
455
|
+
const relations = entry.relations ?? [];
|
|
456
|
+
const hasValidates = relations.some(
|
|
457
|
+
(r) => r.type === "validates" && r.direction === "incoming"
|
|
458
|
+
);
|
|
459
|
+
const hasInvalidates = relations.some(
|
|
460
|
+
(r) => r.type === "invalidates" && r.direction === "incoming"
|
|
461
|
+
);
|
|
462
|
+
const evidenceCount = relations.filter(
|
|
463
|
+
(r) => (r.type === "validates" || r.type === "invalidates") && r.direction === "incoming"
|
|
464
|
+
).length;
|
|
465
|
+
if (coll === "assumptions") {
|
|
466
|
+
const ws2 = entry.workflowStatus;
|
|
467
|
+
if (ws2 === "invalidated" || hasInvalidates) {
|
|
468
|
+
return { level: "invalidated", reason: "Disproved by linked counter-evidence" };
|
|
469
|
+
}
|
|
470
|
+
if (ws2 === "validated" || hasValidates && ws2 !== "untested") {
|
|
471
|
+
return { level: "validated", reason: `${evidenceCount} evidence link${evidenceCount !== 1 ? "s" : ""}` };
|
|
472
|
+
}
|
|
473
|
+
if (ws2 === "testing") {
|
|
474
|
+
return { level: "testing", reason: "Experiment in progress" };
|
|
475
|
+
}
|
|
476
|
+
return { level: "untested", reason: "No evidence linked", action: 'Link evidence via `relations action=create type="validates"`' };
|
|
477
|
+
}
|
|
478
|
+
const ws = entry.workflowStatus;
|
|
479
|
+
if (ws === "validated" && hasValidates) {
|
|
480
|
+
return { level: "validated", reason: `${evidenceCount} evidence link${evidenceCount !== 1 ? "s" : ""}` };
|
|
481
|
+
}
|
|
482
|
+
if (ws === "evidenced" || hasValidates) {
|
|
483
|
+
return { level: "evidenced", reason: `${evidenceCount} evidence link${evidenceCount !== 1 ? "s" : ""}` };
|
|
484
|
+
}
|
|
485
|
+
return {
|
|
486
|
+
level: "hypothesis",
|
|
487
|
+
reason: "No linked evidence",
|
|
488
|
+
action: 'Link proof via `relations action=create type="validates"`'
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
function formatEpistemicLine(es) {
|
|
492
|
+
const icon = es.level === "validated" ? "\u2713" : es.level === "evidenced" ? "\u25CE" : es.level === "hypothesis" ? "\u25B3" : es.level === "untested" ? "?" : es.level === "testing" ? "\u25CE" : "\u2715";
|
|
493
|
+
const suffix = es.action ? ` ${es.action}` : "";
|
|
494
|
+
return `**Confidence:** ${icon} ${es.level} \u2014 ${es.reason}.${suffix}`;
|
|
495
|
+
}
|
|
496
|
+
function toEpistemicInput(entry) {
|
|
497
|
+
return {
|
|
498
|
+
collectionName: typeof entry.collectionName === "string" ? entry.collectionName : void 0,
|
|
499
|
+
workflowStatus: typeof entry.workflowStatus === "string" ? entry.workflowStatus : void 0,
|
|
500
|
+
relations: Array.isArray(entry.relations) ? entry.relations.map((r) => ({ type: r.type, direction: r.direction })) : void 0
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
function extractPreview(data, maxLen) {
|
|
504
|
+
if (!data || typeof data !== "object") return "";
|
|
505
|
+
const d = data;
|
|
506
|
+
const raw = d.description ?? d.canonical ?? d.detail ?? d.rule ?? "";
|
|
507
|
+
if (typeof raw !== "string" || !raw) return "";
|
|
508
|
+
return raw.length > maxLen ? raw.substring(0, maxLen) + "..." : raw;
|
|
509
|
+
}
|
|
510
|
+
var TOOL_NAME_MIGRATIONS = /* @__PURE__ */ new Map([
|
|
511
|
+
["list-entries", 'entries action="list"'],
|
|
512
|
+
["get-entry", 'entries action="get"'],
|
|
513
|
+
["batch-get", 'entries action="batch"'],
|
|
514
|
+
["search", 'entries action="search"'],
|
|
515
|
+
["relate-entries", 'relations action="create"'],
|
|
516
|
+
["batch-relate", 'relations action="batch-create"'],
|
|
517
|
+
["find-related", 'graph action="find"'],
|
|
518
|
+
["suggest-links", 'graph action="suggest"'],
|
|
519
|
+
["gather-context", 'context action="gather"'],
|
|
520
|
+
["get-build-context", 'context action="build"'],
|
|
521
|
+
["list-collections", 'collections action="list"'],
|
|
522
|
+
["create-collection", 'collections action="create"'],
|
|
523
|
+
["update-collection", 'collections action="update"'],
|
|
524
|
+
["agent-start", 'session action="start"'],
|
|
525
|
+
["agent-close", 'session action="close"'],
|
|
526
|
+
["agent-status", 'session action="status"'],
|
|
527
|
+
["workspace-status", 'health action="status"'],
|
|
528
|
+
["mcp-audit", 'health action="audit"'],
|
|
529
|
+
["quality-check", 'quality action="check"'],
|
|
530
|
+
["re-evaluate", 'quality action="re-evaluate"'],
|
|
531
|
+
["list-workflows", 'workflows action="list"'],
|
|
532
|
+
["workflow-checkpoint", 'workflows action="checkpoint"'],
|
|
533
|
+
["wrapup", "session-wrapup"],
|
|
534
|
+
["finish", "session-wrapup"]
|
|
535
|
+
]);
|
|
536
|
+
function translateStaleToolNames(text) {
|
|
537
|
+
const found = [];
|
|
538
|
+
for (const [old, current] of TOOL_NAME_MIGRATIONS) {
|
|
539
|
+
const pattern = new RegExp(`\\b${old.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "g");
|
|
540
|
+
if (pattern.test(text)) {
|
|
541
|
+
found.push(`\`${old}\` \u2192 \`${current}\``);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (found.length === 0) return null;
|
|
545
|
+
return `
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
_Tool name translations (these references use deprecated names):_
|
|
549
|
+
${found.map((f) => `- ${f}`).join("\n")}`;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// src/tool-surface.ts
|
|
553
|
+
function initToolSurface(_server) {
|
|
554
|
+
}
|
|
555
|
+
function trackWriteTool(_tool) {
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// src/featureFlags.ts
|
|
559
|
+
import { PostHog } from "posthog-node";
|
|
560
|
+
var client = null;
|
|
561
|
+
var LOCAL_OVERRIDES = {
|
|
562
|
+
// Uncomment for local dev without PostHog:
|
|
563
|
+
// "workspace-full-surface": false,
|
|
564
|
+
// "chainwork-enabled": true,
|
|
565
|
+
// "active-intelligence-shaping": true, // ENT-59: Opus-powered investigation during shaping
|
|
566
|
+
// "capture-without-thinking": true, // BET-73: collection-optional capture classifier
|
|
567
|
+
};
|
|
568
|
+
function initFeatureFlags(posthogClient) {
|
|
569
|
+
if (posthogClient) {
|
|
570
|
+
client = posthogClient;
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const apiKey = process.env.POSTHOG_MCP_KEY || process.env.PUBLIC_POSTHOG_KEY;
|
|
574
|
+
if (!apiKey) {
|
|
575
|
+
client = null;
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
client = new PostHog(apiKey, {
|
|
579
|
+
host: process.env.PUBLIC_POSTHOG_HOST || "https://eu.i.posthog.com",
|
|
580
|
+
flushAt: 1,
|
|
581
|
+
flushInterval: 5e3,
|
|
582
|
+
featureFlagsPollingInterval: 3e4
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
async function isFeatureEnabled(flag, workspaceId, workspaceSlug) {
|
|
586
|
+
if (process.env.FEATURE_KILL_SWITCH === "true") return false;
|
|
587
|
+
if (flag in LOCAL_OVERRIDES) return LOCAL_OVERRIDES[flag];
|
|
588
|
+
if (!client) return false;
|
|
589
|
+
try {
|
|
590
|
+
const primary = await client.isFeatureEnabled(flag, workspaceId, {
|
|
591
|
+
groups: { workspace: workspaceId },
|
|
592
|
+
groupProperties: {
|
|
593
|
+
workspace: {
|
|
594
|
+
workspace_id: workspaceId,
|
|
595
|
+
slug: workspaceSlug ?? ""
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
if (primary) return true;
|
|
600
|
+
if (workspaceSlug && workspaceSlug !== workspaceId) {
|
|
601
|
+
const secondary = await client.isFeatureEnabled(flag, workspaceSlug, {
|
|
602
|
+
groups: { workspace: workspaceSlug },
|
|
603
|
+
groupProperties: {
|
|
604
|
+
workspace: {
|
|
605
|
+
workspace_id: workspaceId,
|
|
606
|
+
slug: workspaceSlug
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
return secondary ?? false;
|
|
611
|
+
}
|
|
612
|
+
return primary ?? false;
|
|
613
|
+
} catch {
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// src/gap-store.ts
|
|
619
|
+
var SESSION_GAPS = [];
|
|
620
|
+
var DEDUP_WINDOW_MS = 6e4;
|
|
621
|
+
function isDuplicate(gap) {
|
|
622
|
+
const cutoff = gap.timestamp - DEDUP_WINDOW_MS;
|
|
623
|
+
return SESSION_GAPS.some(
|
|
624
|
+
(existing) => existing.query === gap.query && existing.tool === gap.tool && existing.action === gap.action && existing.timestamp >= cutoff
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
function recordGap(gap) {
|
|
628
|
+
const timestamped = { ...gap, timestamp: Date.now() };
|
|
629
|
+
if (isDuplicate(timestamped)) return false;
|
|
630
|
+
SESSION_GAPS.push(timestamped);
|
|
631
|
+
persistGap(gap);
|
|
632
|
+
return true;
|
|
633
|
+
}
|
|
634
|
+
function persistGap(gap) {
|
|
635
|
+
const sessionId = getAgentSessionId();
|
|
636
|
+
mcpMutation("gaps.record", {
|
|
637
|
+
sessionId: sessionId ?? void 0,
|
|
638
|
+
query: gap.query,
|
|
639
|
+
tool: gap.tool,
|
|
640
|
+
action: gap.action,
|
|
641
|
+
gapType: gap.gapType,
|
|
642
|
+
collectionScope: gap.collectionScope
|
|
643
|
+
}).catch(() => {
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
function getSessionGaps() {
|
|
647
|
+
return SESSION_GAPS;
|
|
648
|
+
}
|
|
649
|
+
function getTopGaps(limit = 5) {
|
|
650
|
+
const counts = /* @__PURE__ */ new Map();
|
|
651
|
+
for (const gap of SESSION_GAPS) {
|
|
652
|
+
const key = gap.query.toLowerCase().trim();
|
|
653
|
+
const existing = counts.get(key);
|
|
654
|
+
if (existing) {
|
|
655
|
+
existing.count++;
|
|
656
|
+
} else {
|
|
657
|
+
counts.set(key, { count: 1, gapType: gap.gapType });
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return [...counts.entries()].map(([query, { count, gapType }]) => ({ query, count, gapType })).sort((a, b) => b.count - a.count).slice(0, limit);
|
|
661
|
+
}
|
|
662
|
+
function clearSessionGaps() {
|
|
663
|
+
SESSION_GAPS.length = 0;
|
|
664
|
+
}
|
|
665
|
+
function resolveGapsForEntry(entryName, entryId) {
|
|
666
|
+
mcpMutation("gaps.resolve", { entryName, entryId }).catch(() => {
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// src/lib/collectionRoutingClassifier.ts
|
|
671
|
+
var CLASSIFIER_AUTO_ROUTE_THRESHOLD = 70;
|
|
672
|
+
var CLASSIFIER_AMBIGUITY_MARGIN = 15;
|
|
673
|
+
var CLASSIFIABLE_COLLECTIONS = [
|
|
674
|
+
"decisions",
|
|
675
|
+
"tensions",
|
|
676
|
+
"glossary",
|
|
677
|
+
"insights",
|
|
678
|
+
"features",
|
|
679
|
+
"architecture",
|
|
680
|
+
"business-rules",
|
|
681
|
+
"tracking-events",
|
|
682
|
+
"landscape",
|
|
683
|
+
"standards",
|
|
684
|
+
"principles",
|
|
685
|
+
"assumptions"
|
|
686
|
+
];
|
|
687
|
+
var SIGNAL_WEIGHT = 10;
|
|
688
|
+
var MIN_SCORE_FLOOR = 10;
|
|
689
|
+
var MAX_MATCHES_PER_SIGNAL = 2;
|
|
690
|
+
var MAX_REASON_COUNT = 3;
|
|
691
|
+
var ENTRY_ID_PATTERN = /\b[A-Z]{2,}-\d+\b/g;
|
|
692
|
+
var COLLECTION_SIGNALS = {
|
|
693
|
+
decisions: [
|
|
694
|
+
"decide",
|
|
695
|
+
"decision",
|
|
696
|
+
"chose",
|
|
697
|
+
"chosen",
|
|
698
|
+
"choice",
|
|
699
|
+
"resolved",
|
|
700
|
+
"we will",
|
|
701
|
+
"we should",
|
|
702
|
+
"approved",
|
|
703
|
+
"replaces",
|
|
704
|
+
"instead of",
|
|
705
|
+
"go with",
|
|
706
|
+
"criteria",
|
|
707
|
+
"adopted",
|
|
708
|
+
"reposition",
|
|
709
|
+
"scoring framework",
|
|
710
|
+
"review"
|
|
711
|
+
],
|
|
712
|
+
tensions: [
|
|
713
|
+
"problem",
|
|
714
|
+
"issue",
|
|
715
|
+
"blocked",
|
|
716
|
+
"blocker",
|
|
717
|
+
"friction",
|
|
718
|
+
"pain",
|
|
719
|
+
"bottleneck",
|
|
720
|
+
"struggle",
|
|
721
|
+
"missing",
|
|
722
|
+
"breaks",
|
|
723
|
+
"regression",
|
|
724
|
+
"unclear",
|
|
725
|
+
"no way to",
|
|
726
|
+
"scope creep",
|
|
727
|
+
"coupled",
|
|
728
|
+
"trapped",
|
|
729
|
+
"ambiguous",
|
|
730
|
+
"no batch",
|
|
731
|
+
"undetectable",
|
|
732
|
+
"coordination gap"
|
|
733
|
+
],
|
|
734
|
+
glossary: [
|
|
735
|
+
"definition",
|
|
736
|
+
"define",
|
|
737
|
+
"term",
|
|
738
|
+
"means",
|
|
739
|
+
"refers to",
|
|
740
|
+
"is called",
|
|
741
|
+
"vocabulary",
|
|
742
|
+
"terminology",
|
|
743
|
+
"a governance mechanism",
|
|
744
|
+
"a workspace",
|
|
745
|
+
"a tracked",
|
|
746
|
+
"the atom",
|
|
747
|
+
"the action of",
|
|
748
|
+
"the versioned",
|
|
749
|
+
"a field on",
|
|
750
|
+
"one of the",
|
|
751
|
+
"a constraint on",
|
|
752
|
+
"a hard data",
|
|
753
|
+
"a single"
|
|
754
|
+
],
|
|
755
|
+
insights: [
|
|
756
|
+
"insight",
|
|
757
|
+
"learned",
|
|
758
|
+
"observed",
|
|
759
|
+
"trend",
|
|
760
|
+
"found that",
|
|
761
|
+
"discovery",
|
|
762
|
+
"validates",
|
|
763
|
+
"saturates",
|
|
764
|
+
"convergence",
|
|
765
|
+
"signals from",
|
|
766
|
+
"converge",
|
|
767
|
+
"tam"
|
|
768
|
+
],
|
|
769
|
+
features: [
|
|
770
|
+
"feature",
|
|
771
|
+
"capability",
|
|
772
|
+
"user can",
|
|
773
|
+
"navigation",
|
|
774
|
+
"palette",
|
|
775
|
+
"modal",
|
|
776
|
+
"smart capture",
|
|
777
|
+
"suggest-links",
|
|
778
|
+
"command palette",
|
|
779
|
+
"auto-commit",
|
|
780
|
+
"collection-optional",
|
|
781
|
+
"organisation intelligence",
|
|
782
|
+
"consolidation"
|
|
783
|
+
],
|
|
784
|
+
architecture: [
|
|
785
|
+
"architecture",
|
|
786
|
+
"layer",
|
|
787
|
+
"data model",
|
|
788
|
+
"infrastructure",
|
|
789
|
+
"system design",
|
|
790
|
+
"l1",
|
|
791
|
+
"l2",
|
|
792
|
+
"l3",
|
|
793
|
+
"l4",
|
|
794
|
+
"l5",
|
|
795
|
+
"l6",
|
|
796
|
+
"l7",
|
|
797
|
+
"guard infrastructure",
|
|
798
|
+
"data layer",
|
|
799
|
+
"intelligence layer",
|
|
800
|
+
"mcp layer",
|
|
801
|
+
"core layer"
|
|
802
|
+
],
|
|
803
|
+
"business-rules": [
|
|
804
|
+
"guard",
|
|
805
|
+
"enforce",
|
|
806
|
+
"integrity",
|
|
807
|
+
"prevents",
|
|
808
|
+
"excludes",
|
|
809
|
+
"permitted",
|
|
810
|
+
"policy",
|
|
811
|
+
"feature gate",
|
|
812
|
+
"must not",
|
|
813
|
+
"only permitted",
|
|
814
|
+
"closed enum",
|
|
815
|
+
"write guard",
|
|
816
|
+
"never imports",
|
|
817
|
+
"requires active session",
|
|
818
|
+
"readiness excludes"
|
|
819
|
+
],
|
|
820
|
+
"tracking-events": [
|
|
821
|
+
"track",
|
|
822
|
+
"tracking",
|
|
823
|
+
"analytics",
|
|
824
|
+
"trigger",
|
|
825
|
+
"posthog",
|
|
826
|
+
"instrument",
|
|
827
|
+
"fires when"
|
|
828
|
+
],
|
|
829
|
+
landscape: [
|
|
830
|
+
"competitor",
|
|
831
|
+
"alternative tool",
|
|
832
|
+
"alternative platform",
|
|
833
|
+
"competing product",
|
|
834
|
+
"landscape",
|
|
835
|
+
"comparison"
|
|
836
|
+
],
|
|
837
|
+
standards: [
|
|
838
|
+
"standard",
|
|
839
|
+
"convention",
|
|
840
|
+
"trunk-based",
|
|
841
|
+
"alignment-first",
|
|
842
|
+
"structured bet",
|
|
843
|
+
"system fixes",
|
|
844
|
+
"patches"
|
|
845
|
+
],
|
|
846
|
+
principles: [
|
|
847
|
+
"we believe",
|
|
848
|
+
"principle",
|
|
849
|
+
"compounds",
|
|
850
|
+
"philosophy",
|
|
851
|
+
"simplicity compounds",
|
|
852
|
+
"trust through",
|
|
853
|
+
"evidence over",
|
|
854
|
+
"compensate for",
|
|
855
|
+
"honest by default"
|
|
856
|
+
],
|
|
857
|
+
assumptions: [
|
|
858
|
+
"assume",
|
|
859
|
+
"assumption",
|
|
860
|
+
"hypothesis",
|
|
861
|
+
"untested",
|
|
862
|
+
"we think",
|
|
863
|
+
"we assume",
|
|
864
|
+
"needs validation"
|
|
865
|
+
]
|
|
866
|
+
};
|
|
867
|
+
function escapeRegExp(text) {
|
|
868
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
869
|
+
}
|
|
870
|
+
function countSignalMatches(text, signal) {
|
|
871
|
+
const trimmed = signal.trim().toLowerCase();
|
|
872
|
+
if (!trimmed) return 0;
|
|
873
|
+
const words = trimmed.split(/\s+/).map(escapeRegExp);
|
|
874
|
+
const pattern = `\\b${words.join("\\s+")}\\b`;
|
|
875
|
+
const regex = new RegExp(pattern, "g");
|
|
876
|
+
const matches = text.match(regex);
|
|
877
|
+
return matches?.length ?? 0;
|
|
878
|
+
}
|
|
879
|
+
function classifyCollection(name, description) {
|
|
880
|
+
const text = `${name} ${description}`.replace(ENTRY_ID_PATTERN, "").toLowerCase();
|
|
881
|
+
const rawScores = [];
|
|
882
|
+
for (const collection of CLASSIFIABLE_COLLECTIONS) {
|
|
883
|
+
const signals = COLLECTION_SIGNALS[collection];
|
|
884
|
+
const reasons = [];
|
|
885
|
+
let score = 0;
|
|
886
|
+
for (const signal of signals) {
|
|
887
|
+
const matches = countSignalMatches(text, signal);
|
|
888
|
+
if (matches <= 0) continue;
|
|
889
|
+
const cappedMatches = Math.min(matches, MAX_MATCHES_PER_SIGNAL);
|
|
890
|
+
score += cappedMatches * SIGNAL_WEIGHT;
|
|
891
|
+
if (reasons.length < MAX_REASON_COUNT) {
|
|
892
|
+
const capNote = matches > cappedMatches ? ` (capped at ${cappedMatches})` : "";
|
|
893
|
+
reasons.push(`matched "${signal}" x${matches}${capNote}`);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
rawScores.push({ collection, score, reasons });
|
|
897
|
+
}
|
|
898
|
+
rawScores.sort((left, right) => right.score - left.score);
|
|
899
|
+
const top = rawScores[0];
|
|
900
|
+
const second = rawScores[1];
|
|
901
|
+
if (!top || top.score < MIN_SCORE_FLOOR) return null;
|
|
902
|
+
const margin = Math.max(0, top.score - (second?.score ?? 0));
|
|
903
|
+
if (margin === 0 && top.score <= MIN_SCORE_FLOOR) return null;
|
|
904
|
+
const baseConfidence = Math.min(90, top.score);
|
|
905
|
+
const confidence = Math.min(99, baseConfidence + Math.min(20, margin));
|
|
906
|
+
return {
|
|
907
|
+
collection: top.collection,
|
|
908
|
+
topConfidence: confidence,
|
|
909
|
+
confidence,
|
|
910
|
+
reasons: top.reasons,
|
|
911
|
+
scoreMargin: margin,
|
|
912
|
+
candidates: rawScores.filter((candidate) => candidate.score > 0).slice(0, 3).map((candidate) => ({
|
|
913
|
+
collection: candidate.collection,
|
|
914
|
+
signalScore: Math.min(99, candidate.score),
|
|
915
|
+
confidence: Math.min(99, candidate.score)
|
|
916
|
+
}))
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
function isClassificationAmbiguous(result) {
|
|
920
|
+
return result.scoreMargin < CLASSIFIER_AMBIGUITY_MARGIN;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// src/lib/resolveCollection.ts
|
|
924
|
+
var HIGH_THRESHOLD = 80;
|
|
925
|
+
var MEDIUM_THRESHOLD = 50;
|
|
926
|
+
function getTier(confidence) {
|
|
927
|
+
if (confidence >= HIGH_THRESHOLD) return "high";
|
|
928
|
+
if (confidence >= MEDIUM_THRESHOLD) return "medium";
|
|
929
|
+
return "low";
|
|
930
|
+
}
|
|
931
|
+
var TYPE_HINT_COLLECTION_MAP = {
|
|
932
|
+
element: "features",
|
|
933
|
+
risk: "tensions",
|
|
934
|
+
decision: "decisions"
|
|
935
|
+
};
|
|
936
|
+
async function resolveCollection(params) {
|
|
937
|
+
const { name, description, typeHint } = params;
|
|
938
|
+
const llmResult = await tryLlmClassifier(name, description);
|
|
939
|
+
if (llmResult) {
|
|
940
|
+
return typeHint ? applyTypeHintBias(llmResult, typeHint) : llmResult;
|
|
941
|
+
}
|
|
942
|
+
const heuristic = classifyCollection(name, description);
|
|
943
|
+
if (heuristic) {
|
|
944
|
+
const resolved = heuristicToResolved(heuristic);
|
|
945
|
+
return typeHint ? applyTypeHintBias(resolved, typeHint) : resolved;
|
|
946
|
+
}
|
|
947
|
+
if (typeHint) {
|
|
948
|
+
const mapped = TYPE_HINT_COLLECTION_MAP[typeHint];
|
|
949
|
+
if (mapped) {
|
|
950
|
+
return {
|
|
951
|
+
collection: mapped,
|
|
952
|
+
confidence: 60,
|
|
953
|
+
tier: "medium",
|
|
954
|
+
alternatives: [],
|
|
955
|
+
classifiedBy: "heuristic",
|
|
956
|
+
reasoning: `Fallback from type hint: ${typeHint} \u2192 ${mapped}`
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return null;
|
|
961
|
+
}
|
|
962
|
+
async function tryLlmClassifier(name, description) {
|
|
963
|
+
try {
|
|
964
|
+
const sessionId = getAgentSessionId();
|
|
965
|
+
const result = await mcpCall(
|
|
966
|
+
"chain.classifyCollection",
|
|
967
|
+
{
|
|
968
|
+
entryName: name,
|
|
969
|
+
entryDescription: description,
|
|
970
|
+
...sessionId ? { agentSessionId: sessionId } : {}
|
|
971
|
+
}
|
|
972
|
+
);
|
|
973
|
+
if (!result || typeof result.collection !== "string" || typeof result.confidence !== "number") {
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
return {
|
|
977
|
+
collection: result.collection,
|
|
978
|
+
confidence: result.confidence,
|
|
979
|
+
tier: getTier(result.confidence),
|
|
980
|
+
alternatives: Array.isArray(result.alternatives) ? result.alternatives : [],
|
|
981
|
+
classifiedBy: "llm",
|
|
982
|
+
reasoning: typeof result.reasoning === "string" ? result.reasoning : ""
|
|
983
|
+
};
|
|
984
|
+
} catch (err) {
|
|
985
|
+
process.stderr.write(
|
|
986
|
+
`[resolveCollection] LLM classifier failed, falling back to heuristic: ${err instanceof Error ? err.message : String(err)}
|
|
987
|
+
`
|
|
988
|
+
);
|
|
989
|
+
return null;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
function applyTypeHintBias(result, typeHint) {
|
|
993
|
+
const hintedCollection = TYPE_HINT_COLLECTION_MAP[typeHint];
|
|
994
|
+
if (!hintedCollection) return result;
|
|
995
|
+
if (result.collection === hintedCollection) {
|
|
996
|
+
const boosted = Math.min(99, result.confidence + 10);
|
|
997
|
+
return { ...result, confidence: boosted, tier: getTier(boosted) };
|
|
998
|
+
}
|
|
999
|
+
if (result.tier === "high") return result;
|
|
1000
|
+
return {
|
|
1001
|
+
...result,
|
|
1002
|
+
collection: hintedCollection,
|
|
1003
|
+
confidence: 65,
|
|
1004
|
+
tier: "medium",
|
|
1005
|
+
reasoning: `Type hint '${typeHint}' overrode ${result.classifiedBy} classification (${result.collection} at ${result.confidence}%)`
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
function heuristicToResolved(result) {
|
|
1009
|
+
return {
|
|
1010
|
+
collection: result.collection,
|
|
1011
|
+
confidence: result.confidence,
|
|
1012
|
+
tier: getTier(result.confidence),
|
|
1013
|
+
alternatives: result.candidates.slice(0, 3).map((c) => ({
|
|
1014
|
+
collection: c.collection,
|
|
1015
|
+
confidence: c.confidence
|
|
1016
|
+
})),
|
|
1017
|
+
classifiedBy: "heuristic",
|
|
1018
|
+
reasoning: result.reasons.join("; ")
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// src/envelope.ts
|
|
1023
|
+
import { z } from "zod";
|
|
1024
|
+
|
|
1025
|
+
// src/errors.ts
|
|
1026
|
+
var GateError = class extends Error {
|
|
1027
|
+
constructor(message) {
|
|
1028
|
+
super(message);
|
|
1029
|
+
this.name = "GateError";
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
var BackendError = class extends Error {
|
|
1033
|
+
constructor(message) {
|
|
1034
|
+
super(message);
|
|
1035
|
+
this.name = "BackendError";
|
|
1036
|
+
}
|
|
1037
|
+
};
|
|
1038
|
+
var ValidationError = class extends Error {
|
|
1039
|
+
constructor(message) {
|
|
1040
|
+
super(message);
|
|
1041
|
+
this.name = "ValidationError";
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1044
|
+
var GATE_PATTERNS = [
|
|
1045
|
+
{
|
|
1046
|
+
pattern: /session required|no active.*session|call.*session.*start/i,
|
|
1047
|
+
code: "SESSION_REQUIRED",
|
|
1048
|
+
recovery: "Start an agent session first.",
|
|
1049
|
+
action: { tool: "session", description: "Start session", parameters: { action: "start" } }
|
|
1050
|
+
},
|
|
1051
|
+
{
|
|
1052
|
+
pattern: /session.*closed/i,
|
|
1053
|
+
code: "SESSION_CLOSED",
|
|
1054
|
+
recovery: "Start a new session.",
|
|
1055
|
+
action: { tool: "session", description: "Start new session", parameters: { action: "start" } }
|
|
1056
|
+
},
|
|
1057
|
+
{
|
|
1058
|
+
pattern: /orientation required|call.*orient/i,
|
|
1059
|
+
code: "ORIENTATION_REQUIRED",
|
|
1060
|
+
recovery: "Orient before writing.",
|
|
1061
|
+
action: { tool: "orient", description: "Orient session", parameters: {} }
|
|
1062
|
+
},
|
|
1063
|
+
{
|
|
1064
|
+
pattern: /read.?only.*scope/i,
|
|
1065
|
+
code: "READONLY_SCOPE",
|
|
1066
|
+
recovery: "This API key cannot write. Use a readwrite key.",
|
|
1067
|
+
action: { tool: "health", description: "Check key scope", parameters: { action: "whoami" } }
|
|
1068
|
+
}
|
|
1069
|
+
];
|
|
1070
|
+
function classifyError(err) {
|
|
1071
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1072
|
+
if (err instanceof GateError) {
|
|
1073
|
+
return {
|
|
1074
|
+
code: "PERMISSION_DENIED",
|
|
1075
|
+
message,
|
|
1076
|
+
recovery: "Check permissions or use a readwrite API key.",
|
|
1077
|
+
availableActions: [{ tool: "health", description: "Check key scope", parameters: { action: "whoami" } }]
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
if (err instanceof BackendError) {
|
|
1081
|
+
return { code: "BACKEND_ERROR", message, recovery: "Retry the operation or check backend health." };
|
|
1082
|
+
}
|
|
1083
|
+
if (err instanceof ValidationError) {
|
|
1084
|
+
return { code: "VALIDATION_ERROR", message };
|
|
1085
|
+
}
|
|
1086
|
+
for (const { pattern, code, recovery, action } of GATE_PATTERNS) {
|
|
1087
|
+
if (pattern.test(message)) {
|
|
1088
|
+
return { code, message, recovery, availableActions: [action] };
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
if (/network error|fetch failed|ECONNREFUSED|ETIMEDOUT/i.test(message)) {
|
|
1092
|
+
return { code: "BACKEND_UNAVAILABLE", message, recovery: "Retry in a few seconds." };
|
|
1093
|
+
}
|
|
1094
|
+
if (/not found/i.test(message)) {
|
|
1095
|
+
return {
|
|
1096
|
+
code: "NOT_FOUND",
|
|
1097
|
+
message,
|
|
1098
|
+
recovery: "Use entries action=search to find the correct ID.",
|
|
1099
|
+
availableActions: [{ tool: "entries", description: "Search entries", parameters: { action: "search" } }]
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
if (/duplicate|already exists/i.test(message)) {
|
|
1103
|
+
return {
|
|
1104
|
+
code: "DUPLICATE",
|
|
1105
|
+
message,
|
|
1106
|
+
recovery: "Use entries action=get to inspect the existing entry.",
|
|
1107
|
+
availableActions: [{ tool: "entries", description: "Get entry", parameters: { action: "get" } }]
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
if (/invalid|validation|required field/i.test(message)) {
|
|
1111
|
+
return { code: "VALIDATION_ERROR", message };
|
|
1112
|
+
}
|
|
1113
|
+
return { code: "INTERNAL_ERROR", message: message || "An unexpected error occurred." };
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// src/envelope.ts
|
|
1117
|
+
function success(summary, data, next) {
|
|
1118
|
+
return {
|
|
1119
|
+
ok: true,
|
|
1120
|
+
summary: summary || "Operation completed.",
|
|
1121
|
+
data,
|
|
1122
|
+
...next?.length ? { next } : {}
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
function failure(code, message, recovery, availableActions, diagnostics) {
|
|
1126
|
+
return {
|
|
1127
|
+
ok: false,
|
|
1128
|
+
code,
|
|
1129
|
+
message,
|
|
1130
|
+
...recovery ? { recovery } : {},
|
|
1131
|
+
...availableActions?.length ? { availableActions } : {},
|
|
1132
|
+
...diagnostics ? { diagnostics } : {}
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
function notFound(id, hint) {
|
|
1136
|
+
return failure(
|
|
1137
|
+
"NOT_FOUND",
|
|
1138
|
+
`Entry '${id}' not found.`,
|
|
1139
|
+
hint ?? "Use entries action=search to find the correct ID.",
|
|
1140
|
+
[{ tool: "entries", description: "Search entries", parameters: { action: "search", query: id } }]
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
function validationError(message) {
|
|
1144
|
+
return failure("VALIDATION_ERROR", message);
|
|
1145
|
+
}
|
|
1146
|
+
function textContent(text) {
|
|
1147
|
+
return [{ type: "text", text }];
|
|
1148
|
+
}
|
|
1149
|
+
function parseOrFail(schema, args) {
|
|
1150
|
+
const parsed = schema.safeParse(args);
|
|
1151
|
+
if (parsed.success) return { ok: true, data: parsed.data };
|
|
1152
|
+
const issues = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
1153
|
+
const msg = `Invalid arguments: ${issues}`;
|
|
1154
|
+
return {
|
|
1155
|
+
ok: false,
|
|
1156
|
+
result: { content: textContent(msg), structuredContent: validationError(msg) }
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
function unknownAction(action, valid) {
|
|
1160
|
+
const msg = `Unknown action '${action}'. Valid actions: ${valid.join(", ")}.`;
|
|
1161
|
+
return { content: textContent(msg), structuredContent: validationError(msg) };
|
|
1162
|
+
}
|
|
1163
|
+
function successResult(text, summary, data, next) {
|
|
1164
|
+
return { content: textContent(text), structuredContent: success(summary, data, next) };
|
|
1165
|
+
}
|
|
1166
|
+
function failureResult(text, code, message, recovery, availableActions, diagnostics) {
|
|
1167
|
+
return { content: textContent(text), structuredContent: failure(code, message, recovery, availableActions, diagnostics) };
|
|
1168
|
+
}
|
|
1169
|
+
function notFoundResult(id, text, hint) {
|
|
1170
|
+
return { content: textContent(text ?? `Entry '${id}' not found.`), structuredContent: notFound(id, hint) };
|
|
1171
|
+
}
|
|
1172
|
+
function validationResult(message) {
|
|
1173
|
+
return { content: textContent(message), structuredContent: validationError(message) };
|
|
1174
|
+
}
|
|
1175
|
+
var nextActionSchema = z.object({
|
|
1176
|
+
tool: z.string(),
|
|
1177
|
+
description: z.string(),
|
|
1178
|
+
parameters: z.record(z.unknown())
|
|
1179
|
+
});
|
|
1180
|
+
var metaSchema = z.object({
|
|
1181
|
+
durationMs: z.number().optional()
|
|
1182
|
+
}).optional();
|
|
1183
|
+
function withEnvelope(handler) {
|
|
1184
|
+
return async (args) => {
|
|
1185
|
+
const start = Date.now();
|
|
1186
|
+
try {
|
|
1187
|
+
const result = await handler(args);
|
|
1188
|
+
const durationMs = Date.now() - start;
|
|
1189
|
+
const sc = result.structuredContent;
|
|
1190
|
+
if (sc && typeof sc === "object" && "ok" in sc) {
|
|
1191
|
+
sc._meta = { ...sc._meta, durationMs };
|
|
1192
|
+
return {
|
|
1193
|
+
content: result.content ?? [],
|
|
1194
|
+
structuredContent: sc,
|
|
1195
|
+
...sc.ok === false ? { isError: true } : {}
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
console.warn(`[withEnvelope] Handler returned without envelope shape. Wrapping automatically. Content preview: "${result.content?.[0]?.text?.slice(0, 60) ?? "(empty)"}"`);
|
|
1199
|
+
return {
|
|
1200
|
+
content: result.content ?? [],
|
|
1201
|
+
structuredContent: {
|
|
1202
|
+
...success(
|
|
1203
|
+
result.content?.[0]?.text?.slice(0, 100) ?? "",
|
|
1204
|
+
sc ?? {}
|
|
1205
|
+
),
|
|
1206
|
+
_meta: { durationMs }
|
|
1207
|
+
}
|
|
1208
|
+
};
|
|
1209
|
+
} catch (err) {
|
|
1210
|
+
const durationMs = Date.now() - start;
|
|
1211
|
+
const classified = classifyError(err);
|
|
1212
|
+
const envelope = {
|
|
1213
|
+
...failure(
|
|
1214
|
+
classified.code,
|
|
1215
|
+
classified.message,
|
|
1216
|
+
classified.recovery,
|
|
1217
|
+
classified.availableActions
|
|
1218
|
+
),
|
|
1219
|
+
_meta: { durationMs }
|
|
1220
|
+
};
|
|
1221
|
+
return {
|
|
1222
|
+
content: [{ type: "text", text: classified.message }],
|
|
1223
|
+
structuredContent: envelope,
|
|
1224
|
+
isError: true
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// src/lib/fieldTypes.ts
|
|
1231
|
+
var FIELD_TYPE_DEFAULTS = {
|
|
1232
|
+
"string": "",
|
|
1233
|
+
"text": "",
|
|
1234
|
+
"rich-text": "",
|
|
1235
|
+
"number": null,
|
|
1236
|
+
"boolean": false,
|
|
1237
|
+
"select": null,
|
|
1238
|
+
"multi-select": [],
|
|
1239
|
+
"array": [],
|
|
1240
|
+
"json": null
|
|
1241
|
+
};
|
|
1242
|
+
|
|
1243
|
+
// src/tools/smart-capture-routing.ts
|
|
1244
|
+
var STARTER_COLLECTIONS = CLASSIFIABLE_COLLECTIONS;
|
|
1245
|
+
|
|
1246
|
+
// src/tools/smart-capture.ts
|
|
1247
|
+
var AREA_KEYWORDS = {
|
|
1248
|
+
"Architecture": ["convex", "schema", "database", "migration", "api", "backend", "infrastructure", "scaling", "performance"],
|
|
1249
|
+
"Chain": ["knowledge", "glossary", "entry", "collection", "terminology", "drift", "graph", "chain", "commit"],
|
|
1250
|
+
"AI & MCP Integration": ["mcp", "ai", "cursor", "agent", "tool", "llm", "prompt", "context"],
|
|
1251
|
+
"Developer Experience": ["dx", "developer", "ide", "workflow", "friction", "ceremony"],
|
|
1252
|
+
"Governance & Decision-Making": ["governance", "decision", "rule", "policy", "compliance", "approval"],
|
|
1253
|
+
"Analytics & Tracking": ["analytics", "posthog", "tracking", "event", "metric", "funnel"],
|
|
1254
|
+
"Security": ["security", "auth", "api key", "permission", "access", "token"]
|
|
1255
|
+
};
|
|
1256
|
+
function inferArea(text) {
|
|
1257
|
+
const lower = text.toLowerCase();
|
|
1258
|
+
let bestArea = "";
|
|
1259
|
+
let bestScore = 0;
|
|
1260
|
+
for (const [area, keywords] of Object.entries(AREA_KEYWORDS)) {
|
|
1261
|
+
const score = keywords.filter((kw) => lower.includes(kw)).length;
|
|
1262
|
+
if (score > bestScore) {
|
|
1263
|
+
bestScore = score;
|
|
1264
|
+
bestArea = area;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
return bestArea;
|
|
1268
|
+
}
|
|
1269
|
+
function inferDomain(text) {
|
|
1270
|
+
return inferArea(text) || "";
|
|
1271
|
+
}
|
|
1272
|
+
var COMMON_CHECKS = {
|
|
1273
|
+
clearName: {
|
|
1274
|
+
id: "clear-name",
|
|
1275
|
+
label: "Clear, specific name (not vague)",
|
|
1276
|
+
check: (ctx) => ctx.name.length > 10 && !["new tension", "new entry", "untitled", "test"].includes(ctx.name.toLowerCase()),
|
|
1277
|
+
suggestion: () => "Rename to something specific \u2014 describe the actual problem or concept."
|
|
1278
|
+
},
|
|
1279
|
+
hasDescription: {
|
|
1280
|
+
id: "has-description",
|
|
1281
|
+
label: "Description provided (>50 chars)",
|
|
1282
|
+
check: (ctx) => ctx.description.length > 50,
|
|
1283
|
+
suggestion: () => "Add a fuller description explaining context and impact."
|
|
1284
|
+
},
|
|
1285
|
+
hasRelations: {
|
|
1286
|
+
id: "has-relations",
|
|
1287
|
+
label: "At least 1 relation created",
|
|
1288
|
+
check: (ctx) => ctx.linksCreated.length >= 1,
|
|
1289
|
+
suggestion: () => "Use `graph action=suggest` and `relations action=create` to add more connections."
|
|
1290
|
+
},
|
|
1291
|
+
diverseRelations: {
|
|
1292
|
+
id: "diverse-relations",
|
|
1293
|
+
label: "Relations span multiple collections",
|
|
1294
|
+
check: (ctx) => {
|
|
1295
|
+
const colls = new Set(ctx.linksCreated.map((l) => l.targetCollection));
|
|
1296
|
+
return colls.size >= 2;
|
|
1297
|
+
},
|
|
1298
|
+
suggestion: () => "Try linking to entries in different collections (glossary, business-rules, strategy)."
|
|
1299
|
+
},
|
|
1300
|
+
hasType: {
|
|
1301
|
+
id: "has-type",
|
|
1302
|
+
label: "Has canonical type",
|
|
1303
|
+
check: (ctx) => !!ctx.data?.canonicalKey || !!ctx.canonicalKey,
|
|
1304
|
+
suggestion: () => "Classify this entry with a canonical type for better context assembly. Use update-entry to set canonicalKey."
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
1307
|
+
var STRATEGY_CATEGORY_INFERENCE_THRESHOLD = 2;
|
|
1308
|
+
var STRATEGY_CATEGORY_SIGNALS = [
|
|
1309
|
+
["business-model", [/pricing/, /revenue/, /unit economics/, /cost structure/, /monetiz/, /margin/, /per.?seat/, /subscription/, /freemium/]],
|
|
1310
|
+
["vision", [/vision/, /aspirational/, /future state/, /world where/]],
|
|
1311
|
+
["purpose", [/purpose/, /why we exist/, /mission/, /reason for being/]],
|
|
1312
|
+
["goal", [/goal/, /metric/, /target/, /kpi/, /okr/, /critical number/, /measur/]],
|
|
1313
|
+
["principle", [/we believe/, /guiding principle/, /core belief/, /philosophy/]],
|
|
1314
|
+
["product-area", [/product area/, /module/, /surface/, /capability area/]],
|
|
1315
|
+
["audience", [/audience/, /persona/, /user segment/, /icp/, /target market/]],
|
|
1316
|
+
["insight", [/insight/, /we learned/, /we observed/, /pattern/]],
|
|
1317
|
+
["opportunity", [/opportunity/, /whitespace/, /gap/, /underserved/]]
|
|
1318
|
+
];
|
|
1319
|
+
function inferStrategyCategory(name, description) {
|
|
1320
|
+
const text = `${name} ${description}`.toLowerCase();
|
|
1321
|
+
let bestCategory = null;
|
|
1322
|
+
let bestScore = 0;
|
|
1323
|
+
for (const [cat, signals] of STRATEGY_CATEGORY_SIGNALS) {
|
|
1324
|
+
const score = signals.reduce((s, rx) => s + (rx.test(text) ? 1 : 0), 0);
|
|
1325
|
+
if (score > bestScore) {
|
|
1326
|
+
bestScore = score;
|
|
1327
|
+
bestCategory = cat;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
if (bestCategory && bestScore >= STRATEGY_CATEGORY_INFERENCE_THRESHOLD) return bestCategory;
|
|
1331
|
+
if (bestScore === 0) return "strategy";
|
|
1332
|
+
return null;
|
|
1333
|
+
}
|
|
1334
|
+
var PROFILES = /* @__PURE__ */ new Map([
|
|
1335
|
+
["tensions", {
|
|
1336
|
+
governedDraft: false,
|
|
1337
|
+
descriptionField: "description",
|
|
1338
|
+
defaults: [
|
|
1339
|
+
{ key: "priority", value: "medium" },
|
|
1340
|
+
{ key: "date", value: "today" },
|
|
1341
|
+
{ key: "raised", value: "infer" },
|
|
1342
|
+
{ key: "severity", value: "infer" }
|
|
1343
|
+
],
|
|
1344
|
+
recommendedRelationTypes: ["surfaces_tension_in", "references", "belongs_to", "related_to"],
|
|
1345
|
+
inferField: (ctx) => {
|
|
1346
|
+
const fields = {};
|
|
1347
|
+
const text = `${ctx.name} ${ctx.description}`;
|
|
1348
|
+
const area = inferArea(text);
|
|
1349
|
+
if (area) fields.raised = area;
|
|
1350
|
+
if (text.toLowerCase().includes("critical") || text.toLowerCase().includes("blocker")) {
|
|
1351
|
+
fields.severity = "critical";
|
|
1352
|
+
} else if (text.toLowerCase().includes("bottleneck") || text.toLowerCase().includes("scaling") || text.toLowerCase().includes("breaking")) {
|
|
1353
|
+
fields.severity = "high";
|
|
1354
|
+
} else {
|
|
1355
|
+
fields.severity = "medium";
|
|
1356
|
+
}
|
|
1357
|
+
if (area) fields.affectedArea = area;
|
|
1358
|
+
return fields;
|
|
1359
|
+
},
|
|
1360
|
+
qualityChecks: [
|
|
1361
|
+
COMMON_CHECKS.clearName,
|
|
1362
|
+
COMMON_CHECKS.hasDescription,
|
|
1363
|
+
COMMON_CHECKS.hasRelations,
|
|
1364
|
+
COMMON_CHECKS.hasType,
|
|
1365
|
+
{
|
|
1366
|
+
id: "has-severity",
|
|
1367
|
+
label: "Severity specified",
|
|
1368
|
+
check: (ctx) => !!ctx.data.severity && ctx.data.severity !== "",
|
|
1369
|
+
suggestion: (ctx) => {
|
|
1370
|
+
const text = `${ctx.name} ${ctx.description}`.toLowerCase();
|
|
1371
|
+
const inferred = text.includes("critical") ? "critical" : text.includes("bottleneck") ? "high" : "medium";
|
|
1372
|
+
return `Set severity \u2014 suggest: ${inferred} (based on description keywords).`;
|
|
1373
|
+
}
|
|
1374
|
+
},
|
|
1375
|
+
{
|
|
1376
|
+
id: "has-affected-area",
|
|
1377
|
+
label: "Affected area identified",
|
|
1378
|
+
check: (ctx) => !!ctx.data.affectedArea && ctx.data.affectedArea !== "",
|
|
1379
|
+
suggestion: (ctx) => {
|
|
1380
|
+
const area = inferArea(`${ctx.name} ${ctx.description}`);
|
|
1381
|
+
return area ? `Set affectedArea \u2014 suggest: "${area}" (inferred from content).` : "Specify which product area or domain this tension impacts.";
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
]
|
|
1385
|
+
}],
|
|
1386
|
+
["business-rules", {
|
|
1387
|
+
governedDraft: true,
|
|
1388
|
+
descriptionField: "description",
|
|
1389
|
+
defaults: [
|
|
1390
|
+
{ key: "severity", value: "medium" },
|
|
1391
|
+
{ key: "domain", value: "infer" }
|
|
1392
|
+
],
|
|
1393
|
+
recommendedRelationTypes: ["governs", "references", "conflicts_with", "related_to"],
|
|
1394
|
+
inferField: (ctx) => {
|
|
1395
|
+
const fields = {};
|
|
1396
|
+
const domain = inferDomain(`${ctx.name} ${ctx.description}`);
|
|
1397
|
+
if (domain) fields.domain = domain;
|
|
1398
|
+
return fields;
|
|
1399
|
+
},
|
|
1400
|
+
qualityChecks: [
|
|
1401
|
+
COMMON_CHECKS.clearName,
|
|
1402
|
+
COMMON_CHECKS.hasDescription,
|
|
1403
|
+
COMMON_CHECKS.hasRelations,
|
|
1404
|
+
COMMON_CHECKS.hasType,
|
|
1405
|
+
{
|
|
1406
|
+
id: "has-rationale",
|
|
1407
|
+
label: "Rationale provided",
|
|
1408
|
+
check: (ctx) => typeof ctx.data.rationale === "string" && ctx.data.rationale.length > 10,
|
|
1409
|
+
suggestion: () => "Add a rationale explaining why this rule exists via `update-entry`."
|
|
1410
|
+
},
|
|
1411
|
+
{
|
|
1412
|
+
id: "has-domain",
|
|
1413
|
+
label: "Domain specified",
|
|
1414
|
+
check: (ctx) => !!ctx.data.domain && ctx.data.domain !== "",
|
|
1415
|
+
suggestion: (ctx) => {
|
|
1416
|
+
const domain = inferDomain(`${ctx.name} ${ctx.description}`);
|
|
1417
|
+
return domain ? `Set domain \u2014 suggest: "${domain}" (inferred from content).` : "Specify the business domain this rule belongs to.";
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
]
|
|
1421
|
+
}],
|
|
1422
|
+
["glossary", {
|
|
1423
|
+
governedDraft: true,
|
|
1424
|
+
descriptionField: "canonical",
|
|
1425
|
+
defaults: [
|
|
1426
|
+
{ key: "category", value: "infer" }
|
|
1427
|
+
],
|
|
1428
|
+
recommendedRelationTypes: ["defines_term_for", "confused_with", "related_to", "references"],
|
|
1429
|
+
inferField: (ctx) => {
|
|
1430
|
+
const fields = {};
|
|
1431
|
+
const area = inferArea(`${ctx.name} ${ctx.description}`);
|
|
1432
|
+
if (area) {
|
|
1433
|
+
const categoryMap = {
|
|
1434
|
+
"Architecture": "Platform & Architecture",
|
|
1435
|
+
"Chain": "Knowledge Management",
|
|
1436
|
+
"AI & MCP Integration": "AI & Developer Tools",
|
|
1437
|
+
"Developer Experience": "AI & Developer Tools",
|
|
1438
|
+
"Governance & Decision-Making": "Governance & Process",
|
|
1439
|
+
"Analytics & Tracking": "Platform & Architecture",
|
|
1440
|
+
"Security": "Platform & Architecture"
|
|
1441
|
+
};
|
|
1442
|
+
fields.category = categoryMap[area] ?? "";
|
|
1443
|
+
}
|
|
1444
|
+
return fields;
|
|
1445
|
+
},
|
|
1446
|
+
qualityChecks: [
|
|
1447
|
+
COMMON_CHECKS.clearName,
|
|
1448
|
+
COMMON_CHECKS.hasType,
|
|
1449
|
+
{
|
|
1450
|
+
id: "has-canonical",
|
|
1451
|
+
label: "Canonical definition provided (>20 chars)",
|
|
1452
|
+
check: (ctx) => {
|
|
1453
|
+
const canonical = ctx.data.canonical;
|
|
1454
|
+
return typeof canonical === "string" && canonical.length > 20;
|
|
1455
|
+
},
|
|
1456
|
+
suggestion: () => "Add a clear canonical definition \u2014 this is the single source of truth for this term."
|
|
1457
|
+
},
|
|
1458
|
+
COMMON_CHECKS.hasRelations,
|
|
1459
|
+
{
|
|
1460
|
+
id: "has-category",
|
|
1461
|
+
label: "Category assigned",
|
|
1462
|
+
check: (ctx) => !!ctx.data.category && ctx.data.category !== "",
|
|
1463
|
+
suggestion: () => "Assign a category (e.g., 'Platform & Architecture', 'Governance & Process')."
|
|
1464
|
+
}
|
|
1465
|
+
]
|
|
1466
|
+
}],
|
|
1467
|
+
["decisions", {
|
|
1468
|
+
governedDraft: false,
|
|
1469
|
+
descriptionField: "rationale",
|
|
1470
|
+
defaults: [
|
|
1471
|
+
{ key: "date", value: "today" },
|
|
1472
|
+
{ key: "decidedBy", value: "infer" }
|
|
1473
|
+
],
|
|
1474
|
+
recommendedRelationTypes: ["informs", "references", "replaces", "related_to"],
|
|
1475
|
+
inferField: (ctx) => {
|
|
1476
|
+
const fields = {};
|
|
1477
|
+
const area = inferArea(`${ctx.name} ${ctx.description}`);
|
|
1478
|
+
if (area) fields.decidedBy = area;
|
|
1479
|
+
return fields;
|
|
1480
|
+
},
|
|
1481
|
+
qualityChecks: [
|
|
1482
|
+
COMMON_CHECKS.clearName,
|
|
1483
|
+
COMMON_CHECKS.hasType,
|
|
1484
|
+
{
|
|
1485
|
+
id: "has-rationale",
|
|
1486
|
+
label: "Rationale provided (>30 chars)",
|
|
1487
|
+
check: (ctx) => {
|
|
1488
|
+
const rationale = ctx.data.rationale;
|
|
1489
|
+
return typeof rationale === "string" && rationale.length > 30;
|
|
1490
|
+
},
|
|
1491
|
+
suggestion: () => "Explain why this decision was made \u2014 what was considered and rejected?"
|
|
1492
|
+
},
|
|
1493
|
+
COMMON_CHECKS.hasRelations,
|
|
1494
|
+
{
|
|
1495
|
+
id: "has-date",
|
|
1496
|
+
label: "Decision date recorded",
|
|
1497
|
+
check: (ctx) => !!ctx.data.date && ctx.data.date !== "",
|
|
1498
|
+
suggestion: () => "Record when this decision was made."
|
|
1499
|
+
}
|
|
1500
|
+
]
|
|
1501
|
+
}],
|
|
1502
|
+
["features", {
|
|
1503
|
+
governedDraft: false,
|
|
1504
|
+
descriptionField: "description",
|
|
1505
|
+
defaults: [],
|
|
1506
|
+
recommendedRelationTypes: ["belongs_to", "depends_on", "surfaces_tension_in", "related_to"],
|
|
1507
|
+
qualityChecks: [
|
|
1508
|
+
COMMON_CHECKS.clearName,
|
|
1509
|
+
COMMON_CHECKS.hasDescription,
|
|
1510
|
+
COMMON_CHECKS.hasRelations,
|
|
1511
|
+
COMMON_CHECKS.hasType,
|
|
1512
|
+
{
|
|
1513
|
+
id: "has-owner",
|
|
1514
|
+
label: "Owner assigned",
|
|
1515
|
+
check: (ctx) => !!ctx.data.owner && ctx.data.owner !== "",
|
|
1516
|
+
suggestion: () => "Assign an owner team or product area."
|
|
1517
|
+
},
|
|
1518
|
+
{
|
|
1519
|
+
id: "has-rationale",
|
|
1520
|
+
label: "Rationale documented",
|
|
1521
|
+
check: (ctx) => !!ctx.data.rationale && String(ctx.data.rationale).length > 20,
|
|
1522
|
+
suggestion: () => "Explain why this feature matters \u2014 what problem does it solve?"
|
|
1523
|
+
}
|
|
1524
|
+
]
|
|
1525
|
+
}],
|
|
1526
|
+
["audiences", {
|
|
1527
|
+
governedDraft: false,
|
|
1528
|
+
descriptionField: "description",
|
|
1529
|
+
defaults: [],
|
|
1530
|
+
recommendedRelationTypes: ["fills_slot", "informs", "related_to", "references"],
|
|
1531
|
+
qualityChecks: [
|
|
1532
|
+
COMMON_CHECKS.clearName,
|
|
1533
|
+
COMMON_CHECKS.hasDescription,
|
|
1534
|
+
COMMON_CHECKS.hasRelations,
|
|
1535
|
+
COMMON_CHECKS.hasType,
|
|
1536
|
+
{
|
|
1537
|
+
id: "has-behaviors",
|
|
1538
|
+
label: "Behaviors described",
|
|
1539
|
+
check: (ctx) => typeof ctx.data.behaviors === "string" && ctx.data.behaviors.length > 20,
|
|
1540
|
+
suggestion: () => "Describe how this audience segment behaves \u2014 what do they do, what tools do they use?"
|
|
1541
|
+
}
|
|
1542
|
+
]
|
|
1543
|
+
}],
|
|
1544
|
+
["strategy", {
|
|
1545
|
+
governedDraft: true,
|
|
1546
|
+
descriptionField: "description",
|
|
1547
|
+
defaults: [],
|
|
1548
|
+
recommendedRelationTypes: ["informs", "governs", "belongs_to", "related_to"],
|
|
1549
|
+
inferField: (ctx) => {
|
|
1550
|
+
if (ctx.data?.category) return {};
|
|
1551
|
+
const category = inferStrategyCategory(ctx.name, ctx.description);
|
|
1552
|
+
return category ? { category } : {};
|
|
1553
|
+
},
|
|
1554
|
+
qualityChecks: [
|
|
1555
|
+
COMMON_CHECKS.clearName,
|
|
1556
|
+
COMMON_CHECKS.hasDescription,
|
|
1557
|
+
COMMON_CHECKS.hasRelations,
|
|
1558
|
+
COMMON_CHECKS.hasType,
|
|
1559
|
+
COMMON_CHECKS.diverseRelations
|
|
1560
|
+
]
|
|
1561
|
+
}],
|
|
1562
|
+
["maps", {
|
|
1563
|
+
governedDraft: false,
|
|
1564
|
+
descriptionField: "description",
|
|
1565
|
+
defaults: [],
|
|
1566
|
+
recommendedRelationTypes: ["fills_slot", "references", "related_to"],
|
|
1567
|
+
qualityChecks: [
|
|
1568
|
+
COMMON_CHECKS.clearName,
|
|
1569
|
+
COMMON_CHECKS.hasDescription
|
|
1570
|
+
]
|
|
1571
|
+
}],
|
|
1572
|
+
["chains", {
|
|
1573
|
+
governedDraft: false,
|
|
1574
|
+
descriptionField: "description",
|
|
1575
|
+
defaults: [],
|
|
1576
|
+
recommendedRelationTypes: ["informs", "references", "related_to"],
|
|
1577
|
+
qualityChecks: [
|
|
1578
|
+
COMMON_CHECKS.clearName,
|
|
1579
|
+
COMMON_CHECKS.hasDescription
|
|
1580
|
+
]
|
|
1581
|
+
}],
|
|
1582
|
+
["principles", {
|
|
1583
|
+
governedDraft: true,
|
|
1584
|
+
descriptionField: "description",
|
|
1585
|
+
defaults: [
|
|
1586
|
+
{ key: "severity", value: "high" },
|
|
1587
|
+
{ key: "category", value: "infer" }
|
|
1588
|
+
],
|
|
1589
|
+
recommendedRelationTypes: ["governs", "informs", "references", "related_to"],
|
|
1590
|
+
inferField: (ctx) => {
|
|
1591
|
+
const fields = {};
|
|
1592
|
+
const area = inferArea(`${ctx.name} ${ctx.description}`);
|
|
1593
|
+
if (area) {
|
|
1594
|
+
const categoryMap = {
|
|
1595
|
+
"Architecture": "Engineering",
|
|
1596
|
+
"Chain": "Product",
|
|
1597
|
+
"AI & MCP Integration": "Engineering",
|
|
1598
|
+
"Developer Experience": "Engineering",
|
|
1599
|
+
"Governance & Decision-Making": "Business",
|
|
1600
|
+
"Analytics & Tracking": "Product",
|
|
1601
|
+
"Security": "Engineering"
|
|
1602
|
+
};
|
|
1603
|
+
fields.category = categoryMap[area] ?? "Product";
|
|
1604
|
+
}
|
|
1605
|
+
return fields;
|
|
1606
|
+
},
|
|
1607
|
+
qualityChecks: [
|
|
1608
|
+
COMMON_CHECKS.clearName,
|
|
1609
|
+
COMMON_CHECKS.hasDescription,
|
|
1610
|
+
COMMON_CHECKS.hasRelations,
|
|
1611
|
+
COMMON_CHECKS.hasType,
|
|
1612
|
+
{
|
|
1613
|
+
id: "has-rationale",
|
|
1614
|
+
label: "Rationale provided \u2014 why this principle matters",
|
|
1615
|
+
check: (ctx) => typeof ctx.data.rationale === "string" && ctx.data.rationale.length > 20,
|
|
1616
|
+
suggestion: () => "Explain why this principle exists and what goes wrong without it."
|
|
1617
|
+
}
|
|
1618
|
+
]
|
|
1619
|
+
}],
|
|
1620
|
+
["standards", {
|
|
1621
|
+
governedDraft: true,
|
|
1622
|
+
descriptionField: "description",
|
|
1623
|
+
defaults: [],
|
|
1624
|
+
recommendedRelationTypes: ["governs", "defines_term_for", "references", "related_to"],
|
|
1625
|
+
qualityChecks: [
|
|
1626
|
+
COMMON_CHECKS.clearName,
|
|
1627
|
+
COMMON_CHECKS.hasDescription,
|
|
1628
|
+
COMMON_CHECKS.hasRelations,
|
|
1629
|
+
COMMON_CHECKS.hasType
|
|
1630
|
+
]
|
|
1631
|
+
}],
|
|
1632
|
+
["tracking-events", {
|
|
1633
|
+
governedDraft: false,
|
|
1634
|
+
descriptionField: "description",
|
|
1635
|
+
defaults: [],
|
|
1636
|
+
recommendedRelationTypes: ["references", "belongs_to", "related_to"],
|
|
1637
|
+
qualityChecks: [
|
|
1638
|
+
COMMON_CHECKS.clearName,
|
|
1639
|
+
COMMON_CHECKS.hasDescription
|
|
1640
|
+
]
|
|
1641
|
+
}],
|
|
1642
|
+
["insights", {
|
|
1643
|
+
governedDraft: false,
|
|
1644
|
+
descriptionField: "description",
|
|
1645
|
+
defaults: [
|
|
1646
|
+
{ key: "evidenceStrength", value: "anecdotal" }
|
|
1647
|
+
],
|
|
1648
|
+
recommendedRelationTypes: ["validates", "invalidates", "informs", "related_to"],
|
|
1649
|
+
qualityChecks: [
|
|
1650
|
+
COMMON_CHECKS.clearName,
|
|
1651
|
+
COMMON_CHECKS.hasDescription,
|
|
1652
|
+
COMMON_CHECKS.hasRelations,
|
|
1653
|
+
COMMON_CHECKS.hasType,
|
|
1654
|
+
{
|
|
1655
|
+
id: "has-evidence-link",
|
|
1656
|
+
label: "Evidence link exists (validates or invalidates)",
|
|
1657
|
+
check: (ctx) => ctx.linksCreated.some((l) => l.relationType === "validates" || l.relationType === "invalidates"),
|
|
1658
|
+
suggestion: () => "Link this insight to the assumption or claim it validates/invalidates using `relations action=create type=validates`."
|
|
1659
|
+
},
|
|
1660
|
+
{
|
|
1661
|
+
id: "has-source",
|
|
1662
|
+
label: "Source documented",
|
|
1663
|
+
check: (ctx) => !!ctx.data?.source && String(ctx.data.source).length > 0,
|
|
1664
|
+
suggestion: () => "Document where this insight came from (research, data, user interview, etc.)."
|
|
1665
|
+
}
|
|
1666
|
+
]
|
|
1667
|
+
}],
|
|
1668
|
+
["assumptions", {
|
|
1669
|
+
governedDraft: false,
|
|
1670
|
+
descriptionField: "belief",
|
|
1671
|
+
defaults: [
|
|
1672
|
+
{ key: "risk", value: "medium" },
|
|
1673
|
+
{ key: "evidenceStrength", value: "unvalidated" }
|
|
1674
|
+
],
|
|
1675
|
+
recommendedRelationTypes: ["depends_on", "informs", "related_to", "validates", "invalidates"],
|
|
1676
|
+
qualityChecks: [
|
|
1677
|
+
COMMON_CHECKS.clearName,
|
|
1678
|
+
{
|
|
1679
|
+
id: "has-belief",
|
|
1680
|
+
label: "Belief statement provided (>30 chars)",
|
|
1681
|
+
check: (ctx) => !!ctx.data?.belief && String(ctx.data.belief).length > 30,
|
|
1682
|
+
suggestion: () => "Write the assumption as a clear, testable belief statement."
|
|
1683
|
+
},
|
|
1684
|
+
COMMON_CHECKS.hasRelations,
|
|
1685
|
+
COMMON_CHECKS.hasType,
|
|
1686
|
+
{
|
|
1687
|
+
id: "has-test-method",
|
|
1688
|
+
label: "Test method described",
|
|
1689
|
+
check: (ctx) => !!ctx.data?.testMethod && String(ctx.data.testMethod).length > 0,
|
|
1690
|
+
suggestion: () => "Describe how this assumption could be tested or validated."
|
|
1691
|
+
}
|
|
1692
|
+
]
|
|
1693
|
+
}],
|
|
1694
|
+
["architecture", {
|
|
1695
|
+
governedDraft: true,
|
|
1696
|
+
descriptionField: "description",
|
|
1697
|
+
defaults: [],
|
|
1698
|
+
recommendedRelationTypes: ["belongs_to", "depends_on", "governs", "references", "related_to"],
|
|
1699
|
+
qualityChecks: [
|
|
1700
|
+
COMMON_CHECKS.clearName,
|
|
1701
|
+
COMMON_CHECKS.hasDescription,
|
|
1702
|
+
COMMON_CHECKS.hasRelations,
|
|
1703
|
+
COMMON_CHECKS.hasType,
|
|
1704
|
+
{
|
|
1705
|
+
id: "has-layer",
|
|
1706
|
+
label: "Architecture layer identified",
|
|
1707
|
+
check: (ctx) => !!ctx.data?.layer && String(ctx.data.layer).length > 0,
|
|
1708
|
+
suggestion: () => "Specify which architecture layer this belongs to (L1-L7)."
|
|
1709
|
+
}
|
|
1710
|
+
]
|
|
1711
|
+
}],
|
|
1712
|
+
["landscape", {
|
|
1713
|
+
governedDraft: false,
|
|
1714
|
+
descriptionField: "description",
|
|
1715
|
+
defaults: [],
|
|
1716
|
+
recommendedRelationTypes: ["references", "related_to", "fills_slot"],
|
|
1717
|
+
qualityChecks: [
|
|
1718
|
+
COMMON_CHECKS.clearName,
|
|
1719
|
+
COMMON_CHECKS.hasDescription,
|
|
1720
|
+
COMMON_CHECKS.hasRelations,
|
|
1721
|
+
COMMON_CHECKS.hasType
|
|
1722
|
+
]
|
|
1723
|
+
}]
|
|
1724
|
+
]);
|
|
1725
|
+
var FALLBACK_PROFILE = {
|
|
1726
|
+
governedDraft: false,
|
|
1727
|
+
descriptionField: "description",
|
|
1728
|
+
defaults: [],
|
|
1729
|
+
recommendedRelationTypes: ["related_to", "references"],
|
|
1730
|
+
qualityChecks: [
|
|
1731
|
+
COMMON_CHECKS.clearName,
|
|
1732
|
+
COMMON_CHECKS.hasDescription,
|
|
1733
|
+
COMMON_CHECKS.hasRelations,
|
|
1734
|
+
COMMON_CHECKS.hasType
|
|
1735
|
+
]
|
|
1736
|
+
};
|
|
1737
|
+
function extractSearchTerms(name, description) {
|
|
1738
|
+
const text = `${name} ${description}`;
|
|
1739
|
+
return text.replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length > 3).slice(0, 8).join(" ");
|
|
1740
|
+
}
|
|
1741
|
+
function computeLinkConfidence(candidate, sourceName, sourceDescription, sourceCollection, candidateCollection) {
|
|
1742
|
+
const text = `${sourceName} ${sourceDescription}`.toLowerCase();
|
|
1743
|
+
const candidateName = candidate.name.toLowerCase();
|
|
1744
|
+
let score = 0;
|
|
1745
|
+
const reasons = [];
|
|
1746
|
+
if (text.includes(candidateName) && candidateName.length > 3) {
|
|
1747
|
+
score += 40;
|
|
1748
|
+
reasons.push("name match");
|
|
1749
|
+
}
|
|
1750
|
+
const candidateWords = candidateName.split(/\s+/).filter((w) => w.length > 3);
|
|
1751
|
+
const matchingWords = candidateWords.filter((w) => text.includes(w));
|
|
1752
|
+
const wordScore = matchingWords.length / Math.max(candidateWords.length, 1) * 30;
|
|
1753
|
+
score += wordScore;
|
|
1754
|
+
if (matchingWords.length > 0) {
|
|
1755
|
+
reasons.push(`word overlap (${matchingWords.slice(0, 3).join(", ")})`);
|
|
1756
|
+
}
|
|
1757
|
+
const HUB_COLLECTIONS = /* @__PURE__ */ new Set(["strategy", "features"]);
|
|
1758
|
+
if (HUB_COLLECTIONS.has(candidateCollection)) {
|
|
1759
|
+
score += 15;
|
|
1760
|
+
reasons.push("hub collection");
|
|
1761
|
+
}
|
|
1762
|
+
if (candidateCollection !== sourceCollection) {
|
|
1763
|
+
score += 10;
|
|
1764
|
+
reasons.push("cross-collection");
|
|
1765
|
+
}
|
|
1766
|
+
const finalScore = Math.min(score, 100);
|
|
1767
|
+
const reason = reasons.length > 0 ? reasons.join(" + ") : "low relevance";
|
|
1768
|
+
return { score: finalScore, reason };
|
|
1769
|
+
}
|
|
1770
|
+
function inferRelationType(sourceCollection, targetCollection, profile) {
|
|
1771
|
+
const typeMap = {
|
|
1772
|
+
tensions: {
|
|
1773
|
+
glossary: "surfaces_tension_in",
|
|
1774
|
+
"business-rules": "references",
|
|
1775
|
+
strategy: "belongs_to",
|
|
1776
|
+
features: "surfaces_tension_in",
|
|
1777
|
+
decisions: "references"
|
|
1778
|
+
},
|
|
1779
|
+
"business-rules": {
|
|
1780
|
+
glossary: "references",
|
|
1781
|
+
features: "governs",
|
|
1782
|
+
strategy: "belongs_to",
|
|
1783
|
+
tensions: "references"
|
|
1784
|
+
},
|
|
1785
|
+
glossary: {
|
|
1786
|
+
features: "defines_term_for",
|
|
1787
|
+
"business-rules": "references",
|
|
1788
|
+
strategy: "references"
|
|
1789
|
+
},
|
|
1790
|
+
decisions: {
|
|
1791
|
+
features: "informs",
|
|
1792
|
+
"business-rules": "references",
|
|
1793
|
+
strategy: "references",
|
|
1794
|
+
tensions: "references"
|
|
1795
|
+
}
|
|
1796
|
+
};
|
|
1797
|
+
const mapped = typeMap[sourceCollection]?.[targetCollection];
|
|
1798
|
+
const type = mapped ?? profile.recommendedRelationTypes[0] ?? "related_to";
|
|
1799
|
+
const reason = mapped ? `collection pair (${sourceCollection} \u2192 ${targetCollection})` : `profile default (${profile.recommendedRelationTypes[0] ?? "related_to"})`;
|
|
1800
|
+
return { type, reason };
|
|
1801
|
+
}
|
|
1802
|
+
function scoreQuality(ctx, profile) {
|
|
1803
|
+
const checks = profile.qualityChecks.map((qc) => {
|
|
1804
|
+
const passed2 = qc.check(ctx);
|
|
1805
|
+
return {
|
|
1806
|
+
id: qc.id,
|
|
1807
|
+
label: qc.label,
|
|
1808
|
+
passed: passed2,
|
|
1809
|
+
suggestion: passed2 ? void 0 : qc.suggestion?.(ctx)
|
|
1810
|
+
};
|
|
1811
|
+
});
|
|
1812
|
+
const passed = checks.filter((c) => c.passed).length;
|
|
1813
|
+
const total = checks.length;
|
|
1814
|
+
const score = total > 0 ? Math.round(passed / total * 10) : 10;
|
|
1815
|
+
return { score, maxScore: 10, checks };
|
|
1816
|
+
}
|
|
1817
|
+
function formatQualityReport(result) {
|
|
1818
|
+
const failed = result.checks.filter((c) => !c.passed);
|
|
1819
|
+
const reason = failed.length > 0 ? ` because ${failed.map((c) => c.suggestion ?? c.label.toLowerCase()).join("; ")}` : "";
|
|
1820
|
+
const lines = [`## Quality: ${result.score}/${result.maxScore}${reason}`];
|
|
1821
|
+
for (const check of result.checks) {
|
|
1822
|
+
const icon = check.passed ? "[x]" : "[ ]";
|
|
1823
|
+
const suggestion = check.passed ? "" : ` \u2014 ${check.suggestion ?? check.label}`;
|
|
1824
|
+
lines.push(`${icon} ${check.label}${suggestion}`);
|
|
1825
|
+
}
|
|
1826
|
+
return lines.join("\n");
|
|
1827
|
+
}
|
|
1828
|
+
async function checkEntryQuality(entryId) {
|
|
1829
|
+
const entry = await mcpQuery("chain.getEntry", { entryId });
|
|
1830
|
+
if (!entry) {
|
|
1831
|
+
return {
|
|
1832
|
+
text: `Entry \`${entryId}\` not found. Try search to find the right ID.`,
|
|
1833
|
+
quality: { score: 0, maxScore: 10, checks: [] }
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
const collections = await mcpQuery("chain.listCollections");
|
|
1837
|
+
const collMap = /* @__PURE__ */ new Map();
|
|
1838
|
+
for (const c of collections) collMap.set(c._id, c.slug);
|
|
1839
|
+
const collectionSlug = collMap.get(entry.collectionId) ?? "unknown";
|
|
1840
|
+
const profile = PROFILES.get(collectionSlug) ?? FALLBACK_PROFILE;
|
|
1841
|
+
const relations = await mcpQuery("chain.listEntryRelations", { entryId });
|
|
1842
|
+
const linksCreated = [];
|
|
1843
|
+
for (const r of relations) {
|
|
1844
|
+
const otherId = r.fromId === entry._id ? r.toId : r.fromId;
|
|
1845
|
+
linksCreated.push({
|
|
1846
|
+
targetEntryId: otherId,
|
|
1847
|
+
targetName: "",
|
|
1848
|
+
targetCollection: "",
|
|
1849
|
+
relationType: r.type
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
const descField = profile.descriptionField;
|
|
1853
|
+
const description = typeof entry.data?.[descField] === "string" ? entry.data[descField] : "";
|
|
1854
|
+
const ctx = {
|
|
1855
|
+
collection: collectionSlug,
|
|
1856
|
+
name: entry.name,
|
|
1857
|
+
description,
|
|
1858
|
+
data: entry.data ?? {},
|
|
1859
|
+
entryId: entry.entryId ?? "",
|
|
1860
|
+
canonicalKey: entry.canonicalKey,
|
|
1861
|
+
linksCreated,
|
|
1862
|
+
linksSuggested: [],
|
|
1863
|
+
collectionFields: []
|
|
1864
|
+
};
|
|
1865
|
+
const quality = scoreQuality(ctx, profile);
|
|
1866
|
+
const lines = [
|
|
1867
|
+
`# Quality Check: ${entry.entryId ?? entry.name}`,
|
|
1868
|
+
`**${entry.name}** in \`${collectionSlug}\` [${entry.status}]`,
|
|
1869
|
+
"",
|
|
1870
|
+
formatQualityReport(quality)
|
|
1871
|
+
];
|
|
1872
|
+
if (quality.score < 10) {
|
|
1873
|
+
const failedChecks = quality.checks.filter((c) => !c.passed && c.suggestion);
|
|
1874
|
+
if (failedChecks.length > 0) {
|
|
1875
|
+
lines.push("");
|
|
1876
|
+
lines.push(`_To improve: use \`update-entry\` to fill missing fields, or \`relations action=create\` to add connections._`);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
return { text: lines.join("\n"), quality };
|
|
1880
|
+
}
|
|
1881
|
+
var GOVERNED_COLLECTIONS = /* @__PURE__ */ new Set([
|
|
1882
|
+
"glossary",
|
|
1883
|
+
"business-rules",
|
|
1884
|
+
"principles",
|
|
1885
|
+
"standards",
|
|
1886
|
+
"strategy",
|
|
1887
|
+
"features",
|
|
1888
|
+
"architecture"
|
|
1889
|
+
]);
|
|
1890
|
+
var AUTO_LINK_CONFIDENCE_THRESHOLD = 35;
|
|
1891
|
+
var MAX_AUTO_LINKS = 5;
|
|
1892
|
+
var MAX_SUGGESTIONS = 5;
|
|
1893
|
+
var CAPTURE_WITHOUT_THINKING_FLAG = "capture-without-thinking";
|
|
1894
|
+
var BR_STD_ENTRY_ID_REGEX = /^(BR|STD)-\d+$/;
|
|
1895
|
+
var entryIdSchema = z2.string().regex(BR_STD_ENTRY_ID_REGEX).optional().describe(
|
|
1896
|
+
"Only for business-rules and standards. Must match BR-NNN or STD-NNN. Omit for all other collections \u2014 IDs are auto-generated."
|
|
1897
|
+
);
|
|
1898
|
+
var captureSchema = z2.object({
|
|
1899
|
+
collection: z2.string().optional().describe("Collection slug, e.g. 'tensions', 'business-rules', 'glossary', 'decisions'. Optional when `capture-without-thinking` is enabled."),
|
|
1900
|
+
name: z2.string().describe("Display name \u2014 be specific (e.g. 'Convex adjacency list won't scale for graph traversal')"),
|
|
1901
|
+
description: z2.string().describe("Full context \u2014 what's happening, why it matters, what you observed"),
|
|
1902
|
+
context: z2.string().optional().describe("Optional additional context (e.g. 'Observed during context gather calls taking 700ms+')"),
|
|
1903
|
+
entryId: entryIdSchema,
|
|
1904
|
+
canonicalKey: z2.string().optional().describe("Semantic type (e.g. 'decision', 'tension', 'vision'). Auto-assigned from collection if omitted."),
|
|
1905
|
+
data: z2.record(z2.unknown()).optional().describe("Explicit field values when you know the schema (e.g. canonical_key, cardinality_rule, required_fields). Merged with inferred values; user-provided wins."),
|
|
1906
|
+
links: z2.array(z2.object({
|
|
1907
|
+
to: z2.string().describe("Target entry ID (e.g. 'BR-64', 'ARCH-8')"),
|
|
1908
|
+
type: z2.string().describe("Relation type (e.g. 'governs', 'related_to', 'informs')")
|
|
1909
|
+
})).optional().describe("Relations to create after capture. Skips auto-link discovery when provided."),
|
|
1910
|
+
autoCommit: z2.boolean().optional().describe(
|
|
1911
|
+
"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."
|
|
1912
|
+
)
|
|
1913
|
+
});
|
|
1914
|
+
var batchCaptureSchema = z2.object({
|
|
1915
|
+
entries: z2.array(z2.object({
|
|
1916
|
+
collection: z2.string().optional().describe("Collection slug. Optional \u2014 auto-classified via LLM when omitted (FEAT-160)."),
|
|
1917
|
+
name: z2.string().describe("Display name"),
|
|
1918
|
+
description: z2.string().describe("Full context / definition"),
|
|
1919
|
+
entryId: entryIdSchema
|
|
1920
|
+
})).min(1).max(50).describe("Array of entries to capture"),
|
|
1921
|
+
autoCommit: z2.boolean().optional().describe(
|
|
1922
|
+
"If true, commits created entries immediately after linking. If omitted, Open mode workspaces commit by default and consensus/role modes stay draft-first."
|
|
1923
|
+
)
|
|
1924
|
+
});
|
|
1925
|
+
var captureClassifierSchema = z2.object({
|
|
1926
|
+
enabled: z2.boolean(),
|
|
1927
|
+
autoRouted: z2.boolean(),
|
|
1928
|
+
agrees: z2.boolean(),
|
|
1929
|
+
abstained: z2.boolean(),
|
|
1930
|
+
topConfidence: z2.number(),
|
|
1931
|
+
confidence: z2.number(),
|
|
1932
|
+
reasons: z2.array(z2.string()),
|
|
1933
|
+
candidates: z2.array(
|
|
1934
|
+
z2.object({
|
|
1935
|
+
collection: z2.string(),
|
|
1936
|
+
signalScore: z2.number().optional(),
|
|
1937
|
+
confidence: z2.number()
|
|
1938
|
+
})
|
|
1939
|
+
),
|
|
1940
|
+
agentProvidedCollection: z2.string().optional(),
|
|
1941
|
+
overrideCommand: z2.string().optional(),
|
|
1942
|
+
classifiedBy: z2.enum(["llm", "heuristic", "explicit"]).optional(),
|
|
1943
|
+
confidenceTier: z2.enum(["high", "medium", "low"]).optional(),
|
|
1944
|
+
reasoning: z2.string().optional()
|
|
1945
|
+
});
|
|
1946
|
+
function trackClassifierTelemetry(params) {
|
|
1947
|
+
const telemetry = {
|
|
1948
|
+
predicted_collection: params.predictedCollection,
|
|
1949
|
+
confidence: params.confidence,
|
|
1950
|
+
auto_routed: params.autoRouted,
|
|
1951
|
+
reason_category: params.reasonCategory,
|
|
1952
|
+
explicit_collection_provided: params.explicitCollectionProvided
|
|
1953
|
+
};
|
|
1954
|
+
trackCaptureClassifierEvaluated(params.workspaceId, telemetry);
|
|
1955
|
+
if (params.outcome === "auto-routed") {
|
|
1956
|
+
trackCaptureClassifierAutoRouted(params.workspaceId, telemetry);
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
trackCaptureClassifierFallback(params.workspaceId, telemetry);
|
|
1960
|
+
}
|
|
1961
|
+
function buildCollectionRequiredResult() {
|
|
1962
|
+
return {
|
|
1963
|
+
content: [{
|
|
1964
|
+
type: "text",
|
|
1965
|
+
text: "Collection is required unless `capture-without-thinking` is enabled.\n\nProvide `collection` explicitly, or enable the feature flag for this workspace."
|
|
1966
|
+
}],
|
|
1967
|
+
structuredContent: failure(
|
|
1968
|
+
"VALIDATION_ERROR",
|
|
1969
|
+
"Collection is required unless capture-without-thinking is enabled.",
|
|
1970
|
+
"Provide collection explicitly, or enable the feature flag.",
|
|
1971
|
+
[{ tool: "collections", description: "List available collections", parameters: { action: "list" } }]
|
|
1972
|
+
)
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
function buildClassifierUnknownResult() {
|
|
1976
|
+
return {
|
|
1977
|
+
content: [{
|
|
1978
|
+
type: "text",
|
|
1979
|
+
text: "I could not infer a collection confidently from this input.\n\nPlease provide `collection`, or rewrite with clearer intent (decision/problem/definition/insight/bet)."
|
|
1980
|
+
}],
|
|
1981
|
+
structuredContent: failure(
|
|
1982
|
+
"VALIDATION_ERROR",
|
|
1983
|
+
"Could not infer collection from input.",
|
|
1984
|
+
"Provide collection explicitly, or rewrite with clearer intent.",
|
|
1985
|
+
[{ tool: "collections", description: "List available collections", parameters: { action: "list" } }],
|
|
1986
|
+
{ classifier: { enabled: true, autoRouted: false, agrees: false, abstained: true, topConfidence: 0, confidence: 0, reasons: [], candidates: [] } }
|
|
1987
|
+
)
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
function buildLowConfidenceResult(resolved, classifierMeta) {
|
|
1991
|
+
const allCandidates = [
|
|
1992
|
+
{ collection: resolved.collection, confidence: resolved.confidence },
|
|
1993
|
+
...resolved.alternatives
|
|
1994
|
+
];
|
|
1995
|
+
const suggestions = allCandidates.map((c) => `- \`${c.collection}\` (${c.confidence}% confidence)`).join("\n");
|
|
1996
|
+
const textBody = `Low confidence classification (${resolved.confidence}%, classified by ${resolved.classifiedBy}).
|
|
1997
|
+
|
|
1998
|
+
Predicted: \`${resolved.collection}\`
|
|
1999
|
+
Reason: ${resolved.reasoning || "low signal"}
|
|
2000
|
+
|
|
2001
|
+
Choose one of these and retry with \`collection\`:
|
|
2002
|
+
${suggestions}
|
|
2003
|
+
|
|
2004
|
+
Correction path: rerun with your chosen \`collection\`, or capture and use \`entries action=move\` later.`;
|
|
2005
|
+
return {
|
|
2006
|
+
content: [{ type: "text", text: textBody }],
|
|
2007
|
+
structuredContent: failure(
|
|
2008
|
+
"VALIDATION_ERROR",
|
|
2009
|
+
`Low confidence routing to '${resolved.collection}' (${resolved.confidence}%, ${resolved.classifiedBy}).`,
|
|
2010
|
+
"Rerun with explicit collection.",
|
|
2011
|
+
allCandidates.map((c) => ({
|
|
2012
|
+
tool: "capture",
|
|
2013
|
+
description: `Capture to ${c.collection}`,
|
|
2014
|
+
parameters: { collection: c.collection }
|
|
2015
|
+
})),
|
|
2016
|
+
{ classifier: classifierMeta }
|
|
2017
|
+
)
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
function buildClassifierMeta(resolved, overrides = {}) {
|
|
2021
|
+
return {
|
|
2022
|
+
enabled: true,
|
|
2023
|
+
autoRouted: false,
|
|
2024
|
+
agrees: true,
|
|
2025
|
+
abstained: false,
|
|
2026
|
+
topConfidence: resolved.confidence,
|
|
2027
|
+
confidence: resolved.confidence,
|
|
2028
|
+
reasons: resolved.reasoning ? [resolved.reasoning] : [],
|
|
2029
|
+
candidates: [
|
|
2030
|
+
{ collection: resolved.collection, confidence: resolved.confidence },
|
|
2031
|
+
...resolved.alternatives.map((a) => ({ collection: a.collection, confidence: a.confidence }))
|
|
2032
|
+
].slice(0, 3),
|
|
2033
|
+
classifiedBy: resolved.classifiedBy,
|
|
2034
|
+
confidenceTier: resolved.tier,
|
|
2035
|
+
reasoning: resolved.reasoning,
|
|
2036
|
+
...overrides
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
async function resolveCaptureCollection(params) {
|
|
2040
|
+
const {
|
|
2041
|
+
collection,
|
|
2042
|
+
name,
|
|
2043
|
+
description,
|
|
2044
|
+
classifierFlagOn,
|
|
2045
|
+
workspaceId,
|
|
2046
|
+
explicitCollectionProvided
|
|
2047
|
+
} = params;
|
|
2048
|
+
if (collection) {
|
|
2049
|
+
const resolved2 = await resolveCollection({ name, description });
|
|
2050
|
+
if (resolved2) {
|
|
2051
|
+
const agrees = resolved2.collection === collection;
|
|
2052
|
+
const classifierMeta2 = buildClassifierMeta(resolved2, {
|
|
2053
|
+
autoRouted: false,
|
|
2054
|
+
agrees,
|
|
2055
|
+
agentProvidedCollection: collection,
|
|
2056
|
+
...!agrees && {
|
|
2057
|
+
overrideCommand: `capture collection="${resolved2.collection}"`
|
|
2058
|
+
}
|
|
2059
|
+
});
|
|
2060
|
+
if (!agrees) {
|
|
2061
|
+
trackClassifierTelemetry({
|
|
2062
|
+
workspaceId,
|
|
2063
|
+
predictedCollection: resolved2.collection,
|
|
2064
|
+
confidence: resolved2.confidence,
|
|
2065
|
+
autoRouted: false,
|
|
2066
|
+
reasonCategory: "low-confidence",
|
|
2067
|
+
explicitCollectionProvided: true,
|
|
2068
|
+
outcome: "fallback"
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
return { resolvedCollection: collection, classifierMeta: classifierMeta2 };
|
|
2072
|
+
}
|
|
2073
|
+
return {
|
|
2074
|
+
resolvedCollection: collection,
|
|
2075
|
+
classifierMeta: {
|
|
2076
|
+
enabled: true,
|
|
2077
|
+
autoRouted: false,
|
|
2078
|
+
agrees: false,
|
|
2079
|
+
abstained: true,
|
|
2080
|
+
topConfidence: 0,
|
|
2081
|
+
confidence: 0,
|
|
2082
|
+
reasons: [],
|
|
2083
|
+
candidates: [],
|
|
2084
|
+
agentProvidedCollection: collection
|
|
2085
|
+
}
|
|
2086
|
+
};
|
|
2087
|
+
}
|
|
2088
|
+
if (!classifierFlagOn) {
|
|
2089
|
+
return { earlyResult: buildCollectionRequiredResult() };
|
|
2090
|
+
}
|
|
2091
|
+
const resolved = await resolveCollection({ name, description });
|
|
2092
|
+
if (!resolved) {
|
|
2093
|
+
trackClassifierTelemetry({
|
|
2094
|
+
workspaceId,
|
|
2095
|
+
predictedCollection: "unknown",
|
|
2096
|
+
confidence: 0,
|
|
2097
|
+
autoRouted: false,
|
|
2098
|
+
reasonCategory: "low-confidence",
|
|
2099
|
+
explicitCollectionProvided,
|
|
2100
|
+
outcome: "fallback"
|
|
2101
|
+
});
|
|
2102
|
+
return { earlyResult: buildClassifierUnknownResult() };
|
|
2103
|
+
}
|
|
2104
|
+
const autoRoute = resolved.tier !== "low";
|
|
2105
|
+
const classifierMeta = buildClassifierMeta(resolved, { autoRouted: autoRoute });
|
|
2106
|
+
if (!autoRoute) {
|
|
2107
|
+
trackClassifierTelemetry({
|
|
2108
|
+
workspaceId,
|
|
2109
|
+
predictedCollection: resolved.collection,
|
|
2110
|
+
confidence: resolved.confidence,
|
|
2111
|
+
autoRouted: false,
|
|
2112
|
+
reasonCategory: "low-confidence",
|
|
2113
|
+
explicitCollectionProvided,
|
|
2114
|
+
outcome: "fallback"
|
|
2115
|
+
});
|
|
2116
|
+
return {
|
|
2117
|
+
classifierMeta,
|
|
2118
|
+
earlyResult: buildLowConfidenceResult(resolved, classifierMeta)
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
trackClassifierTelemetry({
|
|
2122
|
+
workspaceId,
|
|
2123
|
+
predictedCollection: resolved.collection,
|
|
2124
|
+
confidence: resolved.confidence,
|
|
2125
|
+
autoRouted: true,
|
|
2126
|
+
reasonCategory: "auto-routed",
|
|
2127
|
+
explicitCollectionProvided,
|
|
2128
|
+
outcome: "auto-routed"
|
|
2129
|
+
});
|
|
2130
|
+
return {
|
|
2131
|
+
resolvedCollection: resolved.collection,
|
|
2132
|
+
classifierMeta
|
|
2133
|
+
};
|
|
2134
|
+
}
|
|
2135
|
+
var captureSuccessOutputSchema = z2.object({
|
|
2136
|
+
entryId: z2.string(),
|
|
2137
|
+
collection: z2.string(),
|
|
2138
|
+
name: z2.string(),
|
|
2139
|
+
status: z2.enum(["draft", "committed", "proposed"]),
|
|
2140
|
+
qualityScore: z2.number(),
|
|
2141
|
+
qualityVerdict: z2.record(z2.unknown()).optional(),
|
|
2142
|
+
classifier: captureClassifierSchema.optional(),
|
|
2143
|
+
studioUrl: z2.string().optional(),
|
|
2144
|
+
warnings: z2.array(z2.string()).optional(),
|
|
2145
|
+
normalization: z2.object({
|
|
2146
|
+
remapped: z2.record(z2.string()).optional(),
|
|
2147
|
+
rejected: z2.array(z2.string()).optional()
|
|
2148
|
+
}).optional(),
|
|
2149
|
+
expectedFields: z2.array(z2.object({
|
|
2150
|
+
key: z2.string(),
|
|
2151
|
+
type: z2.string(),
|
|
2152
|
+
required: z2.boolean().optional()
|
|
2153
|
+
})).optional()
|
|
2154
|
+
}).strict();
|
|
2155
|
+
var captureClassifierOnlyOutputSchema = z2.object({
|
|
2156
|
+
classifier: captureClassifierSchema
|
|
2157
|
+
}).strict();
|
|
2158
|
+
var captureOutputSchema = z2.union([
|
|
2159
|
+
captureSuccessOutputSchema,
|
|
2160
|
+
captureClassifierOnlyOutputSchema
|
|
2161
|
+
]);
|
|
2162
|
+
var batchCaptureOutputSchema = z2.object({
|
|
2163
|
+
captured: z2.array(z2.object({
|
|
2164
|
+
entryId: z2.string(),
|
|
2165
|
+
collection: z2.string(),
|
|
2166
|
+
name: z2.string(),
|
|
2167
|
+
status: z2.enum(["draft", "committed", "proposed"]),
|
|
2168
|
+
classifiedBy: z2.enum(["llm", "heuristic", "explicit"]).optional(),
|
|
2169
|
+
confidence: z2.number().optional(),
|
|
2170
|
+
confidenceTier: z2.enum(["high", "medium", "low"]).optional(),
|
|
2171
|
+
warnings: z2.array(z2.string()).optional(),
|
|
2172
|
+
normalization: z2.object({
|
|
2173
|
+
remapped: z2.record(z2.string()).optional(),
|
|
2174
|
+
rejected: z2.array(z2.string()).optional()
|
|
2175
|
+
}).optional()
|
|
2176
|
+
})),
|
|
2177
|
+
total: z2.number(),
|
|
2178
|
+
failed: z2.number(),
|
|
2179
|
+
committed: z2.number(),
|
|
2180
|
+
proposed: z2.number(),
|
|
2181
|
+
drafts: z2.number(),
|
|
2182
|
+
classified: z2.number().optional(),
|
|
2183
|
+
autoCommitApplied: z2.boolean(),
|
|
2184
|
+
skippedLowConfidence: z2.array(z2.object({
|
|
2185
|
+
index: z2.number(),
|
|
2186
|
+
name: z2.string(),
|
|
2187
|
+
suggestedCollection: z2.string().optional(),
|
|
2188
|
+
confidence: z2.number().optional(),
|
|
2189
|
+
alternatives: z2.array(z2.object({
|
|
2190
|
+
collection: z2.string(),
|
|
2191
|
+
confidence: z2.number()
|
|
2192
|
+
})).optional()
|
|
2193
|
+
})).optional(),
|
|
2194
|
+
failedEntries: z2.array(z2.object({
|
|
2195
|
+
index: z2.number(),
|
|
2196
|
+
collection: z2.string(),
|
|
2197
|
+
name: z2.string(),
|
|
2198
|
+
error: z2.string()
|
|
2199
|
+
})).optional()
|
|
2200
|
+
});
|
|
2201
|
+
function shouldAutoCommitCapture(autoCommit, governanceMode) {
|
|
2202
|
+
return autoCommit === true || autoCommit === void 0 && governanceMode === "open";
|
|
2203
|
+
}
|
|
2204
|
+
function buildDataFromFields(fields, descriptionField, descriptionValue) {
|
|
2205
|
+
const data = {};
|
|
2206
|
+
for (const field of fields) {
|
|
2207
|
+
if (field.key === descriptionField) {
|
|
2208
|
+
data[field.key] = descriptionValue;
|
|
2209
|
+
} else {
|
|
2210
|
+
data[field.key] = FIELD_TYPE_DEFAULTS[field.type] ?? null;
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
return data;
|
|
2214
|
+
}
|
|
2215
|
+
function registerSmartCaptureTools(server) {
|
|
2216
|
+
const captureTool = server.registerTool(
|
|
2217
|
+
"capture",
|
|
2218
|
+
{
|
|
2219
|
+
title: "Capture",
|
|
2220
|
+
description: "The single tool for creating knowledge entries. Creates an entry, auto-links related entries, and returns a quality scorecard \u2014 all in one call. Provide a name and description; `collection` is optional when `capture-without-thinking` is enabled.\n\nSupported collections with smart profiles: tensions, business-rules, glossary, decisions, features, audiences, strategy, standards, maps, chains, insights, assumptions, principles, tracking-events.\nAll other collections get an ENT-{random} ID and sensible defaults.\n\n**Explicit data:** When you know the schema, pass `data: { field: value }` to set fields directly. Top-level `name` and `description` always win for those fields. `data` wins over inference for all other fields.\n\n**Compound capture:** Pass `links` to create relations in the same call (skips auto-link discovery). In Open mode, entries commit by default when `autoCommit` is omitted. Pass `autoCommit: true` to promote the entry from draft to SSOT immediately after linking when you want to be explicit. Governed collections (glossary, business-rules, principles, standards, strategy, features, architecture) will warn but still commit \u2014 use only when you're certain.\n\nEntries are created as `draft` first, then may publish immediately depending on governance and `autoCommit`. Use `update-entry` for post-creation adjustments.",
|
|
2221
|
+
inputSchema: captureSchema.shape,
|
|
2222
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
2223
|
+
},
|
|
2224
|
+
withEnvelope(async ({ collection, name, description, context, entryId, canonicalKey, data: userData, links, autoCommit }) => {
|
|
2225
|
+
requireWriteAccess();
|
|
2226
|
+
const wsCtx = await getWorkspaceContext();
|
|
2227
|
+
const explicitCollectionProvided = typeof collection === "string" && collection.trim().length > 0;
|
|
2228
|
+
const classifierFlagOn = await isFeatureEnabled(
|
|
2229
|
+
CAPTURE_WITHOUT_THINKING_FLAG,
|
|
2230
|
+
wsCtx.workspaceId,
|
|
2231
|
+
wsCtx.workspaceSlug
|
|
2232
|
+
);
|
|
2233
|
+
const resolution = await resolveCaptureCollection({
|
|
2234
|
+
collection,
|
|
2235
|
+
name,
|
|
2236
|
+
description,
|
|
2237
|
+
classifierFlagOn,
|
|
2238
|
+
workspaceId: wsCtx.workspaceId,
|
|
2239
|
+
explicitCollectionProvided
|
|
2240
|
+
});
|
|
2241
|
+
if (resolution.earlyResult) {
|
|
2242
|
+
return resolution.earlyResult;
|
|
2243
|
+
}
|
|
2244
|
+
let resolvedCollection = resolution.resolvedCollection;
|
|
2245
|
+
const classifierMeta = resolution.classifierMeta;
|
|
2246
|
+
if (!resolvedCollection) {
|
|
2247
|
+
return buildCollectionRequiredResult();
|
|
2248
|
+
}
|
|
2249
|
+
if (entryId && resolvedCollection !== "business-rules" && resolvedCollection !== "standards") {
|
|
2250
|
+
return {
|
|
2251
|
+
content: [{
|
|
2252
|
+
type: "text",
|
|
2253
|
+
text: `# BR-111: entryId Not Allowed
|
|
2254
|
+
|
|
2255
|
+
\`entryId\` is only valid for \`business-rules\` and \`standards\` collections. For \`${resolvedCollection}\`, omit \`entryId\` \u2014 IDs are auto-generated.`
|
|
2256
|
+
}],
|
|
2257
|
+
structuredContent: failure(
|
|
2258
|
+
"INVALID_ENTRY_ID",
|
|
2259
|
+
`entryId is only allowed for business-rules and standards. Omit for ${resolvedCollection}.`,
|
|
2260
|
+
"Remove entryId from your capture call.",
|
|
2261
|
+
[]
|
|
2262
|
+
)
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
const profile = PROFILES.get(resolvedCollection) ?? FALLBACK_PROFILE;
|
|
2266
|
+
const col = await mcpQuery("chain.getCollection", { slug: resolvedCollection });
|
|
2267
|
+
if (!col) {
|
|
2268
|
+
const displayName = resolvedCollection.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
2269
|
+
return {
|
|
2270
|
+
content: [{
|
|
2271
|
+
type: "text",
|
|
2272
|
+
text: `Collection \`${resolvedCollection}\` not found.
|
|
2273
|
+
|
|
2274
|
+
**To create it**, run:
|
|
2275
|
+
\`\`\`
|
|
2276
|
+
collections action=create slug="${resolvedCollection}" name="${displayName}" description="..."
|
|
2277
|
+
\`\`\`
|
|
2278
|
+
|
|
2279
|
+
Or use \`collections action=list\` to see available collections.`
|
|
2280
|
+
}],
|
|
2281
|
+
structuredContent: failure(
|
|
2282
|
+
"NOT_FOUND",
|
|
2283
|
+
`Collection '${resolvedCollection}' not found.`,
|
|
2284
|
+
"Create the collection first, or use collections action=list to see available ones.",
|
|
2285
|
+
[
|
|
2286
|
+
{ tool: "collections", description: "Create collection", parameters: { action: "create", slug: resolvedCollection, name: displayName } },
|
|
2287
|
+
{ tool: "collections", description: "List collections", parameters: { action: "list" } }
|
|
2288
|
+
]
|
|
2289
|
+
)
|
|
2290
|
+
};
|
|
2291
|
+
}
|
|
2292
|
+
const data = buildDataFromFields(col.fields ?? [], profile.descriptionField, description);
|
|
2293
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
2294
|
+
for (const def of profile.defaults) {
|
|
2295
|
+
if (def.value === "today") {
|
|
2296
|
+
data[def.key] = today;
|
|
2297
|
+
} else if (def.value !== "infer") {
|
|
2298
|
+
data[def.key] = def.value;
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
if (profile.inferField) {
|
|
2302
|
+
const inferred = profile.inferField({
|
|
2303
|
+
collection: resolvedCollection,
|
|
2304
|
+
name,
|
|
2305
|
+
description,
|
|
2306
|
+
context,
|
|
2307
|
+
data,
|
|
2308
|
+
entryId: "",
|
|
2309
|
+
linksCreated: [],
|
|
2310
|
+
linksSuggested: [],
|
|
2311
|
+
collectionFields: col.fields ?? []
|
|
2312
|
+
});
|
|
2313
|
+
for (const [key, val] of Object.entries(inferred)) {
|
|
2314
|
+
if (val !== void 0 && val !== "") {
|
|
2315
|
+
data[key] = val;
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
if (userData && typeof userData === "object") {
|
|
2320
|
+
for (const [key, val] of Object.entries(userData)) {
|
|
2321
|
+
if (key !== "name") data[key] = val;
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
data[profile.descriptionField || "description"] = description;
|
|
2325
|
+
const isBetCapture = canonicalKey === "bet";
|
|
2326
|
+
if (isBetCapture) {
|
|
2327
|
+
data.chainTypeId = "bet";
|
|
2328
|
+
const betContentFields = ["problem", "appetite", "elements", "architecture", "rabbitHoles", "noGos", "doneWhen", "buildSequence", "buildContract", "solution"];
|
|
2329
|
+
const betLinks = {};
|
|
2330
|
+
for (const field of betContentFields) {
|
|
2331
|
+
if (data[field] !== void 0) {
|
|
2332
|
+
betLinks[field] = data[field];
|
|
2333
|
+
delete data[field];
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
data.links = betLinks;
|
|
2337
|
+
}
|
|
2338
|
+
const E2_LLM_REMAP_ENABLED = false;
|
|
2339
|
+
const payloadForPreview = E2_LLM_REMAP_ENABLED && userData && typeof userData === "object" ? { ...data } : null;
|
|
2340
|
+
if (payloadForPreview) {
|
|
2341
|
+
try {
|
|
2342
|
+
const preview = await mcpCall(
|
|
2343
|
+
"chain.normalizeEntryDataPreview",
|
|
2344
|
+
{ collectionSlug: resolvedCollection, data: payloadForPreview }
|
|
2345
|
+
);
|
|
2346
|
+
const rejected = (preview?.rejected ?? []).filter((k) => k !== "name");
|
|
2347
|
+
if (rejected.length > 0) {
|
|
2348
|
+
const llmResult = await mcpCall(
|
|
2349
|
+
"chain.normalizeEntryDataLLM",
|
|
2350
|
+
{
|
|
2351
|
+
collectionSlug: resolvedCollection,
|
|
2352
|
+
unknownKeys: rejected,
|
|
2353
|
+
data: payloadForPreview
|
|
2354
|
+
}
|
|
2355
|
+
);
|
|
2356
|
+
if (llmResult && Object.keys(llmResult.remapped).length > 0) {
|
|
2357
|
+
const fieldKeySet = new Set((col.fields ?? []).map((f) => f.key));
|
|
2358
|
+
for (const [oldKey, newKey] of Object.entries(llmResult.remapped)) {
|
|
2359
|
+
if (data[oldKey] !== void 0 && !fieldKeySet.has(oldKey)) {
|
|
2360
|
+
data[newKey] = data[oldKey];
|
|
2361
|
+
delete data[oldKey];
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
} catch {
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
const status = "draft";
|
|
2370
|
+
const agentId = getAgentSessionId();
|
|
2371
|
+
let finalEntryId;
|
|
2372
|
+
let internalId;
|
|
2373
|
+
let entryWarnings = [];
|
|
2374
|
+
let normalizationMeta;
|
|
2375
|
+
try {
|
|
2376
|
+
const result = await mcpMutation("chain.createEntry", {
|
|
2377
|
+
collectionSlug: resolvedCollection,
|
|
2378
|
+
entryId: entryId ?? void 0,
|
|
2379
|
+
name,
|
|
2380
|
+
status,
|
|
2381
|
+
data,
|
|
2382
|
+
canonicalKey,
|
|
2383
|
+
createdBy: agentId ? `agent:${agentId}` : "capture",
|
|
2384
|
+
sessionId: agentId ?? void 0
|
|
2385
|
+
});
|
|
2386
|
+
internalId = result.docId;
|
|
2387
|
+
finalEntryId = result.entryId;
|
|
2388
|
+
entryWarnings = result.warnings ?? [];
|
|
2389
|
+
normalizationMeta = result.normalization;
|
|
2390
|
+
resolveGapsForEntry(name, result.entryId);
|
|
2391
|
+
} catch (error) {
|
|
2392
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
2393
|
+
if (msg.includes("Duplicate") || msg.includes("already exists")) {
|
|
2394
|
+
return {
|
|
2395
|
+
content: [{
|
|
2396
|
+
type: "text",
|
|
2397
|
+
text: `# Cannot Capture \u2014 Duplicate Detected
|
|
2398
|
+
|
|
2399
|
+
${msg}
|
|
2400
|
+
|
|
2401
|
+
Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to modify it.`
|
|
2402
|
+
}],
|
|
2403
|
+
structuredContent: failure(
|
|
2404
|
+
"DUPLICATE",
|
|
2405
|
+
msg,
|
|
2406
|
+
"Use entries action=get to inspect the existing entry.",
|
|
2407
|
+
[
|
|
2408
|
+
{ tool: "entries", description: "Get existing entry", parameters: { action: "get", entryId: entryId ?? name } },
|
|
2409
|
+
{ tool: "update-entry", description: "Update existing entry", parameters: { entryId: entryId ?? "" } }
|
|
2410
|
+
]
|
|
2411
|
+
)
|
|
2412
|
+
};
|
|
2413
|
+
}
|
|
2414
|
+
throw error;
|
|
2415
|
+
}
|
|
2416
|
+
const linksCreated = [];
|
|
2417
|
+
const linksSuggested = [];
|
|
2418
|
+
const userLinkResults = [];
|
|
2419
|
+
const pendingRelations = [];
|
|
2420
|
+
const skipAutoDiscovery = links && links.length > 0;
|
|
2421
|
+
const searchQuery = extractSearchTerms(name, description);
|
|
2422
|
+
if (searchQuery && !skipAutoDiscovery) {
|
|
2423
|
+
const [searchResults, allCollections] = await Promise.all([
|
|
2424
|
+
mcpQuery("chain.searchEntries", { query: searchQuery }),
|
|
2425
|
+
mcpQuery("chain.listCollections")
|
|
2426
|
+
]);
|
|
2427
|
+
const collMap = /* @__PURE__ */ new Map();
|
|
2428
|
+
for (const c of allCollections) collMap.set(c._id, c.slug);
|
|
2429
|
+
const candidates = (searchResults ?? []).filter((r) => r.entryId !== finalEntryId && r._id !== internalId).map((r) => {
|
|
2430
|
+
const conf = computeLinkConfidence(r, name, description, resolvedCollection, collMap.get(r.collectionId) ?? "unknown");
|
|
2431
|
+
return {
|
|
2432
|
+
...r,
|
|
2433
|
+
collSlug: collMap.get(r.collectionId) ?? "unknown",
|
|
2434
|
+
confidence: conf.score,
|
|
2435
|
+
confidenceReason: conf.reason
|
|
2436
|
+
};
|
|
2437
|
+
}).sort((a, b) => b.confidence - a.confidence);
|
|
2438
|
+
let autoCount = 0;
|
|
2439
|
+
for (const c of candidates) {
|
|
2440
|
+
if (autoCount >= MAX_AUTO_LINKS) break;
|
|
2441
|
+
if (c.confidence < AUTO_LINK_CONFIDENCE_THRESHOLD) break;
|
|
2442
|
+
if (!c.entryId || !finalEntryId) continue;
|
|
2443
|
+
const { type: relationType, reason: relationReason } = inferRelationType(resolvedCollection, c.collSlug, profile);
|
|
2444
|
+
pendingRelations.push({
|
|
2445
|
+
fromEntryId: finalEntryId,
|
|
2446
|
+
toEntryId: c.entryId,
|
|
2447
|
+
type: relationType,
|
|
2448
|
+
meta: {
|
|
2449
|
+
source: "auto",
|
|
2450
|
+
targetName: c.name,
|
|
2451
|
+
targetCollection: c.collSlug,
|
|
2452
|
+
linkReason: `confidence ${c.confidence} (${c.confidenceReason}) + ${relationReason}`
|
|
2453
|
+
}
|
|
2454
|
+
});
|
|
2455
|
+
autoCount++;
|
|
2456
|
+
}
|
|
2457
|
+
const autoTargetIds = new Set(pendingRelations.map((r) => r.toEntryId));
|
|
2458
|
+
for (const c of candidates) {
|
|
2459
|
+
if (linksSuggested.length >= MAX_SUGGESTIONS) break;
|
|
2460
|
+
if (autoTargetIds.has(c.entryId)) continue;
|
|
2461
|
+
if (c.confidence < 10) continue;
|
|
2462
|
+
const preview = extractPreview(c.data, 80);
|
|
2463
|
+
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`;
|
|
2464
|
+
linksSuggested.push({ entryId: c.entryId, name: c.name, collection: c.collSlug, reason, preview });
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
if (links && links.length > 0 && finalEntryId) {
|
|
2468
|
+
for (const link of links) {
|
|
2469
|
+
pendingRelations.push({
|
|
2470
|
+
fromEntryId: finalEntryId,
|
|
2471
|
+
toEntryId: link.to,
|
|
2472
|
+
type: link.type,
|
|
2473
|
+
meta: { source: "user", targetName: link.to, targetCollection: "unknown" }
|
|
2474
|
+
});
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
const resolvedCK = canonicalKey;
|
|
2478
|
+
const batchRelationsPromise = pendingRelations.length > 0 ? mcpMutation(
|
|
2479
|
+
"chain.createEntryRelations",
|
|
2480
|
+
{
|
|
2481
|
+
relations: pendingRelations.slice(0, 25).map((r) => ({ fromEntryId: r.fromEntryId, toEntryId: r.toEntryId, type: r.type })),
|
|
2482
|
+
sessionId: agentId ?? void 0
|
|
2483
|
+
}
|
|
2484
|
+
).catch((err) => {
|
|
2485
|
+
entryWarnings.push(`Auto-linking partially failed: ${err instanceof Error ? err.message : "unknown error"}`);
|
|
2486
|
+
return null;
|
|
2487
|
+
}) : Promise.resolve(null);
|
|
2488
|
+
const cardinalityPromise = resolvedCK ? mcpQuery("chain.checkCardinalityWarning", { canonicalKey: resolvedCK }).catch(() => null) : Promise.resolve(null);
|
|
2489
|
+
const contradictionPromise = runContradictionCheck(name, description);
|
|
2490
|
+
const coachingPromise = mcpMutation("quality.evaluateHeuristicAndSchedule", {
|
|
2491
|
+
entryId: finalEntryId,
|
|
2492
|
+
context: "capture"
|
|
2493
|
+
}).catch(() => null);
|
|
2494
|
+
const [batchResult, cardinalityCheck, contradictionWarnings, verdictResult] = await Promise.all([
|
|
2495
|
+
batchRelationsPromise,
|
|
2496
|
+
cardinalityPromise,
|
|
2497
|
+
contradictionPromise,
|
|
2498
|
+
coachingPromise
|
|
2499
|
+
]);
|
|
2500
|
+
if (batchResult) {
|
|
2501
|
+
for (let i = 0; i < pendingRelations.length; i++) {
|
|
2502
|
+
const pending = pendingRelations[i];
|
|
2503
|
+
const result = batchResult.results[i];
|
|
2504
|
+
if (!result) continue;
|
|
2505
|
+
if (pending.meta.source === "auto") {
|
|
2506
|
+
if (result.ok) {
|
|
2507
|
+
linksCreated.push({
|
|
2508
|
+
targetEntryId: pending.toEntryId,
|
|
2509
|
+
targetName: pending.meta.targetName,
|
|
2510
|
+
targetCollection: pending.meta.targetCollection,
|
|
2511
|
+
relationType: pending.type,
|
|
2512
|
+
linkReason: pending.meta.linkReason
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
} else {
|
|
2516
|
+
if (result.ok) {
|
|
2517
|
+
userLinkResults.push({ label: `\u2713 ${pending.type} \u2192 ${pending.toEntryId}`, ok: true });
|
|
2518
|
+
linksCreated.push({
|
|
2519
|
+
targetEntryId: pending.toEntryId,
|
|
2520
|
+
targetName: pending.meta.targetName,
|
|
2521
|
+
targetCollection: pending.meta.targetCollection,
|
|
2522
|
+
relationType: pending.type
|
|
2523
|
+
});
|
|
2524
|
+
} else {
|
|
2525
|
+
userLinkResults.push({ label: `\u2717 ${pending.type} \u2192 ${pending.toEntryId}: ${result.error ?? "failed"}`, ok: false });
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
const captureCtx = {
|
|
2531
|
+
collection: resolvedCollection,
|
|
2532
|
+
name,
|
|
2533
|
+
description,
|
|
2534
|
+
context,
|
|
2535
|
+
data,
|
|
2536
|
+
entryId: finalEntryId,
|
|
2537
|
+
canonicalKey,
|
|
2538
|
+
linksCreated,
|
|
2539
|
+
linksSuggested,
|
|
2540
|
+
collectionFields: col.fields ?? []
|
|
2541
|
+
};
|
|
2542
|
+
const quality = scoreQuality(captureCtx, profile);
|
|
2543
|
+
const cardinalityWarning = cardinalityCheck?.warning ?? null;
|
|
2544
|
+
if (contradictionWarnings.length > 0) {
|
|
2545
|
+
await recordSessionActivity({ contradictionWarning: true });
|
|
2546
|
+
}
|
|
2547
|
+
let coachingSection = "";
|
|
2548
|
+
if (verdictResult?.verdict && verdictResult.verdict.tier !== "passive" && verdictResult.verdict.criteria.length > 0) {
|
|
2549
|
+
coachingSection = formatRubricCoaching(verdictResult);
|
|
2550
|
+
}
|
|
2551
|
+
if (verdictResult?.verdict) {
|
|
2552
|
+
try {
|
|
2553
|
+
const wsForTracking = await getWorkspaceContext();
|
|
2554
|
+
const v = verdictResult.verdict;
|
|
2555
|
+
const failedCount = (v.criteria ?? []).filter((c) => !c.passed).length;
|
|
2556
|
+
trackQualityVerdict(wsForTracking.workspaceId, {
|
|
2557
|
+
entry_id: finalEntryId,
|
|
2558
|
+
entry_type: v.canonicalKey ?? resolvedCollection,
|
|
2559
|
+
tier: v.tier,
|
|
2560
|
+
context: "capture",
|
|
2561
|
+
passed: v.passed,
|
|
2562
|
+
source: verdictResult.source ?? "heuristic",
|
|
2563
|
+
criteria_total: v.criteria?.length ?? 0,
|
|
2564
|
+
criteria_failed: failedCount,
|
|
2565
|
+
llm_scheduled: v.tier !== "passive"
|
|
2566
|
+
});
|
|
2567
|
+
} catch {
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
const shouldAutoCommit = shouldAutoCommitCapture(autoCommit, wsCtx.governanceMode);
|
|
2571
|
+
let finalStatus = "draft";
|
|
2572
|
+
let commitError = null;
|
|
2573
|
+
if (shouldAutoCommit && finalEntryId) {
|
|
2574
|
+
try {
|
|
2575
|
+
const commitResult = await mcpMutation("chain.commitEntry", {
|
|
2576
|
+
entryId: finalEntryId,
|
|
2577
|
+
author: agentId ? `agent:${agentId}` : void 0,
|
|
2578
|
+
sessionId: agentId ?? void 0
|
|
2579
|
+
});
|
|
2580
|
+
finalStatus = commitResult?.status === "proposal_created" ? "proposed" : "committed";
|
|
2581
|
+
if (finalStatus === "committed") {
|
|
2582
|
+
await recordSessionActivity({ entryModified: internalId });
|
|
2583
|
+
}
|
|
2584
|
+
} catch (e) {
|
|
2585
|
+
commitError = e instanceof Error ? e.message : "unknown error";
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
const lines = [
|
|
2589
|
+
`# Captured: ${finalEntryId || name}`,
|
|
2590
|
+
`**${name}** added to \`${resolvedCollection}\` as \`${finalStatus}\``,
|
|
2591
|
+
`**Workspace:** ${wsCtx.workspaceSlug} (${wsCtx.workspaceId})`
|
|
2592
|
+
];
|
|
2593
|
+
if (classifierMeta) {
|
|
2594
|
+
lines.push("");
|
|
2595
|
+
lines.push("## Classification");
|
|
2596
|
+
if (classifierMeta.abstained) {
|
|
2597
|
+
lines.push(`No classifier coverage for \`${resolvedCollection}\` \u2014 entry accepted as-is.`);
|
|
2598
|
+
} else if (classifierMeta.autoRouted) {
|
|
2599
|
+
lines.push(`Auto-routed to \`${resolvedCollection}\` (${classifierMeta.topConfidence}% confidence).`);
|
|
2600
|
+
if (classifierMeta.reasons.length > 0) {
|
|
2601
|
+
lines.push(`Reason: ${classifierMeta.reasons.join("; ")}.`);
|
|
2602
|
+
}
|
|
2603
|
+
lines.push(`Override: rerun with explicit \`collection\` if wrong.`);
|
|
2604
|
+
} else if (classifierMeta.agrees) {
|
|
2605
|
+
lines.push(`Classifier confirms \`${resolvedCollection}\` (${classifierMeta.topConfidence}% confidence).`);
|
|
2606
|
+
} else {
|
|
2607
|
+
const suggested = classifierMeta.candidates[0]?.collection ?? "unknown";
|
|
2608
|
+
lines.push(`Classifier suggests \`${suggested}\` (${classifierMeta.topConfidence}% confidence) \u2014 review classification.`);
|
|
2609
|
+
if (classifierMeta.reasons.length > 0) {
|
|
2610
|
+
lines.push(`Reason: ${classifierMeta.reasons.join("; ")}.`);
|
|
2611
|
+
}
|
|
2612
|
+
if (classifierMeta.overrideCommand) {
|
|
2613
|
+
lines.push(`Override: \`${classifierMeta.overrideCommand}\``);
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
const appUrl = process.env.PRODUCTBRAIN_APP_URL ?? "https://productbrain.io";
|
|
2618
|
+
const studioUrl = resolvedCollection === "chains" ? `${appUrl.replace(/\/$/, "")}/w/${wsCtx.workspaceSlug}/studio/${internalId}` : void 0;
|
|
2619
|
+
if (studioUrl) {
|
|
2620
|
+
lines.push("");
|
|
2621
|
+
lines.push(`**View in Studio:** ${studioUrl}`);
|
|
2622
|
+
}
|
|
2623
|
+
if (linksCreated.length > 0) {
|
|
2624
|
+
lines.push("");
|
|
2625
|
+
lines.push(`## Auto-linked (${linksCreated.length})`);
|
|
2626
|
+
for (const link of linksCreated) {
|
|
2627
|
+
const reason = link.linkReason ? ` \u2014 because ${link.linkReason}` : "";
|
|
2628
|
+
lines.push(`- -> **${link.relationType}** ${link.targetEntryId}: ${link.targetName} [${link.targetCollection}]${reason}`);
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
if (userLinkResults.length > 0) {
|
|
2632
|
+
lines.push("");
|
|
2633
|
+
lines.push("## Requested links");
|
|
2634
|
+
for (const r of userLinkResults) lines.push(`- ${r.label}`);
|
|
2635
|
+
}
|
|
2636
|
+
if (finalStatus === "committed") {
|
|
2637
|
+
const wasAutoCommitted = autoCommit === void 0 && wsCtx.governanceMode === "open";
|
|
2638
|
+
lines.push("");
|
|
2639
|
+
lines.push(`## Committed: ${finalEntryId}`);
|
|
2640
|
+
if (wasAutoCommitted) {
|
|
2641
|
+
lines.push(`**${name}** added to your knowledge base.`);
|
|
2642
|
+
} else {
|
|
2643
|
+
lines.push(`**${name}** promoted to SSOT on the Chain.`);
|
|
2644
|
+
}
|
|
2645
|
+
if (GOVERNED_COLLECTIONS.has(resolvedCollection)) {
|
|
2646
|
+
lines.push(`_Note: \`${resolvedCollection}\` is a governed collection \u2014 ensure this entry has been reviewed._`);
|
|
2647
|
+
}
|
|
2648
|
+
} else if (finalStatus === "proposed") {
|
|
2649
|
+
lines.push("");
|
|
2650
|
+
lines.push(`## Proposal created: ${finalEntryId}`);
|
|
2651
|
+
lines.push(`**${name}** requires consent before it can be committed, so a proposal was created instead of publishing directly.`);
|
|
2652
|
+
} else if (commitError) {
|
|
2653
|
+
lines.push("");
|
|
2654
|
+
lines.push("## Commit failed");
|
|
2655
|
+
lines.push(`Error: ${commitError}. Entry saved as draft \u2014 use \`commit-entry entryId="${finalEntryId}"\` to promote when ready.`);
|
|
2656
|
+
}
|
|
2657
|
+
if (linksSuggested.length > 0) {
|
|
2658
|
+
lines.push("");
|
|
2659
|
+
lines.push("## Suggested links (review and use `relations action=create`)");
|
|
2660
|
+
for (let i = 0; i < linksSuggested.length; i++) {
|
|
2661
|
+
const s = linksSuggested[i];
|
|
2662
|
+
const preview = s.preview ? ` \u2014 ${s.preview}` : "";
|
|
2663
|
+
lines.push(`${i + 1}. **${s.entryId ?? "(no ID)"}**: ${s.name} [${s.collection}]${preview}`);
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
lines.push("");
|
|
2667
|
+
lines.push(formatQualityReport(quality));
|
|
2668
|
+
const failedChecks = quality.checks.filter((c) => !c.passed);
|
|
2669
|
+
if (failedChecks.length > 0) {
|
|
2670
|
+
lines.push("");
|
|
2671
|
+
lines.push(`_To improve: \`update-entry entryId="${finalEntryId}"\` to fill missing fields._`);
|
|
2672
|
+
}
|
|
2673
|
+
const isBetOrGoal = isBetCapture || resolvedCK === "bet" || resolvedCK === "goal";
|
|
2674
|
+
const hasStrategyLink = linksCreated.some((l) => l.targetCollection === "strategy");
|
|
2675
|
+
if (isBetOrGoal && !hasStrategyLink) {
|
|
2676
|
+
lines.push("");
|
|
2677
|
+
lines.push(
|
|
2678
|
+
`**Strategy link:** This ${isBetCapture ? "bet" : "goal"} doesn't connect to any strategy entry. Consider linking before commit. Use \`graph action=suggest entryId="${finalEntryId}"\` to find strategy entries to connect to.`
|
|
2679
|
+
);
|
|
2680
|
+
await recordSessionActivity({ strategyLinkWarnedForEntryId: internalId });
|
|
2681
|
+
}
|
|
2682
|
+
if (cardinalityWarning) {
|
|
2683
|
+
lines.push("");
|
|
2684
|
+
lines.push(`**Cardinality warning:** ${cardinalityWarning}`);
|
|
2685
|
+
}
|
|
2686
|
+
if (contradictionWarnings.length > 0) {
|
|
2687
|
+
lines.push("");
|
|
2688
|
+
lines.push("\u26A0 Contradiction check: proposed entry matched existing governance entries:");
|
|
2689
|
+
for (const w of contradictionWarnings) {
|
|
2690
|
+
lines.push(`- ${w.name} (${w.collection}, ${w.entryId}) \u2014 has 'governs' relation to ${w.governsCount} entries`);
|
|
2691
|
+
}
|
|
2692
|
+
lines.push("Run `context action=gather` on these entries before committing.");
|
|
2693
|
+
}
|
|
2694
|
+
if (entryWarnings.length > 0) {
|
|
2695
|
+
lines.push("");
|
|
2696
|
+
lines.push("## Validation Warnings");
|
|
2697
|
+
for (const w of entryWarnings) {
|
|
2698
|
+
lines.push(`- ${w}`);
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
if (coachingSection) {
|
|
2702
|
+
lines.push("");
|
|
2703
|
+
lines.push(coachingSection);
|
|
2704
|
+
}
|
|
2705
|
+
lines.push("");
|
|
2706
|
+
lines.push("## Next Steps");
|
|
2707
|
+
const eid = finalEntryId || "(check entry ID)";
|
|
2708
|
+
if (finalStatus === "committed") {
|
|
2709
|
+
lines.push(`1. **Connect it:** \`graph action=suggest entryId="${eid}"\` \u2014 discover additional links`);
|
|
2710
|
+
if (failedChecks.length > 0) {
|
|
2711
|
+
lines.push(`2. **Improve quality:** \`update-entry entryId="${eid}"\` \u2014 fill missing fields`);
|
|
2712
|
+
}
|
|
2713
|
+
} else if (finalStatus === "proposed") {
|
|
2714
|
+
lines.push(`1. **Connect it:** \`graph action=suggest entryId="${eid}"\` \u2014 discover additional links to support the proposal`);
|
|
2715
|
+
if (failedChecks.length > 0) {
|
|
2716
|
+
lines.push(`2. **Improve quality:** \`update-entry entryId="${eid}"\` \u2014 strengthen the entry before approval`);
|
|
2717
|
+
}
|
|
2718
|
+
} else {
|
|
2719
|
+
if (userLinkResults.length === 0) {
|
|
2720
|
+
lines.push(`1. **Connect it:** \`graph action=suggest entryId="${eid}"\` \u2014 discover what this should link to`);
|
|
2721
|
+
}
|
|
2722
|
+
lines.push(`${userLinkResults.length === 0 ? "2" : "1"}. **Commit it:** \`commit-entry entryId="${eid}"\` \u2014 promote from draft to SSOT on the Chain`);
|
|
2723
|
+
if (failedChecks.length > 0) {
|
|
2724
|
+
lines.push(`${userLinkResults.length === 0 ? "3" : "2"}. **Improve quality:** \`update-entry entryId="${eid}"\` \u2014 fill missing fields`);
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
try {
|
|
2728
|
+
const readiness = await mcpQuery("chain.workspaceReadiness");
|
|
2729
|
+
if (readiness && readiness.gaps && readiness.gaps.length > 0) {
|
|
2730
|
+
const topGaps = readiness.gaps.slice(0, 2);
|
|
2731
|
+
lines.push("");
|
|
2732
|
+
lines.push(`## Workspace Readiness: ${readiness.score}%`);
|
|
2733
|
+
for (const gap of topGaps) {
|
|
2734
|
+
lines.push(`- _${gap.label}:_ ${gap.guidance}`);
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
} catch {
|
|
2738
|
+
}
|
|
2739
|
+
const next = [];
|
|
2740
|
+
if (finalStatus === "committed") {
|
|
2741
|
+
next.push({ tool: "graph", description: "Discover connections", parameters: { action: "suggest", entryId: finalEntryId } });
|
|
2742
|
+
} else if (finalStatus === "proposed") {
|
|
2743
|
+
next.push({ tool: "graph", description: "Discover connections", parameters: { action: "suggest", entryId: finalEntryId } });
|
|
2744
|
+
} else {
|
|
2745
|
+
if (userLinkResults.length === 0) {
|
|
2746
|
+
next.push({ tool: "graph", description: "Discover links", parameters: { action: "suggest", entryId: finalEntryId } });
|
|
2747
|
+
}
|
|
2748
|
+
next.push({ tool: "commit-entry", description: "Commit to Chain", parameters: { entryId: finalEntryId } });
|
|
2749
|
+
}
|
|
2750
|
+
const summary = finalStatus === "committed" ? `Captured and committed ${finalEntryId} (${name}) to ${resolvedCollection}. Quality ${quality.score}/10.` : finalStatus === "proposed" ? `Captured ${finalEntryId} (${name}) in ${resolvedCollection} and created a proposal for commit. Quality ${quality.score}/10.` : `Captured ${finalEntryId} (${name}) as draft in ${resolvedCollection}. Quality ${quality.score}/10.`;
|
|
2751
|
+
const expectedFields = (col.fields ?? []).map((f) => ({
|
|
2752
|
+
key: f.key,
|
|
2753
|
+
type: f.type,
|
|
2754
|
+
...f.required && { required: true }
|
|
2755
|
+
}));
|
|
2756
|
+
const toolResult = {
|
|
2757
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
2758
|
+
structuredContent: success(
|
|
2759
|
+
summary,
|
|
2760
|
+
{
|
|
2761
|
+
entryId: finalEntryId,
|
|
2762
|
+
collection: resolvedCollection,
|
|
2763
|
+
name,
|
|
2764
|
+
status: finalStatus,
|
|
2765
|
+
qualityScore: quality.score,
|
|
2766
|
+
qualityVerdict: verdictResult?.verdict ? { ...verdictResult.verdict, source: verdictResult.source ?? "heuristic" } : void 0,
|
|
2767
|
+
...classifierMeta && { classifier: classifierMeta },
|
|
2768
|
+
...studioUrl && { studioUrl },
|
|
2769
|
+
...entryWarnings.length > 0 && { warnings: entryWarnings },
|
|
2770
|
+
...normalizationMeta && (Object.keys(normalizationMeta.remapped).length > 0 || normalizationMeta.rejected.length > 0) && {
|
|
2771
|
+
normalization: {
|
|
2772
|
+
remapped: normalizationMeta.remapped,
|
|
2773
|
+
rejected: normalizationMeta.rejected
|
|
2774
|
+
}
|
|
2775
|
+
},
|
|
2776
|
+
expectedFields
|
|
2777
|
+
},
|
|
2778
|
+
next
|
|
2779
|
+
)
|
|
2780
|
+
};
|
|
2781
|
+
return toolResult;
|
|
2782
|
+
})
|
|
2783
|
+
);
|
|
2784
|
+
trackWriteTool(captureTool);
|
|
2785
|
+
const batchCaptureTool = server.registerTool(
|
|
2786
|
+
"batch-capture",
|
|
2787
|
+
{
|
|
2788
|
+
title: "Batch Capture",
|
|
2789
|
+
description: "Create multiple knowledge entries in one call. Ideal for workspace setup, document ingestion, or any scenario where you need to capture many entries at once.\n\nCollection is optional per entry \u2014 omit it and the LLM classifier auto-routes (FEAT-160). High/medium confidence entries are created; low confidence entries are skipped and returned with suggested collections for manual review.\n\nEach entry is created independently \u2014 if one fails, the others still succeed. Returns a compact summary instead of per-entry quality scorecards.\n\nAuto-linking runs per entry but contradiction checks and readiness hints are skipped for speed. Use `quality action=check` on individual entries afterward if needed.\n\nPass `autoCommit: false` to keep the whole batch draft-first. If omitted, Open mode workspaces commit by default and consensus/role modes keep drafts.",
|
|
2790
|
+
inputSchema: batchCaptureSchema.shape,
|
|
2791
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
2792
|
+
},
|
|
2793
|
+
withEnvelope(async ({ entries, autoCommit }) => {
|
|
2794
|
+
requireWriteAccess();
|
|
2795
|
+
const agentId = getAgentSessionId();
|
|
2796
|
+
const createdBy = agentId ? `agent:${agentId}` : "capture";
|
|
2797
|
+
const wsCtx = await getWorkspaceContext();
|
|
2798
|
+
const autoCommitApplied = shouldAutoCommitCapture(autoCommit, wsCtx.governanceMode);
|
|
2799
|
+
const needsClassification = entries.map((e, i) => ({ ...e, index: i })).filter((e) => !e.collection);
|
|
2800
|
+
let classificationMap = /* @__PURE__ */ new Map();
|
|
2801
|
+
if (needsClassification.length > 0) {
|
|
2802
|
+
await server.sendLoggingMessage({
|
|
2803
|
+
level: "info",
|
|
2804
|
+
data: `Classifying ${needsClassification.length}/${entries.length} entries via LLM...`,
|
|
2805
|
+
logger: "product-brain"
|
|
2806
|
+
});
|
|
2807
|
+
const classifications = await Promise.all(
|
|
2808
|
+
needsClassification.map(
|
|
2809
|
+
(e) => resolveCollection({ name: e.name, description: e.description }).catch(() => null)
|
|
2810
|
+
)
|
|
2811
|
+
);
|
|
2812
|
+
for (let i = 0; i < needsClassification.length; i++) {
|
|
2813
|
+
classificationMap.set(needsClassification[i].index, classifications[i]);
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
const results = [];
|
|
2817
|
+
const skippedLowConfidence = [];
|
|
2818
|
+
await server.sendLoggingMessage({
|
|
2819
|
+
level: "info",
|
|
2820
|
+
data: `Batch capturing ${entries.length} entries...`,
|
|
2821
|
+
logger: "product-brain"
|
|
2822
|
+
});
|
|
2823
|
+
const allCollections = await mcpQuery("chain.listCollections");
|
|
2824
|
+
const collCache = /* @__PURE__ */ new Map();
|
|
2825
|
+
for (const c of allCollections) collCache.set(c.slug, c);
|
|
2826
|
+
const collIdToSlug = /* @__PURE__ */ new Map();
|
|
2827
|
+
for (const c of allCollections) collIdToSlug.set(c._id, c.slug);
|
|
2828
|
+
for (let entryIdx = 0; entryIdx < entries.length; entryIdx++) {
|
|
2829
|
+
const entry = entries[entryIdx];
|
|
2830
|
+
if (entryIdx > 0 && entryIdx % 5 === 0) {
|
|
2831
|
+
await server.sendLoggingMessage({
|
|
2832
|
+
level: "info",
|
|
2833
|
+
data: `Captured ${entryIdx}/${entries.length} entries...`,
|
|
2834
|
+
logger: "product-brain"
|
|
2835
|
+
});
|
|
2836
|
+
}
|
|
2837
|
+
let resolvedSlug = entry.collection;
|
|
2838
|
+
let classifiedBy;
|
|
2839
|
+
let confidence;
|
|
2840
|
+
let confidenceTier;
|
|
2841
|
+
if (resolvedSlug) {
|
|
2842
|
+
classifiedBy = "explicit";
|
|
2843
|
+
} else {
|
|
2844
|
+
const resolved = classificationMap.get(entryIdx);
|
|
2845
|
+
if (!resolved || resolved.tier === "low") {
|
|
2846
|
+
skippedLowConfidence.push({
|
|
2847
|
+
index: entryIdx,
|
|
2848
|
+
name: entry.name,
|
|
2849
|
+
suggestedCollection: resolved?.collection,
|
|
2850
|
+
confidence: resolved?.confidence,
|
|
2851
|
+
alternatives: resolved?.alternatives.slice(0, 3)
|
|
2852
|
+
});
|
|
2853
|
+
continue;
|
|
2854
|
+
}
|
|
2855
|
+
resolvedSlug = resolved.collection;
|
|
2856
|
+
classifiedBy = resolved.classifiedBy;
|
|
2857
|
+
confidence = resolved.confidence;
|
|
2858
|
+
confidenceTier = resolved.tier;
|
|
2859
|
+
}
|
|
2860
|
+
const profile = PROFILES.get(resolvedSlug) ?? FALLBACK_PROFILE;
|
|
2861
|
+
const col = collCache.get(resolvedSlug);
|
|
2862
|
+
if (!col) {
|
|
2863
|
+
results.push({
|
|
2864
|
+
name: entry.name,
|
|
2865
|
+
collection: resolvedSlug,
|
|
2866
|
+
entryId: "",
|
|
2867
|
+
ok: false,
|
|
2868
|
+
autoLinks: 0,
|
|
2869
|
+
status: "draft",
|
|
2870
|
+
classifiedBy,
|
|
2871
|
+
confidence,
|
|
2872
|
+
confidenceTier,
|
|
2873
|
+
error: `Collection "${resolvedSlug}" not found`
|
|
2874
|
+
});
|
|
2875
|
+
continue;
|
|
2876
|
+
}
|
|
2877
|
+
if (entry.entryId && resolvedSlug !== "business-rules" && resolvedSlug !== "standards") {
|
|
2878
|
+
results.push({
|
|
2879
|
+
name: entry.name,
|
|
2880
|
+
collection: resolvedSlug,
|
|
2881
|
+
entryId: "",
|
|
2882
|
+
ok: false,
|
|
2883
|
+
autoLinks: 0,
|
|
2884
|
+
status: "draft",
|
|
2885
|
+
classifiedBy,
|
|
2886
|
+
confidence,
|
|
2887
|
+
confidenceTier,
|
|
2888
|
+
error: `BR-111: entryId only allowed for business-rules and standards. Omit for ${resolvedSlug}.`
|
|
2889
|
+
});
|
|
2890
|
+
continue;
|
|
2891
|
+
}
|
|
2892
|
+
const data = buildDataFromFields(col.fields ?? [], profile.descriptionField, entry.description);
|
|
2893
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
2894
|
+
for (const def of profile.defaults) {
|
|
2895
|
+
if (def.value === "today") data[def.key] = today;
|
|
2896
|
+
else if (def.value !== "infer") data[def.key] = def.value;
|
|
2897
|
+
}
|
|
2898
|
+
if (profile.inferField) {
|
|
2899
|
+
const inferred = profile.inferField({
|
|
2900
|
+
collection: resolvedSlug,
|
|
2901
|
+
name: entry.name,
|
|
2902
|
+
description: entry.description,
|
|
2903
|
+
data,
|
|
2904
|
+
entryId: "",
|
|
2905
|
+
linksCreated: [],
|
|
2906
|
+
linksSuggested: [],
|
|
2907
|
+
collectionFields: col.fields ?? []
|
|
2908
|
+
});
|
|
2909
|
+
for (const [key, val] of Object.entries(inferred)) {
|
|
2910
|
+
if (val !== void 0 && val !== "") data[key] = val;
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
if (!data[profile.descriptionField] && !data.description && !data.canonical) {
|
|
2914
|
+
data[profile.descriptionField || "description"] = entry.description;
|
|
2915
|
+
}
|
|
2916
|
+
try {
|
|
2917
|
+
const result = await mcpMutation("chain.createEntry", {
|
|
2918
|
+
collectionSlug: resolvedSlug,
|
|
2919
|
+
entryId: entry.entryId ?? void 0,
|
|
2920
|
+
name: entry.name,
|
|
2921
|
+
status: "draft",
|
|
2922
|
+
data,
|
|
2923
|
+
createdBy,
|
|
2924
|
+
sessionId: agentId ?? void 0
|
|
2925
|
+
});
|
|
2926
|
+
const internalId = result.docId;
|
|
2927
|
+
const finalEntryId = result.entryId;
|
|
2928
|
+
const batchEntryWarnings = result.warnings ?? [];
|
|
2929
|
+
resolveGapsForEntry(entry.name, result.entryId);
|
|
2930
|
+
let finalStatus = "draft";
|
|
2931
|
+
let commitError;
|
|
2932
|
+
let autoLinkCount = 0;
|
|
2933
|
+
const searchQuery = extractSearchTerms(entry.name, entry.description);
|
|
2934
|
+
if (searchQuery) {
|
|
2935
|
+
try {
|
|
2936
|
+
const searchResults = await mcpQuery("chain.searchEntries", { query: searchQuery });
|
|
2937
|
+
const candidates = (searchResults ?? []).filter((r) => r.entryId !== finalEntryId).map((r) => {
|
|
2938
|
+
const conf = computeLinkConfidence(r, entry.name, entry.description, resolvedSlug, collIdToSlug.get(r.collectionId) ?? "unknown");
|
|
2939
|
+
return { ...r, collSlug: collIdToSlug.get(r.collectionId) ?? "unknown", confidence: conf.score };
|
|
2940
|
+
}).sort((a, b) => b.confidence - a.confidence);
|
|
2941
|
+
const batchAutoLinks = [];
|
|
2942
|
+
for (const c of candidates) {
|
|
2943
|
+
if (batchAutoLinks.length >= MAX_AUTO_LINKS) break;
|
|
2944
|
+
if (c.confidence < AUTO_LINK_CONFIDENCE_THRESHOLD) break;
|
|
2945
|
+
if (!c.entryId) continue;
|
|
2946
|
+
const { type: relationType } = inferRelationType(resolvedSlug, c.collSlug, profile);
|
|
2947
|
+
batchAutoLinks.push({ fromEntryId: finalEntryId, toEntryId: c.entryId, type: relationType });
|
|
2948
|
+
}
|
|
2949
|
+
if (batchAutoLinks.length > 0) {
|
|
2950
|
+
const batchRes = await mcpMutation("chain.createEntryRelations", {
|
|
2951
|
+
relations: batchAutoLinks,
|
|
2952
|
+
sessionId: agentId ?? void 0
|
|
2953
|
+
});
|
|
2954
|
+
autoLinkCount = batchRes.created;
|
|
2955
|
+
}
|
|
2956
|
+
} catch {
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
if (autoCommitApplied) {
|
|
2960
|
+
try {
|
|
2961
|
+
const commitResult = await mcpMutation("chain.commitEntry", {
|
|
2962
|
+
entryId: finalEntryId,
|
|
2963
|
+
author: agentId ? `agent:${agentId}` : void 0,
|
|
2964
|
+
sessionId: agentId ?? void 0
|
|
2965
|
+
});
|
|
2966
|
+
finalStatus = commitResult?.status === "proposal_created" ? "proposed" : "committed";
|
|
2967
|
+
if (finalStatus === "committed") {
|
|
2968
|
+
await recordSessionActivity({ entryModified: internalId });
|
|
2969
|
+
}
|
|
2970
|
+
} catch (error) {
|
|
2971
|
+
commitError = error instanceof Error ? error.message : String(error);
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
const entryNorm = result.normalization;
|
|
2975
|
+
results.push({
|
|
2976
|
+
name: entry.name,
|
|
2977
|
+
collection: resolvedSlug,
|
|
2978
|
+
entryId: finalEntryId,
|
|
2979
|
+
ok: true,
|
|
2980
|
+
autoLinks: autoLinkCount,
|
|
2981
|
+
status: finalStatus,
|
|
2982
|
+
classifiedBy,
|
|
2983
|
+
confidence,
|
|
2984
|
+
confidenceTier,
|
|
2985
|
+
...commitError ? { commitError } : {},
|
|
2986
|
+
...batchEntryWarnings.length > 0 ? { warnings: batchEntryWarnings } : {},
|
|
2987
|
+
...entryNorm && (Object.keys(entryNorm.remapped).length > 0 || entryNorm.rejected.length > 0) && {
|
|
2988
|
+
normalization: { remapped: entryNorm.remapped, rejected: entryNorm.rejected }
|
|
2989
|
+
}
|
|
2990
|
+
});
|
|
2991
|
+
} catch (error) {
|
|
2992
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
2993
|
+
results.push({
|
|
2994
|
+
name: entry.name,
|
|
2995
|
+
collection: resolvedSlug,
|
|
2996
|
+
entryId: "",
|
|
2997
|
+
ok: false,
|
|
2998
|
+
autoLinks: 0,
|
|
2999
|
+
status: "draft",
|
|
3000
|
+
classifiedBy,
|
|
3001
|
+
confidence,
|
|
3002
|
+
confidenceTier,
|
|
3003
|
+
error: msg
|
|
3004
|
+
});
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
const created = results.filter((r) => r.ok);
|
|
3008
|
+
const failed = results.filter((r) => !r.ok);
|
|
3009
|
+
const committed = created.filter((r) => r.status === "committed");
|
|
3010
|
+
const proposed = created.filter((r) => r.status === "proposed");
|
|
3011
|
+
const drafts = created.filter((r) => r.status === "draft");
|
|
3012
|
+
const commitFailures = created.filter((r) => r.commitError);
|
|
3013
|
+
const classifiedCount = created.filter((r) => r.classifiedBy && r.classifiedBy !== "explicit").length;
|
|
3014
|
+
await server.sendLoggingMessage({
|
|
3015
|
+
level: "info",
|
|
3016
|
+
data: `Batch complete. ${created.length} succeeded, ${failed.length} failed, ${skippedLowConfidence.length} skipped (low confidence).`,
|
|
3017
|
+
logger: "product-brain"
|
|
3018
|
+
});
|
|
3019
|
+
const totalAutoLinks = created.reduce((sum, r) => sum + r.autoLinks, 0);
|
|
3020
|
+
const byCollection = /* @__PURE__ */ new Map();
|
|
3021
|
+
for (const r of created) {
|
|
3022
|
+
byCollection.set(r.collection, (byCollection.get(r.collection) ?? 0) + 1);
|
|
3023
|
+
}
|
|
3024
|
+
const lines = [
|
|
3025
|
+
`# Batch Capture Complete`,
|
|
3026
|
+
`**${created.length}** created, **${failed.length}** failed, **${skippedLowConfidence.length}** skipped out of ${entries.length} total.`,
|
|
3027
|
+
`**Auto-links created:** ${totalAutoLinks}`,
|
|
3028
|
+
""
|
|
3029
|
+
];
|
|
3030
|
+
if (classifiedCount > 0) {
|
|
3031
|
+
lines.push(`**Auto-classified:** ${classifiedCount} entries routed by LLM/heuristic.`);
|
|
3032
|
+
lines.push("");
|
|
3033
|
+
}
|
|
3034
|
+
if (created.length > 0) {
|
|
3035
|
+
lines.push(
|
|
3036
|
+
`**Statuses:** ${committed.length} committed, ${proposed.length} proposed, ${drafts.length} draft.`
|
|
3037
|
+
);
|
|
3038
|
+
lines.push("");
|
|
3039
|
+
}
|
|
3040
|
+
if (byCollection.size > 0) {
|
|
3041
|
+
lines.push("## By Collection");
|
|
3042
|
+
for (const [col, count] of byCollection) {
|
|
3043
|
+
lines.push(`- \`${col}\`: ${count} entries`);
|
|
3044
|
+
}
|
|
3045
|
+
lines.push("");
|
|
3046
|
+
}
|
|
3047
|
+
if (created.length > 0) {
|
|
3048
|
+
const highConfidence = created.filter((r) => r.confidenceTier === "high" || r.classifiedBy === "explicit");
|
|
3049
|
+
const mediumConfidence = created.filter((r) => r.confidenceTier === "medium");
|
|
3050
|
+
if (highConfidence.length > 0) {
|
|
3051
|
+
lines.push("## Created \u2014 High Confidence / Explicit");
|
|
3052
|
+
for (const r of highConfidence) {
|
|
3053
|
+
const linkNote = r.autoLinks > 0 ? `, ${r.autoLinks} auto-links` : "";
|
|
3054
|
+
const classNote = r.classifiedBy !== "explicit" ? ` (${r.classifiedBy} ${r.confidence}%)` : "";
|
|
3055
|
+
lines.push(`- **${r.entryId}**: ${r.name} [${r.collection}] \u2014 \`${r.status}\`${classNote}${linkNote}`);
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
if (mediumConfidence.length > 0) {
|
|
3059
|
+
lines.push("");
|
|
3060
|
+
lines.push("## Created \u2014 Medium Confidence (review recommended)");
|
|
3061
|
+
for (const r of mediumConfidence) {
|
|
3062
|
+
const linkNote = r.autoLinks > 0 ? `, ${r.autoLinks} auto-links` : "";
|
|
3063
|
+
lines.push(`- **${r.entryId}**: ${r.name} [${r.collection}] \u2014 \`${r.status}\` (${r.classifiedBy} ${r.confidence}%)${linkNote}`);
|
|
3064
|
+
}
|
|
3065
|
+
lines.push(`
|
|
3066
|
+
_Use \`entries action=move\` to correct any misclassified entries._`);
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
if (skippedLowConfidence.length > 0) {
|
|
3070
|
+
lines.push("");
|
|
3071
|
+
lines.push("## Skipped \u2014 Low Confidence (needs explicit collection)");
|
|
3072
|
+
for (const s of skippedLowConfidence) {
|
|
3073
|
+
const suggestion = s.suggestedCollection ? ` \u2014 best guess: \`${s.suggestedCollection}\` (${s.confidence}%)` : " \u2014 no classification available";
|
|
3074
|
+
const alts = s.alternatives?.length ? ` | alternatives: ${s.alternatives.map((a) => `\`${a.collection}\` (${a.confidence}%)`).join(", ")}` : "";
|
|
3075
|
+
lines.push(`- **[${s.index}]** ${s.name}${suggestion}${alts}`);
|
|
3076
|
+
}
|
|
3077
|
+
lines.push("");
|
|
3078
|
+
lines.push("_Re-capture these with an explicit `collection` or use the suggested collection._");
|
|
3079
|
+
}
|
|
3080
|
+
if (commitFailures.length > 0) {
|
|
3081
|
+
lines.push("");
|
|
3082
|
+
lines.push("## Saved as draft");
|
|
3083
|
+
for (const r of commitFailures) {
|
|
3084
|
+
lines.push(`- **${r.entryId}**: ${r.name} [${r.collection}] \u2014 ${r.commitError}`);
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
const entriesWithWarnings = created.filter((r) => r.warnings && r.warnings.length > 0);
|
|
3088
|
+
if (entriesWithWarnings.length > 0) {
|
|
3089
|
+
lines.push("");
|
|
3090
|
+
lines.push("## Validation Warnings");
|
|
3091
|
+
for (const r of entriesWithWarnings) {
|
|
3092
|
+
lines.push(`- **${r.entryId}** (${r.name}): ${r.warnings.join("; ")}`);
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
if (failed.length > 0) {
|
|
3096
|
+
lines.push("");
|
|
3097
|
+
lines.push("## Failed");
|
|
3098
|
+
for (const r of failed) {
|
|
3099
|
+
lines.push(`- ${r.name} [${r.collection}]: _${r.error}_`);
|
|
3100
|
+
}
|
|
3101
|
+
lines.push("");
|
|
3102
|
+
lines.push(`_If failed > 0, inspect \`failedEntries\` in the structured response and retry individually._`);
|
|
3103
|
+
}
|
|
3104
|
+
const entryIds = created.map((r) => r.entryId);
|
|
3105
|
+
if (entryIds.length > 0 || skippedLowConfidence.length > 0) {
|
|
3106
|
+
lines.push("");
|
|
3107
|
+
lines.push("## Next Steps");
|
|
3108
|
+
if (entryIds.length > 0) {
|
|
3109
|
+
lines.push(`- **Connect:** Run \`graph action=suggest\` on key entries to build the knowledge graph`);
|
|
3110
|
+
}
|
|
3111
|
+
if (drafts.length > 0) {
|
|
3112
|
+
lines.push(`- **Commit:** Use \`commit-entry\` to promote remaining drafts to SSOT`);
|
|
3113
|
+
}
|
|
3114
|
+
if (skippedLowConfidence.length > 0) {
|
|
3115
|
+
lines.push(`- **Classify:** Re-capture ${skippedLowConfidence.length} skipped entries with explicit collections`);
|
|
3116
|
+
}
|
|
3117
|
+
if (entryIds.length > 0) {
|
|
3118
|
+
lines.push(`- **Quality:** Run \`quality action=check\` on individual entries to assess completeness`);
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
const skippedNote = skippedLowConfidence.length > 0 ? `, ${skippedLowConfidence.length} skipped (low confidence)` : "";
|
|
3122
|
+
const classifiedNote = classifiedCount > 0 ? `, ${classifiedCount} auto-classified` : "";
|
|
3123
|
+
const summary = failed.length > 0 || skippedLowConfidence.length > 0 ? `Batch captured ${created.length}/${entries.length} entries (${failed.length} failed${skippedNote}, ${committed.length} committed, ${proposed.length} proposed, ${drafts.length} draft${classifiedNote}).` : `Batch captured ${created.length} entries successfully (${committed.length} committed, ${proposed.length} proposed, ${drafts.length} draft${classifiedNote}).`;
|
|
3124
|
+
const firstDraft = drafts[0];
|
|
3125
|
+
const next = [];
|
|
3126
|
+
if (created.length > 0) {
|
|
3127
|
+
next.push({ tool: "graph", description: "Discover connections", parameters: { action: "suggest", entryId: created[0].entryId } });
|
|
3128
|
+
}
|
|
3129
|
+
if (firstDraft) {
|
|
3130
|
+
next.push({ tool: "commit-entry", description: "Commit first draft", parameters: { entryId: firstDraft.entryId } });
|
|
3131
|
+
}
|
|
3132
|
+
if (skippedLowConfidence.length > 0) {
|
|
3133
|
+
next.push({ tool: "capture", description: `Capture skipped entry with explicit collection`, parameters: { name: skippedLowConfidence[0].name, collection: skippedLowConfidence[0].suggestedCollection ?? "" } });
|
|
3134
|
+
}
|
|
3135
|
+
return {
|
|
3136
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
3137
|
+
structuredContent: success(
|
|
3138
|
+
summary,
|
|
3139
|
+
{
|
|
3140
|
+
captured: created.map((r) => ({
|
|
3141
|
+
entryId: r.entryId,
|
|
3142
|
+
collection: r.collection,
|
|
3143
|
+
name: r.name,
|
|
3144
|
+
status: r.status,
|
|
3145
|
+
...r.classifiedBy ? { classifiedBy: r.classifiedBy } : {},
|
|
3146
|
+
...r.confidence != null ? { confidence: r.confidence } : {},
|
|
3147
|
+
...r.confidenceTier ? { confidenceTier: r.confidenceTier } : {},
|
|
3148
|
+
...r.warnings?.length ? { warnings: r.warnings } : {},
|
|
3149
|
+
...r.normalization ? { normalization: r.normalization } : {}
|
|
3150
|
+
})),
|
|
3151
|
+
total: created.length,
|
|
3152
|
+
failed: failed.length,
|
|
3153
|
+
committed: committed.length,
|
|
3154
|
+
proposed: proposed.length,
|
|
3155
|
+
drafts: drafts.length,
|
|
3156
|
+
...classifiedCount > 0 ? { classified: classifiedCount } : {},
|
|
3157
|
+
autoCommitApplied,
|
|
3158
|
+
...skippedLowConfidence.length > 0 && {
|
|
3159
|
+
skippedLowConfidence: skippedLowConfidence.map((s) => ({
|
|
3160
|
+
index: s.index,
|
|
3161
|
+
name: s.name,
|
|
3162
|
+
...s.suggestedCollection ? { suggestedCollection: s.suggestedCollection } : {},
|
|
3163
|
+
...s.confidence != null ? { confidence: s.confidence } : {},
|
|
3164
|
+
...s.alternatives?.length ? { alternatives: s.alternatives } : {}
|
|
3165
|
+
}))
|
|
3166
|
+
},
|
|
3167
|
+
...failed.length > 0 && {
|
|
3168
|
+
failedEntries: failed.map((r) => ({
|
|
3169
|
+
index: results.indexOf(r),
|
|
3170
|
+
collection: r.collection,
|
|
3171
|
+
name: r.name,
|
|
3172
|
+
error: r.error ?? "unknown error"
|
|
3173
|
+
}))
|
|
3174
|
+
}
|
|
3175
|
+
},
|
|
3176
|
+
next
|
|
3177
|
+
)
|
|
3178
|
+
};
|
|
3179
|
+
})
|
|
3180
|
+
);
|
|
3181
|
+
trackWriteTool(batchCaptureTool);
|
|
3182
|
+
}
|
|
3183
|
+
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
3184
|
+
"the",
|
|
3185
|
+
"and",
|
|
3186
|
+
"for",
|
|
3187
|
+
"are",
|
|
3188
|
+
"but",
|
|
3189
|
+
"not",
|
|
3190
|
+
"you",
|
|
3191
|
+
"all",
|
|
3192
|
+
"can",
|
|
3193
|
+
"has",
|
|
3194
|
+
"her",
|
|
3195
|
+
"was",
|
|
3196
|
+
"one",
|
|
3197
|
+
"our",
|
|
3198
|
+
"out",
|
|
3199
|
+
"day",
|
|
3200
|
+
"had",
|
|
3201
|
+
"hot",
|
|
3202
|
+
"how",
|
|
3203
|
+
"its",
|
|
3204
|
+
"may",
|
|
3205
|
+
"new",
|
|
3206
|
+
"now",
|
|
3207
|
+
"old",
|
|
3208
|
+
"see",
|
|
3209
|
+
"way",
|
|
3210
|
+
"who",
|
|
3211
|
+
"did",
|
|
3212
|
+
"get",
|
|
3213
|
+
"let",
|
|
3214
|
+
"say",
|
|
3215
|
+
"she",
|
|
3216
|
+
"too",
|
|
3217
|
+
"use",
|
|
3218
|
+
"from",
|
|
3219
|
+
"have",
|
|
3220
|
+
"been",
|
|
3221
|
+
"each",
|
|
3222
|
+
"that",
|
|
3223
|
+
"this",
|
|
3224
|
+
"with",
|
|
3225
|
+
"will",
|
|
3226
|
+
"they",
|
|
3227
|
+
"what",
|
|
3228
|
+
"when",
|
|
3229
|
+
"make",
|
|
3230
|
+
"like",
|
|
3231
|
+
"long",
|
|
3232
|
+
"look",
|
|
3233
|
+
"many",
|
|
3234
|
+
"some",
|
|
3235
|
+
"them",
|
|
3236
|
+
"than",
|
|
3237
|
+
"most",
|
|
3238
|
+
"only",
|
|
3239
|
+
"over",
|
|
3240
|
+
"such",
|
|
3241
|
+
"into",
|
|
3242
|
+
"also",
|
|
3243
|
+
"back",
|
|
3244
|
+
"just",
|
|
3245
|
+
"much",
|
|
3246
|
+
"must",
|
|
3247
|
+
"name",
|
|
3248
|
+
"very",
|
|
3249
|
+
"your",
|
|
3250
|
+
"after",
|
|
3251
|
+
"which",
|
|
3252
|
+
"their",
|
|
3253
|
+
"about",
|
|
3254
|
+
"would",
|
|
3255
|
+
"there",
|
|
3256
|
+
"should",
|
|
3257
|
+
"could",
|
|
3258
|
+
"other",
|
|
3259
|
+
"these",
|
|
3260
|
+
"first",
|
|
3261
|
+
"being",
|
|
3262
|
+
"those",
|
|
3263
|
+
"still",
|
|
3264
|
+
"where"
|
|
3265
|
+
]);
|
|
3266
|
+
function tokenizeText(input) {
|
|
3267
|
+
return input.toLowerCase().replace(/[^\p{L}\p{N}\s]+/gu, " ").split(/\s+/).filter(Boolean);
|
|
3268
|
+
}
|
|
3269
|
+
async function runContradictionCheck(name, description) {
|
|
3270
|
+
const warnings = [];
|
|
3271
|
+
try {
|
|
3272
|
+
const keyTerms = tokenizeText(`${name} ${description}`).filter((w) => w.length >= 4 && !STOP_WORDS.has(w)).slice(0, 8);
|
|
3273
|
+
if (keyTerms.length === 0) return warnings;
|
|
3274
|
+
const searchQuery = keyTerms.slice(0, 5).join(" ");
|
|
3275
|
+
const [govResults, archResults] = await Promise.all([
|
|
3276
|
+
mcpQuery("chain.searchEntries", { query: searchQuery, collectionSlug: "business-rules" }),
|
|
3277
|
+
mcpQuery("chain.searchEntries", { query: searchQuery, collectionSlug: "architecture" })
|
|
3278
|
+
]);
|
|
3279
|
+
const allGov = [...govResults ?? [], ...archResults ?? []].slice(0, 5);
|
|
3280
|
+
for (const entry of allGov) {
|
|
3281
|
+
const entryTokens = new Set(tokenizeText(`${entry.name} ${entry.data?.description ?? ""}`));
|
|
3282
|
+
const matched = keyTerms.filter((t) => entryTokens.has(t));
|
|
3283
|
+
if (matched.length < 3) continue;
|
|
3284
|
+
let governsCount = 0;
|
|
3285
|
+
try {
|
|
3286
|
+
const relations = await mcpQuery("chain.listEntryRelations", {
|
|
3287
|
+
entryId: entry.entryId
|
|
3288
|
+
});
|
|
3289
|
+
governsCount = (relations ?? []).filter((r) => r.type === "governs").length;
|
|
3290
|
+
} catch {
|
|
3291
|
+
}
|
|
3292
|
+
warnings.push({
|
|
3293
|
+
entryId: entry.entryId ?? "",
|
|
3294
|
+
name: entry.name,
|
|
3295
|
+
collection: entry.collectionSlug ?? "",
|
|
3296
|
+
governsCount
|
|
3297
|
+
});
|
|
3298
|
+
}
|
|
3299
|
+
} catch {
|
|
3300
|
+
}
|
|
3301
|
+
return warnings;
|
|
3302
|
+
}
|
|
3303
|
+
function formatRubricCoaching(result) {
|
|
3304
|
+
const { verdict, rogerMartin } = result;
|
|
3305
|
+
if (!verdict || verdict.criteria.length === 0) return "";
|
|
3306
|
+
const lines = ["## Semantic Quality"];
|
|
3307
|
+
const failed = (verdict.criteria ?? []).filter((c) => !c.passed);
|
|
3308
|
+
const total = verdict.criteria?.length ?? 0;
|
|
3309
|
+
const passedCount = total - failed.length;
|
|
3310
|
+
if (verdict.passed) {
|
|
3311
|
+
lines.push(`All ${total} rubric criteria pass for \`${verdict.canonicalKey}\` (${verdict.tier} tier).`);
|
|
3312
|
+
} else {
|
|
3313
|
+
lines.push(`${passedCount}/${total} criteria pass for \`${verdict.canonicalKey}\` (${verdict.tier} tier)`);
|
|
3314
|
+
lines.push("");
|
|
3315
|
+
for (const c of verdict.criteria) {
|
|
3316
|
+
const icon = c.passed ? "[x]" : "[ ]";
|
|
3317
|
+
const extra = c.passed ? "" : ` \u2014 ${c.hint}`;
|
|
3318
|
+
lines.push(`${icon} ${c.id}${extra}`);
|
|
3319
|
+
}
|
|
3320
|
+
if (verdict.weakest) {
|
|
3321
|
+
lines.push("");
|
|
3322
|
+
lines.push(`**Coaching hint:** ${verdict.weakest.hint}`);
|
|
3323
|
+
lines.push(`_Question to consider:_ ${verdict.weakest.questionTemplate}`);
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
if (rogerMartin) {
|
|
3327
|
+
lines.push("");
|
|
3328
|
+
lines.push("### Roger Martin Test");
|
|
3329
|
+
if (rogerMartin.isStrategicChoice) {
|
|
3330
|
+
lines.push("This principle passes \u2014 the opposite is a reasonable strategic choice.");
|
|
3331
|
+
} else {
|
|
3332
|
+
lines.push(`This principle may not be a strategic choice. ${rogerMartin.reasoning}`);
|
|
3333
|
+
if (rogerMartin.suggestion) {
|
|
3334
|
+
lines.push(`_Suggestion:_ ${rogerMartin.suggestion}`);
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
return lines.join("\n");
|
|
3339
|
+
}
|
|
3340
|
+
function formatRubricVerdictSection(verdict) {
|
|
3341
|
+
if (!verdict || !verdict.criteria || verdict.criteria.length === 0) return "";
|
|
3342
|
+
const lines = ["## Semantic Quality"];
|
|
3343
|
+
const failed = verdict.criteria.filter((c) => !c.passed);
|
|
3344
|
+
const total = verdict.criteria.length;
|
|
3345
|
+
const passedCount = total - failed.length;
|
|
3346
|
+
const durationSuffix = verdict.llmDurationMs ? ` in ${(verdict.llmDurationMs / 1e3).toFixed(1)}s` : "";
|
|
3347
|
+
const statusNote = verdict.llmStatus === "pending" ? " \u2014 LLM evaluation in progress..." : verdict.llmStatus === "failed" ? ` \u2014 LLM evaluation failed${verdict.llmError ? `: ${verdict.llmError}` : ""}, showing heuristic results` : verdict.source === "llm" && durationSuffix ? ` \u2014 evaluated${durationSuffix}` : "";
|
|
3348
|
+
if (failed.length === 0) {
|
|
3349
|
+
lines.push(`All ${total} rubric criteria pass for \`${verdict.canonicalKey}\` (${verdict.tier} tier, ${verdict.source} evaluation).${statusNote}`);
|
|
3350
|
+
} else {
|
|
3351
|
+
lines.push(`${passedCount}/${total} criteria pass for \`${verdict.canonicalKey}\` (${verdict.tier} tier, ${verdict.source} evaluation)${statusNote}`);
|
|
3352
|
+
lines.push("");
|
|
3353
|
+
for (const c of verdict.criteria) {
|
|
3354
|
+
const icon = c.passed ? "[x]" : "[ ]";
|
|
3355
|
+
const extra = c.passed ? "" : ` \u2014 ${c.hint}`;
|
|
3356
|
+
lines.push(`${icon} ${c.id}${extra}`);
|
|
3357
|
+
}
|
|
3358
|
+
if (verdict.weakest) {
|
|
3359
|
+
lines.push("");
|
|
3360
|
+
lines.push(`**Top improvement:** ${verdict.weakest.hint}`);
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
if (verdict.rogerMartin) {
|
|
3364
|
+
const rm = verdict.rogerMartin;
|
|
3365
|
+
lines.push("");
|
|
3366
|
+
lines.push("### Roger Martin Test");
|
|
3367
|
+
if (rm.isStrategicChoice) {
|
|
3368
|
+
lines.push(`This is a real strategic choice \u2014 the opposite is reasonable. ${rm.reasoning}`);
|
|
3369
|
+
} else {
|
|
3370
|
+
lines.push(`This may not be a strategic choice. ${rm.reasoning}`);
|
|
3371
|
+
if (rm.suggestion) {
|
|
3372
|
+
lines.push(`_Suggestion:_ ${rm.suggestion}`);
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
return lines.join("\n");
|
|
3377
|
+
}
|
|
3378
|
+
|
|
3379
|
+
export {
|
|
3380
|
+
runWithAuth,
|
|
3381
|
+
runWithToolContext,
|
|
3382
|
+
getAgentSessionId,
|
|
3383
|
+
isSessionOriented,
|
|
3384
|
+
setSessionOriented,
|
|
3385
|
+
getApiKeyScope,
|
|
3386
|
+
startAgentSession,
|
|
3387
|
+
closeAgentSession,
|
|
3388
|
+
orphanAgentSession,
|
|
3389
|
+
recordSessionActivity,
|
|
3390
|
+
bootstrap,
|
|
3391
|
+
bootstrapHttp,
|
|
3392
|
+
getAuditLog,
|
|
3393
|
+
mcpCall,
|
|
3394
|
+
getWorkspaceId,
|
|
3395
|
+
getWorkspaceContext,
|
|
3396
|
+
mcpQuery,
|
|
3397
|
+
mcpMutation,
|
|
3398
|
+
requireActiveSession,
|
|
3399
|
+
requireWriteAccess,
|
|
3400
|
+
recoverSessionState,
|
|
3401
|
+
deriveEpistemicStatus,
|
|
3402
|
+
formatEpistemicLine,
|
|
3403
|
+
toEpistemicInput,
|
|
3404
|
+
extractPreview,
|
|
3405
|
+
translateStaleToolNames,
|
|
3406
|
+
initToolSurface,
|
|
3407
|
+
trackWriteTool,
|
|
3408
|
+
initFeatureFlags,
|
|
3409
|
+
recordGap,
|
|
3410
|
+
getSessionGaps,
|
|
3411
|
+
getTopGaps,
|
|
3412
|
+
clearSessionGaps,
|
|
3413
|
+
CLASSIFIER_AUTO_ROUTE_THRESHOLD,
|
|
3414
|
+
CLASSIFIABLE_COLLECTIONS,
|
|
3415
|
+
classifyCollection,
|
|
3416
|
+
isClassificationAmbiguous,
|
|
3417
|
+
resolveCollection,
|
|
3418
|
+
success,
|
|
3419
|
+
failure,
|
|
3420
|
+
parseOrFail,
|
|
3421
|
+
unknownAction,
|
|
3422
|
+
successResult,
|
|
3423
|
+
failureResult,
|
|
3424
|
+
notFoundResult,
|
|
3425
|
+
validationResult,
|
|
3426
|
+
withEnvelope,
|
|
3427
|
+
STARTER_COLLECTIONS,
|
|
3428
|
+
inferStrategyCategory,
|
|
3429
|
+
formatQualityReport,
|
|
3430
|
+
checkEntryQuality,
|
|
3431
|
+
captureSchema,
|
|
3432
|
+
batchCaptureSchema,
|
|
3433
|
+
captureClassifierSchema,
|
|
3434
|
+
captureOutputSchema,
|
|
3435
|
+
batchCaptureOutputSchema,
|
|
3436
|
+
registerSmartCaptureTools,
|
|
3437
|
+
runContradictionCheck,
|
|
3438
|
+
formatRubricCoaching,
|
|
3439
|
+
formatRubricVerdictSection
|
|
3440
|
+
};
|
|
3441
|
+
//# sourceMappingURL=chunk-G4JJNINW.js.map
|