@productbrain/mcp 0.0.1-beta.3 → 0.0.1-beta.30
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/.env.mcp.example +8 -13
- package/dist/chunk-IUSCWY4O.js +5007 -0
- package/dist/chunk-IUSCWY4O.js.map +1 -0
- package/dist/chunk-P5KVCIYN.js +1694 -0
- package/dist/chunk-P5KVCIYN.js.map +1 -0
- package/dist/chunk-SJ2ODB3Y.js +128 -0
- package/dist/chunk-SJ2ODB3Y.js.map +1 -0
- package/dist/cli/index.js +1 -1
- package/dist/http.js +402 -0
- package/dist/http.js.map +1 -0
- package/dist/index.js +46 -4222
- package/dist/index.js.map +1 -1
- package/dist/setup-4RSCIDNZ.js +365 -0
- package/dist/setup-4RSCIDNZ.js.map +1 -0
- package/dist/smart-capture-TLGJDCEQ.js +16 -0
- package/package.json +4 -1
- package/dist/chunk-DGUM43GV.js +0 -11
- package/dist/setup-V6HIAYXL.js +0 -227
- package/dist/setup-V6HIAYXL.js.map +0 -1
- /package/dist/{chunk-DGUM43GV.js.map → smart-capture-TLGJDCEQ.js.map} +0 -0
|
@@ -0,0 +1,1694 @@
|
|
|
1
|
+
import {
|
|
2
|
+
trackQualityCheck,
|
|
3
|
+
trackQualityVerdict,
|
|
4
|
+
trackToolCall
|
|
5
|
+
} from "./chunk-SJ2ODB3Y.js";
|
|
6
|
+
|
|
7
|
+
// src/tools/smart-capture.ts
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
// src/auth.ts
|
|
11
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
12
|
+
var requestStore = new AsyncLocalStorage();
|
|
13
|
+
function runWithAuth(auth, fn) {
|
|
14
|
+
return requestStore.run(auth, fn);
|
|
15
|
+
}
|
|
16
|
+
function getRequestApiKey() {
|
|
17
|
+
return requestStore.getStore()?.apiKey;
|
|
18
|
+
}
|
|
19
|
+
var SESSION_TTL_MS = 30 * 60 * 1e3;
|
|
20
|
+
var MAX_KEYS = 100;
|
|
21
|
+
var keyStateMap = /* @__PURE__ */ new Map();
|
|
22
|
+
function newKeyState() {
|
|
23
|
+
return {
|
|
24
|
+
workspaceId: null,
|
|
25
|
+
workspaceSlug: null,
|
|
26
|
+
workspaceName: null,
|
|
27
|
+
workspaceCreatedAt: null,
|
|
28
|
+
agentSessionId: null,
|
|
29
|
+
apiKeyId: null,
|
|
30
|
+
apiKeyScope: "readwrite",
|
|
31
|
+
sessionOriented: false,
|
|
32
|
+
sessionClosed: false,
|
|
33
|
+
lastAccess: Date.now()
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function getKeyState(apiKey) {
|
|
37
|
+
let s = keyStateMap.get(apiKey);
|
|
38
|
+
if (!s) {
|
|
39
|
+
s = newKeyState();
|
|
40
|
+
keyStateMap.set(apiKey, s);
|
|
41
|
+
evictStale();
|
|
42
|
+
}
|
|
43
|
+
s.lastAccess = Date.now();
|
|
44
|
+
return s;
|
|
45
|
+
}
|
|
46
|
+
function evictStale() {
|
|
47
|
+
if (keyStateMap.size <= MAX_KEYS) return;
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
for (const [key, s] of keyStateMap) {
|
|
50
|
+
if (now - s.lastAccess > SESSION_TTL_MS) keyStateMap.delete(key);
|
|
51
|
+
}
|
|
52
|
+
if (keyStateMap.size > MAX_KEYS) {
|
|
53
|
+
const sorted = [...keyStateMap.entries()].sort((a, b) => a[1].lastAccess - b[1].lastAccess);
|
|
54
|
+
for (let i = 0; i < sorted.length - MAX_KEYS; i++) {
|
|
55
|
+
keyStateMap.delete(sorted[i][0]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/client.ts
|
|
61
|
+
var DEFAULT_CLOUD_URL = "https://trustworthy-kangaroo-277.convex.site";
|
|
62
|
+
var CACHE_TTL_MS = 6e4;
|
|
63
|
+
var CACHEABLE_FNS = [
|
|
64
|
+
"chain.getOrientEntries",
|
|
65
|
+
"chain.gatherContext",
|
|
66
|
+
"chain.graphGatherContext",
|
|
67
|
+
"chain.taskAwareGatherContext",
|
|
68
|
+
"chain.assembleBuildContext"
|
|
69
|
+
];
|
|
70
|
+
function isCacheable(fn) {
|
|
71
|
+
return CACHEABLE_FNS.includes(fn);
|
|
72
|
+
}
|
|
73
|
+
var READ_PATTERN = /^(chain\.(get|list|search|batchGet|gather|graph|task|assemble|workspace|score|absence)|maps\.(get|list)|gitchain\.(get|list|diff|history|runGate))/i;
|
|
74
|
+
function isWrite(fn) {
|
|
75
|
+
if (fn.startsWith("agent.")) return false;
|
|
76
|
+
return !READ_PATTERN.test(fn);
|
|
77
|
+
}
|
|
78
|
+
var readCache = /* @__PURE__ */ new Map();
|
|
79
|
+
function cacheKey(fn, args) {
|
|
80
|
+
return `${fn}:${JSON.stringify(args)}`;
|
|
81
|
+
}
|
|
82
|
+
function getCached(fn, args) {
|
|
83
|
+
if (!isCacheable(fn)) return void 0;
|
|
84
|
+
const key = cacheKey(fn, args);
|
|
85
|
+
const entry = readCache.get(key);
|
|
86
|
+
if (!entry || Date.now() > entry.expiresAt) {
|
|
87
|
+
if (entry) readCache.delete(key);
|
|
88
|
+
return void 0;
|
|
89
|
+
}
|
|
90
|
+
return entry.data;
|
|
91
|
+
}
|
|
92
|
+
function setCached(fn, args, data) {
|
|
93
|
+
if (!isCacheable(fn)) return;
|
|
94
|
+
const key = cacheKey(fn, args);
|
|
95
|
+
readCache.set(key, { data, expiresAt: Date.now() + CACHE_TTL_MS });
|
|
96
|
+
}
|
|
97
|
+
function invalidateReadCache() {
|
|
98
|
+
readCache.clear();
|
|
99
|
+
}
|
|
100
|
+
var _stdioState = {
|
|
101
|
+
workspaceId: null,
|
|
102
|
+
workspaceSlug: null,
|
|
103
|
+
workspaceName: null,
|
|
104
|
+
workspaceCreatedAt: null,
|
|
105
|
+
agentSessionId: null,
|
|
106
|
+
apiKeyId: null,
|
|
107
|
+
apiKeyScope: "readwrite",
|
|
108
|
+
sessionOriented: false,
|
|
109
|
+
sessionClosed: false,
|
|
110
|
+
lastAccess: 0
|
|
111
|
+
};
|
|
112
|
+
function state() {
|
|
113
|
+
const reqKey = getRequestApiKey();
|
|
114
|
+
if (reqKey) return getKeyState(reqKey);
|
|
115
|
+
return _stdioState;
|
|
116
|
+
}
|
|
117
|
+
function getActiveApiKey() {
|
|
118
|
+
const fromRequest = getRequestApiKey();
|
|
119
|
+
if (fromRequest) return fromRequest;
|
|
120
|
+
const fromEnv = process.env.PRODUCTBRAIN_API_KEY;
|
|
121
|
+
if (!fromEnv) throw new Error("No API key available \u2014 set PRODUCTBRAIN_API_KEY or provide Bearer token");
|
|
122
|
+
return fromEnv;
|
|
123
|
+
}
|
|
124
|
+
function getAgentSessionId() {
|
|
125
|
+
return state().agentSessionId;
|
|
126
|
+
}
|
|
127
|
+
function isSessionOriented() {
|
|
128
|
+
return state().sessionOriented;
|
|
129
|
+
}
|
|
130
|
+
function setSessionOriented(value) {
|
|
131
|
+
state().sessionOriented = value;
|
|
132
|
+
}
|
|
133
|
+
async function startAgentSession() {
|
|
134
|
+
const workspaceId = await getWorkspaceId();
|
|
135
|
+
const s = state();
|
|
136
|
+
if (!s.apiKeyId) {
|
|
137
|
+
throw new Error("Cannot start session: API key ID not resolved. Ensure workspace resolution completed.");
|
|
138
|
+
}
|
|
139
|
+
const result = await mcpCall("agent.startSession", {
|
|
140
|
+
workspaceId,
|
|
141
|
+
apiKeyId: s.apiKeyId
|
|
142
|
+
});
|
|
143
|
+
s.agentSessionId = result.sessionId;
|
|
144
|
+
s.apiKeyScope = result.toolsScope;
|
|
145
|
+
s.sessionOriented = false;
|
|
146
|
+
s.sessionClosed = false;
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
async function closeAgentSession() {
|
|
150
|
+
const s = state();
|
|
151
|
+
if (!s.agentSessionId) return;
|
|
152
|
+
try {
|
|
153
|
+
await mcpCall("agent.closeSession", {
|
|
154
|
+
sessionId: s.agentSessionId,
|
|
155
|
+
status: "closed"
|
|
156
|
+
});
|
|
157
|
+
} finally {
|
|
158
|
+
s.sessionClosed = true;
|
|
159
|
+
s.agentSessionId = null;
|
|
160
|
+
s.sessionOriented = false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function orphanAgentSession() {
|
|
164
|
+
const s = state();
|
|
165
|
+
if (!s.agentSessionId) return;
|
|
166
|
+
try {
|
|
167
|
+
await mcpCall("agent.closeSession", {
|
|
168
|
+
sessionId: s.agentSessionId,
|
|
169
|
+
status: "orphaned"
|
|
170
|
+
});
|
|
171
|
+
} catch {
|
|
172
|
+
} finally {
|
|
173
|
+
s.agentSessionId = null;
|
|
174
|
+
s.sessionOriented = false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function touchSessionActivity() {
|
|
178
|
+
const s = state();
|
|
179
|
+
if (!s.agentSessionId) return;
|
|
180
|
+
mcpCall("agent.touchSession", {
|
|
181
|
+
sessionId: s.agentSessionId
|
|
182
|
+
}).catch(() => {
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
async function recordSessionActivity(activity) {
|
|
186
|
+
const s = state();
|
|
187
|
+
if (!s.agentSessionId) return;
|
|
188
|
+
try {
|
|
189
|
+
await mcpCall("agent.recordActivity", {
|
|
190
|
+
sessionId: s.agentSessionId,
|
|
191
|
+
...activity
|
|
192
|
+
});
|
|
193
|
+
} catch {
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
var AUDIT_BUFFER_SIZE = 50;
|
|
197
|
+
var auditBuffer = [];
|
|
198
|
+
function bootstrap() {
|
|
199
|
+
const pbKey = process.env.PRODUCTBRAIN_API_KEY;
|
|
200
|
+
if (!pbKey?.startsWith("pb_sk_")) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
"PRODUCTBRAIN_API_KEY is required and must start with 'pb_sk_'. Generate one at Settings > API Keys in the Product OS UI."
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
process.env.CONVEX_SITE_URL ??= process.env.PRODUCTBRAIN_URL ?? DEFAULT_CLOUD_URL;
|
|
206
|
+
}
|
|
207
|
+
function bootstrapHttp() {
|
|
208
|
+
process.env.CONVEX_SITE_URL ??= process.env.PRODUCTBRAIN_URL ?? DEFAULT_CLOUD_URL;
|
|
209
|
+
}
|
|
210
|
+
function getEnv(key) {
|
|
211
|
+
const value = process.env[key];
|
|
212
|
+
if (!value) throw new Error(`${key} environment variable is required`);
|
|
213
|
+
return value;
|
|
214
|
+
}
|
|
215
|
+
function shouldLogAudit(status) {
|
|
216
|
+
return status === "error" || process.env.MCP_DEBUG === "1";
|
|
217
|
+
}
|
|
218
|
+
function audit(fn, status, durationMs, errorMsg) {
|
|
219
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
220
|
+
const workspace = state().workspaceId ?? "unresolved";
|
|
221
|
+
const entry = { ts, fn, workspace, status, durationMs };
|
|
222
|
+
if (errorMsg) entry.error = errorMsg;
|
|
223
|
+
auditBuffer.push(entry);
|
|
224
|
+
if (auditBuffer.length > AUDIT_BUFFER_SIZE) auditBuffer.shift();
|
|
225
|
+
trackToolCall(fn, status, durationMs, workspace, errorMsg);
|
|
226
|
+
if (!shouldLogAudit(status)) return;
|
|
227
|
+
const base = `[MCP-AUDIT] ${ts} fn=${fn} workspace=${workspace} status=${status} duration=${durationMs}ms`;
|
|
228
|
+
if (status === "error" && errorMsg) {
|
|
229
|
+
process.stderr.write(`${base} error=${JSON.stringify(errorMsg)}
|
|
230
|
+
`);
|
|
231
|
+
} else {
|
|
232
|
+
process.stderr.write(`${base}
|
|
233
|
+
`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function getAuditLog() {
|
|
237
|
+
return auditBuffer;
|
|
238
|
+
}
|
|
239
|
+
async function mcpCall(fn, args = {}) {
|
|
240
|
+
const cached = getCached(fn, args);
|
|
241
|
+
if (cached !== void 0) {
|
|
242
|
+
return cached;
|
|
243
|
+
}
|
|
244
|
+
const siteUrl = getEnv("CONVEX_SITE_URL").replace(/\/$/, "");
|
|
245
|
+
const apiKey = getActiveApiKey();
|
|
246
|
+
const start = Date.now();
|
|
247
|
+
let res;
|
|
248
|
+
try {
|
|
249
|
+
res = await fetch(`${siteUrl}/api/mcp`, {
|
|
250
|
+
method: "POST",
|
|
251
|
+
headers: {
|
|
252
|
+
"Content-Type": "application/json",
|
|
253
|
+
Authorization: `Bearer ${apiKey}`
|
|
254
|
+
},
|
|
255
|
+
body: JSON.stringify({ fn, args })
|
|
256
|
+
});
|
|
257
|
+
} catch (err) {
|
|
258
|
+
audit(fn, "error", Date.now() - start, err.message);
|
|
259
|
+
throw new Error(`MCP call "${fn}" network error: ${err.message}`);
|
|
260
|
+
}
|
|
261
|
+
const json = await res.json();
|
|
262
|
+
if (!res.ok || json.error) {
|
|
263
|
+
audit(fn, "error", Date.now() - start, json.error);
|
|
264
|
+
throw new Error(`MCP call "${fn}" failed (${res.status}): ${json.error ?? "unknown error"}`);
|
|
265
|
+
}
|
|
266
|
+
audit(fn, "ok", Date.now() - start);
|
|
267
|
+
const data = json.data;
|
|
268
|
+
if (isWrite(fn)) {
|
|
269
|
+
invalidateReadCache();
|
|
270
|
+
} else {
|
|
271
|
+
setCached(fn, args, data);
|
|
272
|
+
}
|
|
273
|
+
const s = state();
|
|
274
|
+
if (s.agentSessionId && fn !== "agent.touchSession" && fn !== "agent.startSession") {
|
|
275
|
+
touchSessionActivity();
|
|
276
|
+
}
|
|
277
|
+
return data;
|
|
278
|
+
}
|
|
279
|
+
var resolveInFlightMap = /* @__PURE__ */ new Map();
|
|
280
|
+
async function getWorkspaceId() {
|
|
281
|
+
const s = state();
|
|
282
|
+
if (s.workspaceId) return s.workspaceId;
|
|
283
|
+
const apiKey = getActiveApiKey();
|
|
284
|
+
const existing = resolveInFlightMap.get(apiKey);
|
|
285
|
+
if (existing) return existing;
|
|
286
|
+
const promise = resolveWorkspaceWithRetry().finally(() => resolveInFlightMap.delete(apiKey));
|
|
287
|
+
resolveInFlightMap.set(apiKey, promise);
|
|
288
|
+
return promise;
|
|
289
|
+
}
|
|
290
|
+
async function resolveWorkspaceWithRetry(maxRetries = 2) {
|
|
291
|
+
let lastError = null;
|
|
292
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
293
|
+
try {
|
|
294
|
+
const workspace = await mcpCall("resolveWorkspace", {});
|
|
295
|
+
if (!workspace) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
"API key is valid but no workspace is associated. Run `npx productbrain setup` or regenerate your key."
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
const s = state();
|
|
301
|
+
s.workspaceId = workspace._id;
|
|
302
|
+
s.workspaceSlug = workspace.slug;
|
|
303
|
+
s.workspaceName = workspace.name;
|
|
304
|
+
s.workspaceCreatedAt = workspace.createdAt ?? null;
|
|
305
|
+
if (workspace.keyScope) s.apiKeyScope = workspace.keyScope;
|
|
306
|
+
if (workspace.keyId) s.apiKeyId = workspace.keyId;
|
|
307
|
+
return s.workspaceId;
|
|
308
|
+
} catch (err) {
|
|
309
|
+
lastError = err;
|
|
310
|
+
const isTransient = /network error|fetch failed|ECONNREFUSED|ETIMEDOUT/i.test(err.message);
|
|
311
|
+
if (!isTransient || attempt === maxRetries) break;
|
|
312
|
+
const delay = 1e3 * (attempt + 1);
|
|
313
|
+
process.stderr.write(
|
|
314
|
+
`[MCP] Workspace resolution failed (attempt ${attempt + 1}/${maxRetries + 1}), retrying in ${delay}ms...
|
|
315
|
+
`
|
|
316
|
+
);
|
|
317
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
throw lastError;
|
|
321
|
+
}
|
|
322
|
+
async function getWorkspaceContext() {
|
|
323
|
+
const workspaceId = await getWorkspaceId();
|
|
324
|
+
const s = state();
|
|
325
|
+
return {
|
|
326
|
+
workspaceId,
|
|
327
|
+
workspaceSlug: s.workspaceSlug ?? "unknown",
|
|
328
|
+
workspaceName: s.workspaceName ?? "unknown",
|
|
329
|
+
createdAt: s.workspaceCreatedAt
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
async function mcpQuery(fn, args = {}) {
|
|
333
|
+
const workspaceId = await getWorkspaceId();
|
|
334
|
+
return mcpCall(fn, { ...args, workspaceId });
|
|
335
|
+
}
|
|
336
|
+
async function mcpMutation(fn, args = {}) {
|
|
337
|
+
const workspaceId = await getWorkspaceId();
|
|
338
|
+
return mcpCall(fn, { ...args, workspaceId });
|
|
339
|
+
}
|
|
340
|
+
function requireActiveSession() {
|
|
341
|
+
const s = state();
|
|
342
|
+
if (!s.agentSessionId) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
"Active session required (SOS-iszqu7). Call `agent-start` then `orient` first."
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
if (s.sessionClosed) {
|
|
348
|
+
throw new Error(
|
|
349
|
+
"Session has been closed (SOS-iszqu7). Start a new session with `agent-start`."
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
if (!s.sessionOriented) {
|
|
353
|
+
throw new Error(
|
|
354
|
+
"Orientation required before accessing build context (SOS-iszqu7). Call `orient` first."
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function requireWriteAccess() {
|
|
359
|
+
const s = state();
|
|
360
|
+
if (!s.agentSessionId) {
|
|
361
|
+
throw new Error(
|
|
362
|
+
"Agent session required for write operations. Call `agent-start` first."
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
if (s.sessionClosed) {
|
|
366
|
+
throw new Error(
|
|
367
|
+
"Agent session has been closed. Write tools are no longer available."
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
if (!s.sessionOriented) {
|
|
371
|
+
throw new Error(
|
|
372
|
+
"Orientation required before writing to the Chain. Call 'orient' first."
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
if (s.apiKeyScope === "read") {
|
|
376
|
+
throw new Error(
|
|
377
|
+
"This API key has read-only scope. Write tools are not available."
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
async function recoverSessionState() {
|
|
382
|
+
const s = state();
|
|
383
|
+
if (!s.workspaceId) return;
|
|
384
|
+
try {
|
|
385
|
+
const session = await mcpCall("agent.getActiveSession", { workspaceId: s.workspaceId });
|
|
386
|
+
if (session && session.status === "active") {
|
|
387
|
+
s.agentSessionId = session._id;
|
|
388
|
+
s.sessionOriented = session.oriented;
|
|
389
|
+
s.apiKeyScope = session.toolsScope;
|
|
390
|
+
s.sessionClosed = false;
|
|
391
|
+
}
|
|
392
|
+
} catch {
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/tools/smart-capture.ts
|
|
397
|
+
var AREA_KEYWORDS = {
|
|
398
|
+
"Architecture": ["convex", "schema", "database", "migration", "api", "backend", "infrastructure", "scaling", "performance"],
|
|
399
|
+
"Chain": ["knowledge", "glossary", "entry", "collection", "terminology", "drift", "graph", "chain", "commit"],
|
|
400
|
+
"AI & MCP Integration": ["mcp", "ai", "cursor", "agent", "tool", "llm", "prompt", "context"],
|
|
401
|
+
"Developer Experience": ["dx", "developer", "ide", "workflow", "friction", "ceremony"],
|
|
402
|
+
"Governance & Decision-Making": ["governance", "decision", "rule", "policy", "compliance", "approval"],
|
|
403
|
+
"Analytics & Tracking": ["analytics", "posthog", "tracking", "event", "metric", "funnel"],
|
|
404
|
+
"Security": ["security", "auth", "api key", "permission", "access", "token"]
|
|
405
|
+
};
|
|
406
|
+
function inferArea(text) {
|
|
407
|
+
const lower = text.toLowerCase();
|
|
408
|
+
let bestArea = "";
|
|
409
|
+
let bestScore = 0;
|
|
410
|
+
for (const [area, keywords] of Object.entries(AREA_KEYWORDS)) {
|
|
411
|
+
const score = keywords.filter((kw) => lower.includes(kw)).length;
|
|
412
|
+
if (score > bestScore) {
|
|
413
|
+
bestScore = score;
|
|
414
|
+
bestArea = area;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return bestArea;
|
|
418
|
+
}
|
|
419
|
+
function inferDomain(text) {
|
|
420
|
+
return inferArea(text) || "";
|
|
421
|
+
}
|
|
422
|
+
var COMMON_CHECKS = {
|
|
423
|
+
clearName: {
|
|
424
|
+
id: "clear-name",
|
|
425
|
+
label: "Clear, specific name (not vague)",
|
|
426
|
+
check: (ctx) => ctx.name.length > 10 && !["new tension", "new entry", "untitled", "test"].includes(ctx.name.toLowerCase()),
|
|
427
|
+
suggestion: () => "Rename to something specific \u2014 describe the actual problem or concept."
|
|
428
|
+
},
|
|
429
|
+
hasDescription: {
|
|
430
|
+
id: "has-description",
|
|
431
|
+
label: "Description provided (>50 chars)",
|
|
432
|
+
check: (ctx) => ctx.description.length > 50,
|
|
433
|
+
suggestion: () => "Add a fuller description explaining context and impact."
|
|
434
|
+
},
|
|
435
|
+
hasRelations: {
|
|
436
|
+
id: "has-relations",
|
|
437
|
+
label: "At least 1 relation created",
|
|
438
|
+
check: (ctx) => ctx.linksCreated.length >= 1,
|
|
439
|
+
suggestion: () => "Use `suggest-links` and `relate-entries` to add more connections."
|
|
440
|
+
},
|
|
441
|
+
diverseRelations: {
|
|
442
|
+
id: "diverse-relations",
|
|
443
|
+
label: "Relations span multiple collections",
|
|
444
|
+
check: (ctx) => {
|
|
445
|
+
const colls = new Set(ctx.linksCreated.map((l) => l.targetCollection));
|
|
446
|
+
return colls.size >= 2;
|
|
447
|
+
},
|
|
448
|
+
suggestion: () => "Try linking to entries in different collections (glossary, business-rules, strategy)."
|
|
449
|
+
},
|
|
450
|
+
hasType: {
|
|
451
|
+
id: "has-type",
|
|
452
|
+
label: "Has canonical type",
|
|
453
|
+
check: (ctx) => !!ctx.data?.canonicalKey || !!ctx.canonicalKey,
|
|
454
|
+
suggestion: () => "Classify this entry with a canonical type for better context assembly. Use update-entry to set canonicalKey."
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
var PROFILES = /* @__PURE__ */ new Map([
|
|
458
|
+
["tensions", {
|
|
459
|
+
governedDraft: false,
|
|
460
|
+
descriptionField: "description",
|
|
461
|
+
defaults: [
|
|
462
|
+
{ key: "priority", value: "medium" },
|
|
463
|
+
{ key: "date", value: "today" },
|
|
464
|
+
{ key: "raised", value: "infer" },
|
|
465
|
+
{ key: "severity", value: "infer" }
|
|
466
|
+
],
|
|
467
|
+
recommendedRelationTypes: ["surfaces_tension_in", "references", "belongs_to", "related_to"],
|
|
468
|
+
inferField: (ctx) => {
|
|
469
|
+
const fields = {};
|
|
470
|
+
const text = `${ctx.name} ${ctx.description}`;
|
|
471
|
+
const area = inferArea(text);
|
|
472
|
+
if (area) fields.raised = area;
|
|
473
|
+
if (text.toLowerCase().includes("critical") || text.toLowerCase().includes("blocker")) {
|
|
474
|
+
fields.severity = "critical";
|
|
475
|
+
} else if (text.toLowerCase().includes("bottleneck") || text.toLowerCase().includes("scaling") || text.toLowerCase().includes("breaking")) {
|
|
476
|
+
fields.severity = "high";
|
|
477
|
+
} else {
|
|
478
|
+
fields.severity = "medium";
|
|
479
|
+
}
|
|
480
|
+
if (area) fields.affectedArea = area;
|
|
481
|
+
return fields;
|
|
482
|
+
},
|
|
483
|
+
qualityChecks: [
|
|
484
|
+
COMMON_CHECKS.clearName,
|
|
485
|
+
COMMON_CHECKS.hasDescription,
|
|
486
|
+
COMMON_CHECKS.hasRelations,
|
|
487
|
+
COMMON_CHECKS.hasType,
|
|
488
|
+
{
|
|
489
|
+
id: "has-severity",
|
|
490
|
+
label: "Severity specified",
|
|
491
|
+
check: (ctx) => !!ctx.data.severity && ctx.data.severity !== "",
|
|
492
|
+
suggestion: (ctx) => {
|
|
493
|
+
const text = `${ctx.name} ${ctx.description}`.toLowerCase();
|
|
494
|
+
const inferred = text.includes("critical") ? "critical" : text.includes("bottleneck") ? "high" : "medium";
|
|
495
|
+
return `Set severity \u2014 suggest: ${inferred} (based on description keywords).`;
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
id: "has-affected-area",
|
|
500
|
+
label: "Affected area identified",
|
|
501
|
+
check: (ctx) => !!ctx.data.affectedArea && ctx.data.affectedArea !== "",
|
|
502
|
+
suggestion: (ctx) => {
|
|
503
|
+
const area = inferArea(`${ctx.name} ${ctx.description}`);
|
|
504
|
+
return area ? `Set affectedArea \u2014 suggest: "${area}" (inferred from content).` : "Specify which product area or domain this tension impacts.";
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
]
|
|
508
|
+
}],
|
|
509
|
+
["business-rules", {
|
|
510
|
+
governedDraft: true,
|
|
511
|
+
descriptionField: "description",
|
|
512
|
+
defaults: [
|
|
513
|
+
{ key: "severity", value: "medium" },
|
|
514
|
+
{ key: "domain", value: "infer" }
|
|
515
|
+
],
|
|
516
|
+
recommendedRelationTypes: ["governs", "references", "conflicts_with", "related_to"],
|
|
517
|
+
inferField: (ctx) => {
|
|
518
|
+
const fields = {};
|
|
519
|
+
const domain = inferDomain(`${ctx.name} ${ctx.description}`);
|
|
520
|
+
if (domain) fields.domain = domain;
|
|
521
|
+
return fields;
|
|
522
|
+
},
|
|
523
|
+
qualityChecks: [
|
|
524
|
+
COMMON_CHECKS.clearName,
|
|
525
|
+
COMMON_CHECKS.hasDescription,
|
|
526
|
+
COMMON_CHECKS.hasRelations,
|
|
527
|
+
COMMON_CHECKS.hasType,
|
|
528
|
+
{
|
|
529
|
+
id: "has-rationale",
|
|
530
|
+
label: "Rationale provided",
|
|
531
|
+
check: (ctx) => typeof ctx.data.rationale === "string" && ctx.data.rationale.length > 10,
|
|
532
|
+
suggestion: () => "Add a rationale explaining why this rule exists via `update-entry`."
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
id: "has-domain",
|
|
536
|
+
label: "Domain specified",
|
|
537
|
+
check: (ctx) => !!ctx.data.domain && ctx.data.domain !== "",
|
|
538
|
+
suggestion: (ctx) => {
|
|
539
|
+
const domain = inferDomain(`${ctx.name} ${ctx.description}`);
|
|
540
|
+
return domain ? `Set domain \u2014 suggest: "${domain}" (inferred from content).` : "Specify the business domain this rule belongs to.";
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
]
|
|
544
|
+
}],
|
|
545
|
+
["glossary", {
|
|
546
|
+
governedDraft: true,
|
|
547
|
+
descriptionField: "canonical",
|
|
548
|
+
defaults: [
|
|
549
|
+
{ key: "category", value: "infer" }
|
|
550
|
+
],
|
|
551
|
+
recommendedRelationTypes: ["defines_term_for", "confused_with", "related_to", "references"],
|
|
552
|
+
inferField: (ctx) => {
|
|
553
|
+
const fields = {};
|
|
554
|
+
const area = inferArea(`${ctx.name} ${ctx.description}`);
|
|
555
|
+
if (area) {
|
|
556
|
+
const categoryMap = {
|
|
557
|
+
"Architecture": "Platform & Architecture",
|
|
558
|
+
"Chain": "Knowledge Management",
|
|
559
|
+
"AI & MCP Integration": "AI & Developer Tools",
|
|
560
|
+
"Developer Experience": "AI & Developer Tools",
|
|
561
|
+
"Governance & Decision-Making": "Governance & Process",
|
|
562
|
+
"Analytics & Tracking": "Platform & Architecture",
|
|
563
|
+
"Security": "Platform & Architecture"
|
|
564
|
+
};
|
|
565
|
+
fields.category = categoryMap[area] ?? "";
|
|
566
|
+
}
|
|
567
|
+
return fields;
|
|
568
|
+
},
|
|
569
|
+
qualityChecks: [
|
|
570
|
+
COMMON_CHECKS.clearName,
|
|
571
|
+
COMMON_CHECKS.hasType,
|
|
572
|
+
{
|
|
573
|
+
id: "has-canonical",
|
|
574
|
+
label: "Canonical definition provided (>20 chars)",
|
|
575
|
+
check: (ctx) => {
|
|
576
|
+
const canonical = ctx.data.canonical;
|
|
577
|
+
return typeof canonical === "string" && canonical.length > 20;
|
|
578
|
+
},
|
|
579
|
+
suggestion: () => "Add a clear canonical definition \u2014 this is the single source of truth for this term."
|
|
580
|
+
},
|
|
581
|
+
COMMON_CHECKS.hasRelations,
|
|
582
|
+
{
|
|
583
|
+
id: "has-category",
|
|
584
|
+
label: "Category assigned",
|
|
585
|
+
check: (ctx) => !!ctx.data.category && ctx.data.category !== "",
|
|
586
|
+
suggestion: () => "Assign a category (e.g., 'Platform & Architecture', 'Governance & Process')."
|
|
587
|
+
}
|
|
588
|
+
]
|
|
589
|
+
}],
|
|
590
|
+
["decisions", {
|
|
591
|
+
governedDraft: false,
|
|
592
|
+
descriptionField: "rationale",
|
|
593
|
+
defaults: [
|
|
594
|
+
{ key: "date", value: "today" },
|
|
595
|
+
{ key: "decidedBy", value: "infer" }
|
|
596
|
+
],
|
|
597
|
+
recommendedRelationTypes: ["informs", "references", "replaces", "related_to"],
|
|
598
|
+
inferField: (ctx) => {
|
|
599
|
+
const fields = {};
|
|
600
|
+
const area = inferArea(`${ctx.name} ${ctx.description}`);
|
|
601
|
+
if (area) fields.decidedBy = area;
|
|
602
|
+
return fields;
|
|
603
|
+
},
|
|
604
|
+
qualityChecks: [
|
|
605
|
+
COMMON_CHECKS.clearName,
|
|
606
|
+
COMMON_CHECKS.hasType,
|
|
607
|
+
{
|
|
608
|
+
id: "has-rationale",
|
|
609
|
+
label: "Rationale provided (>30 chars)",
|
|
610
|
+
check: (ctx) => {
|
|
611
|
+
const rationale = ctx.data.rationale;
|
|
612
|
+
return typeof rationale === "string" && rationale.length > 30;
|
|
613
|
+
},
|
|
614
|
+
suggestion: () => "Explain why this decision was made \u2014 what was considered and rejected?"
|
|
615
|
+
},
|
|
616
|
+
COMMON_CHECKS.hasRelations,
|
|
617
|
+
{
|
|
618
|
+
id: "has-date",
|
|
619
|
+
label: "Decision date recorded",
|
|
620
|
+
check: (ctx) => !!ctx.data.date && ctx.data.date !== "",
|
|
621
|
+
suggestion: () => "Record when this decision was made."
|
|
622
|
+
}
|
|
623
|
+
]
|
|
624
|
+
}],
|
|
625
|
+
["features", {
|
|
626
|
+
governedDraft: false,
|
|
627
|
+
descriptionField: "description",
|
|
628
|
+
defaults: [],
|
|
629
|
+
recommendedRelationTypes: ["belongs_to", "depends_on", "surfaces_tension_in", "related_to"],
|
|
630
|
+
qualityChecks: [
|
|
631
|
+
COMMON_CHECKS.clearName,
|
|
632
|
+
COMMON_CHECKS.hasDescription,
|
|
633
|
+
COMMON_CHECKS.hasRelations,
|
|
634
|
+
COMMON_CHECKS.hasType,
|
|
635
|
+
{
|
|
636
|
+
id: "has-owner",
|
|
637
|
+
label: "Owner assigned",
|
|
638
|
+
check: (ctx) => !!ctx.data.owner && ctx.data.owner !== "",
|
|
639
|
+
suggestion: () => "Assign an owner team or product area."
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
id: "has-rationale",
|
|
643
|
+
label: "Rationale documented",
|
|
644
|
+
check: (ctx) => !!ctx.data.rationale && String(ctx.data.rationale).length > 20,
|
|
645
|
+
suggestion: () => "Explain why this feature matters \u2014 what problem does it solve?"
|
|
646
|
+
}
|
|
647
|
+
]
|
|
648
|
+
}],
|
|
649
|
+
["audiences", {
|
|
650
|
+
governedDraft: false,
|
|
651
|
+
descriptionField: "description",
|
|
652
|
+
defaults: [],
|
|
653
|
+
recommendedRelationTypes: ["fills_slot", "informs", "related_to", "references"],
|
|
654
|
+
qualityChecks: [
|
|
655
|
+
COMMON_CHECKS.clearName,
|
|
656
|
+
COMMON_CHECKS.hasDescription,
|
|
657
|
+
COMMON_CHECKS.hasRelations,
|
|
658
|
+
COMMON_CHECKS.hasType,
|
|
659
|
+
{
|
|
660
|
+
id: "has-behaviors",
|
|
661
|
+
label: "Behaviors described",
|
|
662
|
+
check: (ctx) => typeof ctx.data.behaviors === "string" && ctx.data.behaviors.length > 20,
|
|
663
|
+
suggestion: () => "Describe how this audience segment behaves \u2014 what do they do, what tools do they use?"
|
|
664
|
+
}
|
|
665
|
+
]
|
|
666
|
+
}],
|
|
667
|
+
["strategy", {
|
|
668
|
+
governedDraft: true,
|
|
669
|
+
descriptionField: "description",
|
|
670
|
+
defaults: [],
|
|
671
|
+
recommendedRelationTypes: ["informs", "governs", "belongs_to", "related_to"],
|
|
672
|
+
qualityChecks: [
|
|
673
|
+
COMMON_CHECKS.clearName,
|
|
674
|
+
COMMON_CHECKS.hasDescription,
|
|
675
|
+
COMMON_CHECKS.hasRelations,
|
|
676
|
+
COMMON_CHECKS.hasType,
|
|
677
|
+
COMMON_CHECKS.diverseRelations
|
|
678
|
+
]
|
|
679
|
+
}],
|
|
680
|
+
["maps", {
|
|
681
|
+
governedDraft: false,
|
|
682
|
+
descriptionField: "description",
|
|
683
|
+
defaults: [],
|
|
684
|
+
recommendedRelationTypes: ["fills_slot", "references", "related_to"],
|
|
685
|
+
qualityChecks: [
|
|
686
|
+
COMMON_CHECKS.clearName,
|
|
687
|
+
COMMON_CHECKS.hasDescription
|
|
688
|
+
]
|
|
689
|
+
}],
|
|
690
|
+
["chains", {
|
|
691
|
+
governedDraft: false,
|
|
692
|
+
descriptionField: "description",
|
|
693
|
+
defaults: [],
|
|
694
|
+
recommendedRelationTypes: ["informs", "references", "related_to"],
|
|
695
|
+
qualityChecks: [
|
|
696
|
+
COMMON_CHECKS.clearName,
|
|
697
|
+
COMMON_CHECKS.hasDescription
|
|
698
|
+
]
|
|
699
|
+
}],
|
|
700
|
+
["standards", {
|
|
701
|
+
governedDraft: true,
|
|
702
|
+
descriptionField: "description",
|
|
703
|
+
defaults: [],
|
|
704
|
+
recommendedRelationTypes: ["governs", "defines_term_for", "references", "related_to"],
|
|
705
|
+
qualityChecks: [
|
|
706
|
+
COMMON_CHECKS.clearName,
|
|
707
|
+
COMMON_CHECKS.hasDescription,
|
|
708
|
+
COMMON_CHECKS.hasRelations,
|
|
709
|
+
COMMON_CHECKS.hasType
|
|
710
|
+
]
|
|
711
|
+
}],
|
|
712
|
+
["tracking-events", {
|
|
713
|
+
governedDraft: false,
|
|
714
|
+
descriptionField: "description",
|
|
715
|
+
defaults: [],
|
|
716
|
+
recommendedRelationTypes: ["references", "belongs_to", "related_to"],
|
|
717
|
+
qualityChecks: [
|
|
718
|
+
COMMON_CHECKS.clearName,
|
|
719
|
+
COMMON_CHECKS.hasDescription
|
|
720
|
+
]
|
|
721
|
+
}]
|
|
722
|
+
]);
|
|
723
|
+
var FALLBACK_PROFILE = {
|
|
724
|
+
governedDraft: false,
|
|
725
|
+
descriptionField: "description",
|
|
726
|
+
defaults: [],
|
|
727
|
+
recommendedRelationTypes: ["related_to", "references"],
|
|
728
|
+
qualityChecks: [
|
|
729
|
+
COMMON_CHECKS.clearName,
|
|
730
|
+
COMMON_CHECKS.hasDescription,
|
|
731
|
+
COMMON_CHECKS.hasRelations,
|
|
732
|
+
COMMON_CHECKS.hasType
|
|
733
|
+
]
|
|
734
|
+
};
|
|
735
|
+
function extractSearchTerms(name, description) {
|
|
736
|
+
const text = `${name} ${description}`;
|
|
737
|
+
return text.replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length > 3).slice(0, 8).join(" ");
|
|
738
|
+
}
|
|
739
|
+
function computeLinkConfidence(candidate, sourceName, sourceDescription, sourceCollection, candidateCollection) {
|
|
740
|
+
const text = `${sourceName} ${sourceDescription}`.toLowerCase();
|
|
741
|
+
const candidateName = candidate.name.toLowerCase();
|
|
742
|
+
let score = 0;
|
|
743
|
+
const reasons = [];
|
|
744
|
+
if (text.includes(candidateName) && candidateName.length > 3) {
|
|
745
|
+
score += 40;
|
|
746
|
+
reasons.push("name match");
|
|
747
|
+
}
|
|
748
|
+
const candidateWords = candidateName.split(/\s+/).filter((w) => w.length > 3);
|
|
749
|
+
const matchingWords = candidateWords.filter((w) => text.includes(w));
|
|
750
|
+
const wordScore = matchingWords.length / Math.max(candidateWords.length, 1) * 30;
|
|
751
|
+
score += wordScore;
|
|
752
|
+
if (matchingWords.length > 0) {
|
|
753
|
+
reasons.push(`word overlap (${matchingWords.slice(0, 3).join(", ")})`);
|
|
754
|
+
}
|
|
755
|
+
const HUB_COLLECTIONS = /* @__PURE__ */ new Set(["strategy", "features"]);
|
|
756
|
+
if (HUB_COLLECTIONS.has(candidateCollection)) {
|
|
757
|
+
score += 15;
|
|
758
|
+
reasons.push("hub collection");
|
|
759
|
+
}
|
|
760
|
+
if (candidateCollection !== sourceCollection) {
|
|
761
|
+
score += 10;
|
|
762
|
+
reasons.push("cross-collection");
|
|
763
|
+
}
|
|
764
|
+
const finalScore = Math.min(score, 100);
|
|
765
|
+
const reason = reasons.length > 0 ? reasons.join(" + ") : "low relevance";
|
|
766
|
+
return { score: finalScore, reason };
|
|
767
|
+
}
|
|
768
|
+
function inferRelationType(sourceCollection, targetCollection, profile) {
|
|
769
|
+
const typeMap = {
|
|
770
|
+
tensions: {
|
|
771
|
+
glossary: "surfaces_tension_in",
|
|
772
|
+
"business-rules": "references",
|
|
773
|
+
strategy: "belongs_to",
|
|
774
|
+
features: "surfaces_tension_in",
|
|
775
|
+
decisions: "references"
|
|
776
|
+
},
|
|
777
|
+
"business-rules": {
|
|
778
|
+
glossary: "references",
|
|
779
|
+
features: "governs",
|
|
780
|
+
strategy: "belongs_to",
|
|
781
|
+
tensions: "references"
|
|
782
|
+
},
|
|
783
|
+
glossary: {
|
|
784
|
+
features: "defines_term_for",
|
|
785
|
+
"business-rules": "references",
|
|
786
|
+
strategy: "references"
|
|
787
|
+
},
|
|
788
|
+
decisions: {
|
|
789
|
+
features: "informs",
|
|
790
|
+
"business-rules": "references",
|
|
791
|
+
strategy: "references",
|
|
792
|
+
tensions: "references"
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
const mapped = typeMap[sourceCollection]?.[targetCollection];
|
|
796
|
+
const type = mapped ?? profile.recommendedRelationTypes[0] ?? "related_to";
|
|
797
|
+
const reason = mapped ? `collection pair (${sourceCollection} \u2192 ${targetCollection})` : `profile default (${profile.recommendedRelationTypes[0] ?? "related_to"})`;
|
|
798
|
+
return { type, reason };
|
|
799
|
+
}
|
|
800
|
+
function scoreQuality(ctx, profile) {
|
|
801
|
+
const checks = profile.qualityChecks.map((qc) => {
|
|
802
|
+
const passed2 = qc.check(ctx);
|
|
803
|
+
return {
|
|
804
|
+
id: qc.id,
|
|
805
|
+
label: qc.label,
|
|
806
|
+
passed: passed2,
|
|
807
|
+
suggestion: passed2 ? void 0 : qc.suggestion?.(ctx)
|
|
808
|
+
};
|
|
809
|
+
});
|
|
810
|
+
const passed = checks.filter((c) => c.passed).length;
|
|
811
|
+
const total = checks.length;
|
|
812
|
+
const score = total > 0 ? Math.round(passed / total * 10) : 10;
|
|
813
|
+
return { score, maxScore: 10, checks };
|
|
814
|
+
}
|
|
815
|
+
function formatQualityReport(result) {
|
|
816
|
+
const failed = result.checks.filter((c) => !c.passed);
|
|
817
|
+
const reason = failed.length > 0 ? ` because ${failed.map((c) => c.suggestion ?? c.label.toLowerCase()).join("; ")}` : "";
|
|
818
|
+
const lines = [`## Quality: ${result.score}/${result.maxScore}${reason}`];
|
|
819
|
+
for (const check of result.checks) {
|
|
820
|
+
const icon = check.passed ? "[x]" : "[ ]";
|
|
821
|
+
const suggestion = check.passed ? "" : ` \u2014 ${check.suggestion ?? check.label}`;
|
|
822
|
+
lines.push(`${icon} ${check.label}${suggestion}`);
|
|
823
|
+
}
|
|
824
|
+
return lines.join("\n");
|
|
825
|
+
}
|
|
826
|
+
async function checkEntryQuality(entryId) {
|
|
827
|
+
const entry = await mcpQuery("chain.getEntry", { entryId });
|
|
828
|
+
if (!entry) {
|
|
829
|
+
return {
|
|
830
|
+
text: `Entry \`${entryId}\` not found. Try search to find the right ID.`,
|
|
831
|
+
quality: { score: 0, maxScore: 10, checks: [] }
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
const collections = await mcpQuery("chain.listCollections");
|
|
835
|
+
const collMap = /* @__PURE__ */ new Map();
|
|
836
|
+
for (const c of collections) collMap.set(c._id, c.slug);
|
|
837
|
+
const collectionSlug = collMap.get(entry.collectionId) ?? "unknown";
|
|
838
|
+
const profile = PROFILES.get(collectionSlug) ?? FALLBACK_PROFILE;
|
|
839
|
+
const relations = await mcpQuery("chain.listEntryRelations", { entryId });
|
|
840
|
+
const linksCreated = [];
|
|
841
|
+
for (const r of relations) {
|
|
842
|
+
const otherId = r.fromId === entry._id ? r.toId : r.fromId;
|
|
843
|
+
linksCreated.push({
|
|
844
|
+
targetEntryId: otherId,
|
|
845
|
+
targetName: "",
|
|
846
|
+
targetCollection: "",
|
|
847
|
+
relationType: r.type
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
const descField = profile.descriptionField;
|
|
851
|
+
const description = typeof entry.data?.[descField] === "string" ? entry.data[descField] : "";
|
|
852
|
+
const ctx = {
|
|
853
|
+
collection: collectionSlug,
|
|
854
|
+
name: entry.name,
|
|
855
|
+
description,
|
|
856
|
+
data: entry.data ?? {},
|
|
857
|
+
entryId: entry.entryId ?? "",
|
|
858
|
+
canonicalKey: entry.canonicalKey,
|
|
859
|
+
linksCreated,
|
|
860
|
+
linksSuggested: [],
|
|
861
|
+
collectionFields: []
|
|
862
|
+
};
|
|
863
|
+
const quality = scoreQuality(ctx, profile);
|
|
864
|
+
const lines = [
|
|
865
|
+
`# Quality Check: ${entry.entryId ?? entry.name}`,
|
|
866
|
+
`**${entry.name}** in \`${collectionSlug}\` [${entry.status}]`,
|
|
867
|
+
"",
|
|
868
|
+
formatQualityReport(quality)
|
|
869
|
+
];
|
|
870
|
+
if (quality.score < 10) {
|
|
871
|
+
const failedChecks = quality.checks.filter((c) => !c.passed && c.suggestion);
|
|
872
|
+
if (failedChecks.length > 0) {
|
|
873
|
+
lines.push("");
|
|
874
|
+
lines.push(`_To improve: use \`update-entry\` to fill missing fields, or \`relate-entries\` to add connections._`);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return { text: lines.join("\n"), quality };
|
|
878
|
+
}
|
|
879
|
+
var GOVERNED_COLLECTIONS = /* @__PURE__ */ new Set([
|
|
880
|
+
"glossary",
|
|
881
|
+
"business-rules",
|
|
882
|
+
"principles",
|
|
883
|
+
"standards",
|
|
884
|
+
"strategy",
|
|
885
|
+
"features"
|
|
886
|
+
]);
|
|
887
|
+
var AUTO_LINK_CONFIDENCE_THRESHOLD = 35;
|
|
888
|
+
var MAX_AUTO_LINKS = 5;
|
|
889
|
+
var MAX_SUGGESTIONS = 5;
|
|
890
|
+
function registerSmartCaptureTools(server) {
|
|
891
|
+
server.registerTool(
|
|
892
|
+
"capture",
|
|
893
|
+
{
|
|
894
|
+
title: "Capture",
|
|
895
|
+
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 collection, name, and description \u2014 everything else is inferred or auto-filled.\n\nSupported collections with smart profiles: tensions, business-rules, glossary, decisions, features, audiences, strategy, standards, maps, chains, 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\nAlways creates as 'draft' for governed collections. Use `update-entry` for post-creation adjustments.",
|
|
896
|
+
inputSchema: {
|
|
897
|
+
collection: z.string().describe("Collection slug, e.g. 'tensions', 'business-rules', 'glossary', 'decisions'"),
|
|
898
|
+
name: z.string().describe("Display name \u2014 be specific (e.g. 'Convex adjacency list won't scale for graph traversal')"),
|
|
899
|
+
description: z.string().describe("Full context \u2014 what's happening, why it matters, what you observed"),
|
|
900
|
+
context: z.string().optional().describe("Optional additional context (e.g. 'Observed during gather-context calls taking 700ms+')"),
|
|
901
|
+
entryId: z.string().optional().describe("Optional custom entry ID (e.g. 'TEN-my-id'). Auto-generated if omitted."),
|
|
902
|
+
canonicalKey: z.string().optional().describe("Semantic type (e.g. 'decision', 'tension', 'vision'). Auto-assigned from collection if omitted."),
|
|
903
|
+
data: z.record(z.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.")
|
|
904
|
+
},
|
|
905
|
+
annotations: { destructiveHint: false }
|
|
906
|
+
},
|
|
907
|
+
async ({ collection, name, description, context, entryId, canonicalKey, data: userData }) => {
|
|
908
|
+
requireWriteAccess();
|
|
909
|
+
const profile = PROFILES.get(collection) ?? FALLBACK_PROFILE;
|
|
910
|
+
const col = await mcpQuery("chain.getCollection", { slug: collection });
|
|
911
|
+
if (!col) {
|
|
912
|
+
const displayName = collection.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
913
|
+
return {
|
|
914
|
+
content: [{
|
|
915
|
+
type: "text",
|
|
916
|
+
text: `Collection \`${collection}\` not found.
|
|
917
|
+
|
|
918
|
+
**To create it**, run:
|
|
919
|
+
\`\`\`
|
|
920
|
+
create-collection slug="${collection}" name="${displayName}" description="..."
|
|
921
|
+
\`\`\`
|
|
922
|
+
|
|
923
|
+
Or use \`list-collections\` to see available collections.`
|
|
924
|
+
}]
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
const data = {};
|
|
928
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
929
|
+
for (const field of col.fields ?? []) {
|
|
930
|
+
const key = field.key;
|
|
931
|
+
if (key === profile.descriptionField) {
|
|
932
|
+
data[key] = description;
|
|
933
|
+
} else if (field.type === "array" || field.type === "multi-select") {
|
|
934
|
+
data[key] = [];
|
|
935
|
+
} else if (field.type === "select") {
|
|
936
|
+
} else {
|
|
937
|
+
data[key] = "";
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
for (const def of profile.defaults) {
|
|
941
|
+
if (def.value === "today") {
|
|
942
|
+
data[def.key] = today;
|
|
943
|
+
} else if (def.value !== "infer") {
|
|
944
|
+
data[def.key] = def.value;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
if (profile.inferField) {
|
|
948
|
+
const inferred = profile.inferField({
|
|
949
|
+
collection,
|
|
950
|
+
name,
|
|
951
|
+
description,
|
|
952
|
+
context,
|
|
953
|
+
data,
|
|
954
|
+
entryId: "",
|
|
955
|
+
linksCreated: [],
|
|
956
|
+
linksSuggested: [],
|
|
957
|
+
collectionFields: col.fields ?? []
|
|
958
|
+
});
|
|
959
|
+
for (const [key, val] of Object.entries(inferred)) {
|
|
960
|
+
if (val !== void 0 && val !== "") {
|
|
961
|
+
data[key] = val;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
if (userData && typeof userData === "object") {
|
|
966
|
+
for (const [key, val] of Object.entries(userData)) {
|
|
967
|
+
if (key !== "name") data[key] = val;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
data[profile.descriptionField || "description"] = description;
|
|
971
|
+
const status = GOVERNED_COLLECTIONS.has(collection) ? "draft" : "draft";
|
|
972
|
+
let finalEntryId;
|
|
973
|
+
let internalId;
|
|
974
|
+
try {
|
|
975
|
+
const agentId = getAgentSessionId();
|
|
976
|
+
const result = await mcpMutation("chain.createEntry", {
|
|
977
|
+
collectionSlug: collection,
|
|
978
|
+
entryId: entryId ?? void 0,
|
|
979
|
+
name,
|
|
980
|
+
status,
|
|
981
|
+
data,
|
|
982
|
+
canonicalKey,
|
|
983
|
+
createdBy: agentId ? `agent:${agentId}` : "capture",
|
|
984
|
+
sessionId: agentId ?? void 0
|
|
985
|
+
});
|
|
986
|
+
internalId = result.docId;
|
|
987
|
+
finalEntryId = result.entryId;
|
|
988
|
+
await recordSessionActivity({ entryCreated: internalId });
|
|
989
|
+
} catch (error) {
|
|
990
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
991
|
+
if (msg.includes("Duplicate") || msg.includes("already exists")) {
|
|
992
|
+
return {
|
|
993
|
+
content: [{
|
|
994
|
+
type: "text",
|
|
995
|
+
text: `# Cannot Capture \u2014 Duplicate Detected
|
|
996
|
+
|
|
997
|
+
${msg}
|
|
998
|
+
|
|
999
|
+
Use \`get-entry\` to inspect the existing entry, or \`update-entry\` to modify it.`
|
|
1000
|
+
}]
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
throw error;
|
|
1004
|
+
}
|
|
1005
|
+
const linksCreated = [];
|
|
1006
|
+
const linksSuggested = [];
|
|
1007
|
+
const searchQuery = extractSearchTerms(name, description);
|
|
1008
|
+
if (searchQuery) {
|
|
1009
|
+
const [searchResults, allCollections] = await Promise.all([
|
|
1010
|
+
mcpQuery("chain.searchEntries", { query: searchQuery }),
|
|
1011
|
+
mcpQuery("chain.listCollections")
|
|
1012
|
+
]);
|
|
1013
|
+
const collMap = /* @__PURE__ */ new Map();
|
|
1014
|
+
for (const c of allCollections) collMap.set(c._id, c.slug);
|
|
1015
|
+
const candidates = (searchResults ?? []).filter((r) => r.entryId !== finalEntryId && r._id !== internalId).map((r) => {
|
|
1016
|
+
const conf = computeLinkConfidence(r, name, description, collection, collMap.get(r.collectionId) ?? "unknown");
|
|
1017
|
+
return {
|
|
1018
|
+
...r,
|
|
1019
|
+
collSlug: collMap.get(r.collectionId) ?? "unknown",
|
|
1020
|
+
confidence: conf.score,
|
|
1021
|
+
confidenceReason: conf.reason
|
|
1022
|
+
};
|
|
1023
|
+
}).sort((a, b) => b.confidence - a.confidence);
|
|
1024
|
+
for (const c of candidates) {
|
|
1025
|
+
if (linksCreated.length >= MAX_AUTO_LINKS) break;
|
|
1026
|
+
if (c.confidence < AUTO_LINK_CONFIDENCE_THRESHOLD) break;
|
|
1027
|
+
if (!c.entryId || !finalEntryId) continue;
|
|
1028
|
+
const { type: relationType, reason: relationReason } = inferRelationType(collection, c.collSlug, profile);
|
|
1029
|
+
try {
|
|
1030
|
+
await mcpMutation("chain.createEntryRelation", {
|
|
1031
|
+
fromEntryId: finalEntryId,
|
|
1032
|
+
toEntryId: c.entryId,
|
|
1033
|
+
type: relationType
|
|
1034
|
+
});
|
|
1035
|
+
linksCreated.push({
|
|
1036
|
+
targetEntryId: c.entryId,
|
|
1037
|
+
targetName: c.name,
|
|
1038
|
+
targetCollection: c.collSlug,
|
|
1039
|
+
relationType,
|
|
1040
|
+
linkReason: `confidence ${c.confidence} (${c.confidenceReason}) + ${relationReason}`
|
|
1041
|
+
});
|
|
1042
|
+
} catch {
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
const linkedIds = new Set(linksCreated.map((l) => l.targetEntryId));
|
|
1046
|
+
for (const c of candidates) {
|
|
1047
|
+
if (linksSuggested.length >= MAX_SUGGESTIONS) break;
|
|
1048
|
+
if (linkedIds.has(c.entryId)) continue;
|
|
1049
|
+
if (c.confidence < 10) continue;
|
|
1050
|
+
const preview = extractPreview(c.data, 80);
|
|
1051
|
+
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`;
|
|
1052
|
+
linksSuggested.push({
|
|
1053
|
+
entryId: c.entryId,
|
|
1054
|
+
name: c.name,
|
|
1055
|
+
collection: c.collSlug,
|
|
1056
|
+
reason,
|
|
1057
|
+
preview
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
const captureCtx = {
|
|
1062
|
+
collection,
|
|
1063
|
+
name,
|
|
1064
|
+
description,
|
|
1065
|
+
context,
|
|
1066
|
+
data,
|
|
1067
|
+
entryId: finalEntryId,
|
|
1068
|
+
canonicalKey,
|
|
1069
|
+
linksCreated,
|
|
1070
|
+
linksSuggested,
|
|
1071
|
+
collectionFields: col.fields ?? []
|
|
1072
|
+
};
|
|
1073
|
+
const quality = scoreQuality(captureCtx, profile);
|
|
1074
|
+
let cardinalityWarning = null;
|
|
1075
|
+
const resolvedCK = canonicalKey ?? captureCtx.canonicalKey;
|
|
1076
|
+
if (resolvedCK) {
|
|
1077
|
+
try {
|
|
1078
|
+
const check = await mcpQuery("chain.checkCardinalityWarning", {
|
|
1079
|
+
canonicalKey: resolvedCK
|
|
1080
|
+
});
|
|
1081
|
+
if (check?.warning) {
|
|
1082
|
+
cardinalityWarning = check.warning;
|
|
1083
|
+
}
|
|
1084
|
+
} catch {
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
const contradictionWarnings = await runContradictionCheck(name, description);
|
|
1088
|
+
if (contradictionWarnings.length > 0) {
|
|
1089
|
+
await recordSessionActivity({ contradictionWarning: true });
|
|
1090
|
+
}
|
|
1091
|
+
let coachingSection = "";
|
|
1092
|
+
let verdictResult = null;
|
|
1093
|
+
try {
|
|
1094
|
+
verdictResult = await mcpMutation("quality.evaluateHeuristicAndSchedule", {
|
|
1095
|
+
entryId: finalEntryId,
|
|
1096
|
+
context: "capture"
|
|
1097
|
+
});
|
|
1098
|
+
if (verdictResult?.verdict && verdictResult.verdict.tier !== "passive" && verdictResult.verdict.criteria.length > 0) {
|
|
1099
|
+
coachingSection = formatRubricCoaching(verdictResult);
|
|
1100
|
+
}
|
|
1101
|
+
} catch {
|
|
1102
|
+
}
|
|
1103
|
+
if (verdictResult?.verdict) {
|
|
1104
|
+
try {
|
|
1105
|
+
const wsForTracking = await getWorkspaceContext();
|
|
1106
|
+
const v = verdictResult.verdict;
|
|
1107
|
+
const failedCount = (v.criteria ?? []).filter((c) => !c.passed).length;
|
|
1108
|
+
trackQualityVerdict(wsForTracking.workspaceId, {
|
|
1109
|
+
entry_id: finalEntryId,
|
|
1110
|
+
entry_type: v.canonicalKey ?? collection,
|
|
1111
|
+
tier: v.tier,
|
|
1112
|
+
context: "capture",
|
|
1113
|
+
passed: v.passed,
|
|
1114
|
+
source: verdictResult.source ?? "heuristic",
|
|
1115
|
+
criteria_total: v.criteria?.length ?? 0,
|
|
1116
|
+
criteria_failed: failedCount,
|
|
1117
|
+
llm_scheduled: v.tier !== "passive"
|
|
1118
|
+
});
|
|
1119
|
+
} catch {
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
const wsCtx = await getWorkspaceContext();
|
|
1123
|
+
const lines = [
|
|
1124
|
+
`# Captured: ${finalEntryId || name}`,
|
|
1125
|
+
`**${name}** added to \`${collection}\` as \`${status}\``,
|
|
1126
|
+
`**Workspace:** ${wsCtx.workspaceSlug} (${wsCtx.workspaceId})`
|
|
1127
|
+
];
|
|
1128
|
+
if (linksCreated.length > 0) {
|
|
1129
|
+
lines.push("");
|
|
1130
|
+
lines.push(`## Auto-linked (${linksCreated.length})`);
|
|
1131
|
+
for (const link of linksCreated) {
|
|
1132
|
+
const reason = link.linkReason ? ` \u2014 because ${link.linkReason}` : "";
|
|
1133
|
+
lines.push(`- -> **${link.relationType}** ${link.targetEntryId}: ${link.targetName} [${link.targetCollection}]${reason}`);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
if (linksSuggested.length > 0) {
|
|
1137
|
+
lines.push("");
|
|
1138
|
+
lines.push("## Suggested links (review and use relate-entries)");
|
|
1139
|
+
for (let i = 0; i < linksSuggested.length; i++) {
|
|
1140
|
+
const s = linksSuggested[i];
|
|
1141
|
+
const preview = s.preview ? ` \u2014 ${s.preview}` : "";
|
|
1142
|
+
lines.push(`${i + 1}. **${s.entryId ?? "(no ID)"}**: ${s.name} [${s.collection}]${preview}`);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
lines.push("");
|
|
1146
|
+
lines.push(formatQualityReport(quality));
|
|
1147
|
+
const failedChecks = quality.checks.filter((c) => !c.passed);
|
|
1148
|
+
if (failedChecks.length > 0) {
|
|
1149
|
+
lines.push("");
|
|
1150
|
+
lines.push(`_To improve: \`update-entry entryId="${finalEntryId}"\` to fill missing fields._`);
|
|
1151
|
+
}
|
|
1152
|
+
const isBetOrGoal = collection === "bets" || resolvedCK === "bet" || resolvedCK === "goal";
|
|
1153
|
+
const hasStrategyLink = linksCreated.some((l) => l.targetCollection === "strategy");
|
|
1154
|
+
if (isBetOrGoal && !hasStrategyLink) {
|
|
1155
|
+
lines.push("");
|
|
1156
|
+
lines.push(
|
|
1157
|
+
`**Strategy link:** This ${collection === "bets" ? "bet" : "goal"} doesn't connect to any strategy entry. Consider linking before commit. Use \`suggest-links entryId="${finalEntryId}"\` to find strategy entries to connect to.`
|
|
1158
|
+
);
|
|
1159
|
+
await recordSessionActivity({ strategyLinkWarnedForEntryId: internalId });
|
|
1160
|
+
}
|
|
1161
|
+
if (cardinalityWarning) {
|
|
1162
|
+
lines.push("");
|
|
1163
|
+
lines.push(`**Cardinality warning:** ${cardinalityWarning}`);
|
|
1164
|
+
}
|
|
1165
|
+
if (contradictionWarnings.length > 0) {
|
|
1166
|
+
lines.push("");
|
|
1167
|
+
lines.push("\u26A0 Contradiction check: proposed entry matched existing governance entries:");
|
|
1168
|
+
for (const w of contradictionWarnings) {
|
|
1169
|
+
lines.push(`- ${w.name} (${w.collection}, ${w.entryId}) \u2014 has 'governs' relation to ${w.governsCount} entries`);
|
|
1170
|
+
}
|
|
1171
|
+
lines.push("Run gather-context on these entries before committing.");
|
|
1172
|
+
}
|
|
1173
|
+
if (coachingSection) {
|
|
1174
|
+
lines.push("");
|
|
1175
|
+
lines.push(coachingSection);
|
|
1176
|
+
}
|
|
1177
|
+
lines.push("");
|
|
1178
|
+
lines.push("## Next Steps");
|
|
1179
|
+
const eid = finalEntryId || "(check entry ID)";
|
|
1180
|
+
lines.push(`1. **Connect it:** \`suggest-links entryId="${eid}"\` \u2014 discover what this should link to`);
|
|
1181
|
+
lines.push(`2. **Commit it:** \`commit-entry entryId="${eid}"\` \u2014 promote from draft to SSOT on the Chain`);
|
|
1182
|
+
if (failedChecks.length > 0) {
|
|
1183
|
+
lines.push(`3. **Improve quality:** \`update-entry entryId="${eid}"\` \u2014 fill missing fields`);
|
|
1184
|
+
}
|
|
1185
|
+
try {
|
|
1186
|
+
const readiness = await mcpQuery("chain.workspaceReadiness");
|
|
1187
|
+
if (readiness && readiness.gaps && readiness.gaps.length > 0) {
|
|
1188
|
+
const topGaps = readiness.gaps.slice(0, 2);
|
|
1189
|
+
lines.push("");
|
|
1190
|
+
lines.push(`## Workspace Readiness: ${readiness.score}%`);
|
|
1191
|
+
for (const gap of topGaps) {
|
|
1192
|
+
lines.push(`- _${gap.label}:_ ${gap.guidance}`);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
} catch {
|
|
1196
|
+
}
|
|
1197
|
+
const toolResult = {
|
|
1198
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1199
|
+
structuredContent: verdictResult?.verdict ? { qualityVerdict: verdictResult.verdict, source: verdictResult.source ?? "heuristic" } : void 0
|
|
1200
|
+
};
|
|
1201
|
+
return toolResult;
|
|
1202
|
+
}
|
|
1203
|
+
);
|
|
1204
|
+
server.registerTool(
|
|
1205
|
+
"batch-capture",
|
|
1206
|
+
{
|
|
1207
|
+
title: "Batch Capture",
|
|
1208
|
+
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\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-check` on individual entries afterward if needed.",
|
|
1209
|
+
inputSchema: {
|
|
1210
|
+
entries: z.array(z.object({
|
|
1211
|
+
collection: z.string().describe("Collection slug"),
|
|
1212
|
+
name: z.string().describe("Display name"),
|
|
1213
|
+
description: z.string().describe("Full context / definition"),
|
|
1214
|
+
entryId: z.string().optional().describe("Optional custom entry ID")
|
|
1215
|
+
})).min(1).max(50).describe("Array of entries to capture")
|
|
1216
|
+
},
|
|
1217
|
+
annotations: { destructiveHint: false }
|
|
1218
|
+
},
|
|
1219
|
+
async ({ entries }) => {
|
|
1220
|
+
requireWriteAccess();
|
|
1221
|
+
const agentId = getAgentSessionId();
|
|
1222
|
+
const createdBy = agentId ? `agent:${agentId}` : "capture";
|
|
1223
|
+
const results = [];
|
|
1224
|
+
const allCollections = await mcpQuery("chain.listCollections");
|
|
1225
|
+
const collCache = /* @__PURE__ */ new Map();
|
|
1226
|
+
for (const c of allCollections) collCache.set(c.slug, c);
|
|
1227
|
+
const collIdToSlug = /* @__PURE__ */ new Map();
|
|
1228
|
+
for (const c of allCollections) collIdToSlug.set(c._id, c.slug);
|
|
1229
|
+
for (const entry of entries) {
|
|
1230
|
+
const profile = PROFILES.get(entry.collection) ?? FALLBACK_PROFILE;
|
|
1231
|
+
const col = collCache.get(entry.collection);
|
|
1232
|
+
if (!col) {
|
|
1233
|
+
results.push({ name: entry.name, collection: entry.collection, entryId: "", ok: false, autoLinks: 0, error: `Collection "${entry.collection}" not found` });
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
const data = {};
|
|
1237
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1238
|
+
for (const field of col.fields ?? []) {
|
|
1239
|
+
const key = field.key;
|
|
1240
|
+
if (key === profile.descriptionField) {
|
|
1241
|
+
data[key] = entry.description;
|
|
1242
|
+
} else if (field.type === "array" || field.type === "multi-select") {
|
|
1243
|
+
data[key] = [];
|
|
1244
|
+
} else if (field.type === "select") {
|
|
1245
|
+
} else {
|
|
1246
|
+
data[key] = "";
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
for (const def of profile.defaults) {
|
|
1250
|
+
if (def.value === "today") data[def.key] = today;
|
|
1251
|
+
else if (def.value !== "infer") data[def.key] = def.value;
|
|
1252
|
+
}
|
|
1253
|
+
if (profile.inferField) {
|
|
1254
|
+
const inferred = profile.inferField({
|
|
1255
|
+
collection: entry.collection,
|
|
1256
|
+
name: entry.name,
|
|
1257
|
+
description: entry.description,
|
|
1258
|
+
data,
|
|
1259
|
+
entryId: "",
|
|
1260
|
+
linksCreated: [],
|
|
1261
|
+
linksSuggested: [],
|
|
1262
|
+
collectionFields: col.fields ?? []
|
|
1263
|
+
});
|
|
1264
|
+
for (const [key, val] of Object.entries(inferred)) {
|
|
1265
|
+
if (val !== void 0 && val !== "") data[key] = val;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
if (!data[profile.descriptionField] && !data.description && !data.canonical) {
|
|
1269
|
+
data[profile.descriptionField || "description"] = entry.description;
|
|
1270
|
+
}
|
|
1271
|
+
try {
|
|
1272
|
+
const result = await mcpMutation("chain.createEntry", {
|
|
1273
|
+
collectionSlug: entry.collection,
|
|
1274
|
+
entryId: entry.entryId ?? void 0,
|
|
1275
|
+
name: entry.name,
|
|
1276
|
+
status: "draft",
|
|
1277
|
+
data,
|
|
1278
|
+
createdBy,
|
|
1279
|
+
sessionId: agentId ?? void 0
|
|
1280
|
+
});
|
|
1281
|
+
const internalId = result.docId;
|
|
1282
|
+
const finalEntryId = result.entryId;
|
|
1283
|
+
let autoLinkCount = 0;
|
|
1284
|
+
const searchQuery = extractSearchTerms(entry.name, entry.description);
|
|
1285
|
+
if (searchQuery) {
|
|
1286
|
+
try {
|
|
1287
|
+
const searchResults = await mcpQuery("chain.searchEntries", { query: searchQuery });
|
|
1288
|
+
const candidates = (searchResults ?? []).filter((r) => r.entryId !== finalEntryId).map((r) => {
|
|
1289
|
+
const conf = computeLinkConfidence(r, entry.name, entry.description, entry.collection, collIdToSlug.get(r.collectionId) ?? "unknown");
|
|
1290
|
+
return {
|
|
1291
|
+
...r,
|
|
1292
|
+
collSlug: collIdToSlug.get(r.collectionId) ?? "unknown",
|
|
1293
|
+
confidence: conf.score
|
|
1294
|
+
};
|
|
1295
|
+
}).sort((a, b) => b.confidence - a.confidence);
|
|
1296
|
+
for (const c of candidates) {
|
|
1297
|
+
if (autoLinkCount >= MAX_AUTO_LINKS) break;
|
|
1298
|
+
if (c.confidence < AUTO_LINK_CONFIDENCE_THRESHOLD) break;
|
|
1299
|
+
if (!c.entryId) continue;
|
|
1300
|
+
const { type: relationType } = inferRelationType(entry.collection, c.collSlug, profile);
|
|
1301
|
+
try {
|
|
1302
|
+
await mcpMutation("chain.createEntryRelation", {
|
|
1303
|
+
fromEntryId: finalEntryId,
|
|
1304
|
+
toEntryId: c.entryId,
|
|
1305
|
+
type: relationType
|
|
1306
|
+
});
|
|
1307
|
+
autoLinkCount++;
|
|
1308
|
+
} catch {
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
} catch {
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
results.push({ name: entry.name, collection: entry.collection, entryId: finalEntryId, ok: true, autoLinks: autoLinkCount });
|
|
1315
|
+
await recordSessionActivity({ entryCreated: internalId });
|
|
1316
|
+
} catch (error) {
|
|
1317
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1318
|
+
results.push({ name: entry.name, collection: entry.collection, entryId: "", ok: false, autoLinks: 0, error: msg });
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
const created = results.filter((r) => r.ok);
|
|
1322
|
+
const failed = results.filter((r) => !r.ok);
|
|
1323
|
+
const totalAutoLinks = created.reduce((sum, r) => sum + r.autoLinks, 0);
|
|
1324
|
+
const byCollection = /* @__PURE__ */ new Map();
|
|
1325
|
+
for (const r of created) {
|
|
1326
|
+
byCollection.set(r.collection, (byCollection.get(r.collection) ?? 0) + 1);
|
|
1327
|
+
}
|
|
1328
|
+
const lines = [
|
|
1329
|
+
`# Batch Capture Complete`,
|
|
1330
|
+
`**${created.length}** created, **${failed.length}** failed out of ${entries.length} total.`,
|
|
1331
|
+
`**Auto-links created:** ${totalAutoLinks}`,
|
|
1332
|
+
""
|
|
1333
|
+
];
|
|
1334
|
+
if (byCollection.size > 0) {
|
|
1335
|
+
lines.push("## By Collection");
|
|
1336
|
+
for (const [col, count] of byCollection) {
|
|
1337
|
+
lines.push(`- \`${col}\`: ${count} entries`);
|
|
1338
|
+
}
|
|
1339
|
+
lines.push("");
|
|
1340
|
+
}
|
|
1341
|
+
if (created.length > 0) {
|
|
1342
|
+
lines.push("## Created");
|
|
1343
|
+
for (const r of created) {
|
|
1344
|
+
const linkNote = r.autoLinks > 0 ? ` (${r.autoLinks} auto-links)` : "";
|
|
1345
|
+
lines.push(`- **${r.entryId}**: ${r.name} [${r.collection}]${linkNote}`);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
if (failed.length > 0) {
|
|
1349
|
+
lines.push("");
|
|
1350
|
+
lines.push("## Failed");
|
|
1351
|
+
for (const r of failed) {
|
|
1352
|
+
lines.push(`- ${r.name} [${r.collection}]: _${r.error}_`);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
const entryIds = created.map((r) => r.entryId);
|
|
1356
|
+
if (entryIds.length > 0) {
|
|
1357
|
+
lines.push("");
|
|
1358
|
+
lines.push("## Next Steps");
|
|
1359
|
+
lines.push(`- **Connect:** Run \`suggest-links\` on key entries to build the knowledge graph`);
|
|
1360
|
+
lines.push(`- **Commit:** Use \`commit-entry\` to promote drafts to SSOT`);
|
|
1361
|
+
lines.push(`- **Quality:** Run \`quality-check\` on individual entries to assess completeness`);
|
|
1362
|
+
}
|
|
1363
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1364
|
+
}
|
|
1365
|
+
);
|
|
1366
|
+
server.registerTool(
|
|
1367
|
+
"quality-check",
|
|
1368
|
+
{
|
|
1369
|
+
title: "Quality Check",
|
|
1370
|
+
description: "Score an existing knowledge entry against collection-specific quality criteria. Returns a scorecard (X/10) with specific, actionable suggestions for improvement \u2014 including concrete link suggestions from graph analysis when relations are missing.\n\nChecks: name clarity, description completeness, relation connectedness, and collection-specific fields.\n\nUse after creating entries to assess their quality, or to audit existing entries.",
|
|
1371
|
+
inputSchema: {
|
|
1372
|
+
entryId: z.string().describe("Entry ID to check, e.g. 'TEN-graph-db', 'GT-019', 'SOS-006'")
|
|
1373
|
+
},
|
|
1374
|
+
annotations: { readOnlyHint: true }
|
|
1375
|
+
},
|
|
1376
|
+
async ({ entryId }) => {
|
|
1377
|
+
const result = await checkEntryQuality(entryId);
|
|
1378
|
+
const needsRelations = result.quality.checks.some(
|
|
1379
|
+
(c) => !c.passed && (c.id === "has-relations" || c.id === "diverse-relations")
|
|
1380
|
+
);
|
|
1381
|
+
if (needsRelations) {
|
|
1382
|
+
try {
|
|
1383
|
+
const suggestions = await mcpQuery("chain.graphSuggestLinks", {
|
|
1384
|
+
entryId,
|
|
1385
|
+
maxHops: 2,
|
|
1386
|
+
limit: 3
|
|
1387
|
+
});
|
|
1388
|
+
if (suggestions?.suggestions?.length > 0) {
|
|
1389
|
+
const linkHints = suggestions.suggestions.map((s) => ` \u2192 \`relate-entries from='${entryId}' to='${s.entryId}' type='${s.recommendedRelationType}'\` \u2014 ${s.name} [${s.collectionSlug}] (${s.score}/100)`).join("\n");
|
|
1390
|
+
result.text += `
|
|
1391
|
+
|
|
1392
|
+
## Suggested Links to Improve Quality
|
|
1393
|
+
${linkHints}`;
|
|
1394
|
+
}
|
|
1395
|
+
} catch {
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
try {
|
|
1399
|
+
const verdict = await mcpQuery("quality.getLatestVerdictForEntry", { entryId });
|
|
1400
|
+
if (verdict && verdict.criteria?.length > 0) {
|
|
1401
|
+
result.text += "\n\n" + formatRubricVerdictSection(verdict);
|
|
1402
|
+
try {
|
|
1403
|
+
const wsForTracking = await getWorkspaceContext();
|
|
1404
|
+
trackQualityCheck(wsForTracking.workspaceId, {
|
|
1405
|
+
entry_id: entryId,
|
|
1406
|
+
entry_type: verdict.canonicalKey ?? "",
|
|
1407
|
+
tier: verdict.tier,
|
|
1408
|
+
passed: verdict.passed,
|
|
1409
|
+
source: verdict.source,
|
|
1410
|
+
llm_status: verdict.llmStatus,
|
|
1411
|
+
llm_duration_ms: verdict.llmDurationMs,
|
|
1412
|
+
llm_error: verdict.llmError,
|
|
1413
|
+
has_roger_martin: !!verdict.rogerMartin
|
|
1414
|
+
});
|
|
1415
|
+
} catch {
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
} catch {
|
|
1419
|
+
}
|
|
1420
|
+
return { content: [{ type: "text", text: result.text }] };
|
|
1421
|
+
}
|
|
1422
|
+
);
|
|
1423
|
+
server.registerTool(
|
|
1424
|
+
"re-evaluate",
|
|
1425
|
+
{
|
|
1426
|
+
title: "Re-evaluate Entry Quality",
|
|
1427
|
+
description: "Trigger a fresh quality evaluation on an existing entry without creating a new one. Useful for canary testing or re-assessing after edits. Returns the heuristic verdict immediately and schedules LLM evaluation in the background.",
|
|
1428
|
+
inputSchema: {
|
|
1429
|
+
entryId: z.string().describe("Entry ID to re-evaluate, e.g. 'VIS-001', 'STR-012'"),
|
|
1430
|
+
context: z.enum(["capture", "commit", "review"]).default("review").describe("Evaluation context")
|
|
1431
|
+
}
|
|
1432
|
+
},
|
|
1433
|
+
async ({ entryId, context }) => {
|
|
1434
|
+
const ws = await getWorkspaceContext();
|
|
1435
|
+
const result = await mcpMutation("quality.reEvaluateEntry", {
|
|
1436
|
+
entryId,
|
|
1437
|
+
context: context ?? "review"
|
|
1438
|
+
});
|
|
1439
|
+
if (!result) {
|
|
1440
|
+
return { content: [{ type: "text", text: `Entry \`${entryId}\` not found or has no rubric.` }] };
|
|
1441
|
+
}
|
|
1442
|
+
const llmScheduled = result.verdict.tier !== "passive";
|
|
1443
|
+
const lines = [`Re-evaluated \`${entryId}\` in \`${context ?? "review"}\` context.`];
|
|
1444
|
+
lines.push("");
|
|
1445
|
+
lines.push(formatRubricVerdictSection({
|
|
1446
|
+
...result.verdict,
|
|
1447
|
+
source: result.source,
|
|
1448
|
+
llmStatus: llmScheduled ? "pending" : "skipped"
|
|
1449
|
+
}));
|
|
1450
|
+
try {
|
|
1451
|
+
trackQualityVerdict(ws.workspaceId, {
|
|
1452
|
+
entry_id: entryId,
|
|
1453
|
+
entry_type: result.verdict.canonicalKey ?? "",
|
|
1454
|
+
tier: result.verdict.tier,
|
|
1455
|
+
context: context ?? "review",
|
|
1456
|
+
passed: result.verdict.passed,
|
|
1457
|
+
source: "heuristic",
|
|
1458
|
+
criteria_total: result.verdict.criteria?.length ?? 0,
|
|
1459
|
+
criteria_failed: result.verdict.criteria?.filter((c) => !c.passed).length ?? 0,
|
|
1460
|
+
llm_scheduled: llmScheduled
|
|
1461
|
+
});
|
|
1462
|
+
} catch {
|
|
1463
|
+
}
|
|
1464
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1465
|
+
}
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
function extractPreview(data, maxLen) {
|
|
1469
|
+
if (!data || typeof data !== "object") return "";
|
|
1470
|
+
const raw = data.description ?? data.canonical ?? data.detail ?? data.rule ?? "";
|
|
1471
|
+
if (typeof raw !== "string" || !raw) return "";
|
|
1472
|
+
return raw.length > maxLen ? raw.substring(0, maxLen) + "..." : raw;
|
|
1473
|
+
}
|
|
1474
|
+
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
1475
|
+
"the",
|
|
1476
|
+
"and",
|
|
1477
|
+
"for",
|
|
1478
|
+
"are",
|
|
1479
|
+
"but",
|
|
1480
|
+
"not",
|
|
1481
|
+
"you",
|
|
1482
|
+
"all",
|
|
1483
|
+
"can",
|
|
1484
|
+
"has",
|
|
1485
|
+
"her",
|
|
1486
|
+
"was",
|
|
1487
|
+
"one",
|
|
1488
|
+
"our",
|
|
1489
|
+
"out",
|
|
1490
|
+
"day",
|
|
1491
|
+
"had",
|
|
1492
|
+
"hot",
|
|
1493
|
+
"how",
|
|
1494
|
+
"its",
|
|
1495
|
+
"may",
|
|
1496
|
+
"new",
|
|
1497
|
+
"now",
|
|
1498
|
+
"old",
|
|
1499
|
+
"see",
|
|
1500
|
+
"way",
|
|
1501
|
+
"who",
|
|
1502
|
+
"did",
|
|
1503
|
+
"get",
|
|
1504
|
+
"let",
|
|
1505
|
+
"say",
|
|
1506
|
+
"she",
|
|
1507
|
+
"too",
|
|
1508
|
+
"use",
|
|
1509
|
+
"from",
|
|
1510
|
+
"have",
|
|
1511
|
+
"been",
|
|
1512
|
+
"each",
|
|
1513
|
+
"that",
|
|
1514
|
+
"this",
|
|
1515
|
+
"with",
|
|
1516
|
+
"will",
|
|
1517
|
+
"they",
|
|
1518
|
+
"what",
|
|
1519
|
+
"when",
|
|
1520
|
+
"make",
|
|
1521
|
+
"like",
|
|
1522
|
+
"long",
|
|
1523
|
+
"look",
|
|
1524
|
+
"many",
|
|
1525
|
+
"some",
|
|
1526
|
+
"them",
|
|
1527
|
+
"than",
|
|
1528
|
+
"most",
|
|
1529
|
+
"only",
|
|
1530
|
+
"over",
|
|
1531
|
+
"such",
|
|
1532
|
+
"into",
|
|
1533
|
+
"also",
|
|
1534
|
+
"back",
|
|
1535
|
+
"just",
|
|
1536
|
+
"much",
|
|
1537
|
+
"must",
|
|
1538
|
+
"name",
|
|
1539
|
+
"very",
|
|
1540
|
+
"your",
|
|
1541
|
+
"after",
|
|
1542
|
+
"which",
|
|
1543
|
+
"their",
|
|
1544
|
+
"about",
|
|
1545
|
+
"would",
|
|
1546
|
+
"there",
|
|
1547
|
+
"should",
|
|
1548
|
+
"could",
|
|
1549
|
+
"other",
|
|
1550
|
+
"these",
|
|
1551
|
+
"first",
|
|
1552
|
+
"being",
|
|
1553
|
+
"those",
|
|
1554
|
+
"still",
|
|
1555
|
+
"where"
|
|
1556
|
+
]);
|
|
1557
|
+
async function runContradictionCheck(name, description) {
|
|
1558
|
+
const warnings = [];
|
|
1559
|
+
try {
|
|
1560
|
+
const text = `${name} ${description}`.toLowerCase();
|
|
1561
|
+
const keyTerms = text.split(/\s+/).filter((w) => w.length >= 4 && !STOP_WORDS.has(w)).slice(0, 8);
|
|
1562
|
+
if (keyTerms.length === 0) return warnings;
|
|
1563
|
+
const searchQuery = keyTerms.slice(0, 3).join(" ");
|
|
1564
|
+
const [govResults, archResults] = await Promise.all([
|
|
1565
|
+
mcpQuery("chain.searchEntries", { query: searchQuery, collectionSlug: "business-rules" }),
|
|
1566
|
+
mcpQuery("chain.searchEntries", { query: searchQuery, collectionSlug: "architecture" })
|
|
1567
|
+
]);
|
|
1568
|
+
const allGov = [...govResults ?? [], ...archResults ?? []].slice(0, 5);
|
|
1569
|
+
for (const entry of allGov) {
|
|
1570
|
+
const entryText = `${entry.name} ${entry.data?.description ?? ""}`.toLowerCase();
|
|
1571
|
+
const matched = keyTerms.filter((t) => entryText.includes(t));
|
|
1572
|
+
if (matched.length < 2) continue;
|
|
1573
|
+
let governsCount = 0;
|
|
1574
|
+
try {
|
|
1575
|
+
const relations = await mcpQuery("chain.listEntryRelations", {
|
|
1576
|
+
entryId: entry.entryId
|
|
1577
|
+
});
|
|
1578
|
+
governsCount = (relations ?? []).filter((r) => r.type === "governs").length;
|
|
1579
|
+
} catch {
|
|
1580
|
+
}
|
|
1581
|
+
warnings.push({
|
|
1582
|
+
entryId: entry.entryId ?? "",
|
|
1583
|
+
name: entry.name,
|
|
1584
|
+
collection: entry.collectionSlug ?? "",
|
|
1585
|
+
governsCount
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
} catch {
|
|
1589
|
+
}
|
|
1590
|
+
return warnings;
|
|
1591
|
+
}
|
|
1592
|
+
function formatRubricCoaching(result) {
|
|
1593
|
+
const { verdict, rogerMartin } = result;
|
|
1594
|
+
if (!verdict || verdict.criteria.length === 0) return "";
|
|
1595
|
+
const lines = ["## Semantic Quality"];
|
|
1596
|
+
const failed = (verdict.criteria ?? []).filter((c) => !c.passed);
|
|
1597
|
+
const total = verdict.criteria?.length ?? 0;
|
|
1598
|
+
const passedCount = total - failed.length;
|
|
1599
|
+
if (verdict.passed) {
|
|
1600
|
+
lines.push(`All ${total} rubric criteria pass for \`${verdict.canonicalKey}\` (${verdict.tier} tier).`);
|
|
1601
|
+
} else {
|
|
1602
|
+
lines.push(`${passedCount}/${total} criteria pass for \`${verdict.canonicalKey}\` (${verdict.tier} tier)`);
|
|
1603
|
+
lines.push("");
|
|
1604
|
+
for (const c of verdict.criteria) {
|
|
1605
|
+
const icon = c.passed ? "[x]" : "[ ]";
|
|
1606
|
+
const extra = c.passed ? "" : ` \u2014 ${c.hint}`;
|
|
1607
|
+
lines.push(`${icon} ${c.id}${extra}`);
|
|
1608
|
+
}
|
|
1609
|
+
if (verdict.weakest) {
|
|
1610
|
+
lines.push("");
|
|
1611
|
+
lines.push(`**Coaching hint:** ${verdict.weakest.hint}`);
|
|
1612
|
+
lines.push(`_Question to consider:_ ${verdict.weakest.questionTemplate}`);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
if (rogerMartin) {
|
|
1616
|
+
lines.push("");
|
|
1617
|
+
lines.push("### Roger Martin Test");
|
|
1618
|
+
if (rogerMartin.isStrategicChoice) {
|
|
1619
|
+
lines.push("This principle passes \u2014 the opposite is a reasonable strategic choice.");
|
|
1620
|
+
} else {
|
|
1621
|
+
lines.push(`This principle may not be a strategic choice. ${rogerMartin.reasoning}`);
|
|
1622
|
+
if (rogerMartin.suggestion) {
|
|
1623
|
+
lines.push(`_Suggestion:_ ${rogerMartin.suggestion}`);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
return lines.join("\n");
|
|
1628
|
+
}
|
|
1629
|
+
function formatRubricVerdictSection(verdict) {
|
|
1630
|
+
if (!verdict || !verdict.criteria || verdict.criteria.length === 0) return "";
|
|
1631
|
+
const lines = ["## Semantic Quality"];
|
|
1632
|
+
const failed = verdict.criteria.filter((c) => !c.passed);
|
|
1633
|
+
const total = verdict.criteria.length;
|
|
1634
|
+
const passedCount = total - failed.length;
|
|
1635
|
+
const durationSuffix = verdict.llmDurationMs ? ` in ${(verdict.llmDurationMs / 1e3).toFixed(1)}s` : "";
|
|
1636
|
+
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}` : "";
|
|
1637
|
+
if (failed.length === 0) {
|
|
1638
|
+
lines.push(`All ${total} rubric criteria pass for \`${verdict.canonicalKey}\` (${verdict.tier} tier, ${verdict.source} evaluation).${statusNote}`);
|
|
1639
|
+
} else {
|
|
1640
|
+
lines.push(`${passedCount}/${total} criteria pass for \`${verdict.canonicalKey}\` (${verdict.tier} tier, ${verdict.source} evaluation)${statusNote}`);
|
|
1641
|
+
lines.push("");
|
|
1642
|
+
for (const c of verdict.criteria) {
|
|
1643
|
+
const icon = c.passed ? "[x]" : "[ ]";
|
|
1644
|
+
const extra = c.passed ? "" : ` \u2014 ${c.hint}`;
|
|
1645
|
+
lines.push(`${icon} ${c.id}${extra}`);
|
|
1646
|
+
}
|
|
1647
|
+
if (verdict.weakest) {
|
|
1648
|
+
lines.push("");
|
|
1649
|
+
lines.push(`**Top improvement:** ${verdict.weakest.hint}`);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
if (verdict.rogerMartin) {
|
|
1653
|
+
const rm = verdict.rogerMartin;
|
|
1654
|
+
lines.push("");
|
|
1655
|
+
lines.push("### Roger Martin Test");
|
|
1656
|
+
if (rm.isStrategicChoice) {
|
|
1657
|
+
lines.push(`This is a real strategic choice \u2014 the opposite is reasonable. ${rm.reasoning}`);
|
|
1658
|
+
} else {
|
|
1659
|
+
lines.push(`This may not be a strategic choice. ${rm.reasoning}`);
|
|
1660
|
+
if (rm.suggestion) {
|
|
1661
|
+
lines.push(`_Suggestion:_ ${rm.suggestion}`);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
return lines.join("\n");
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
export {
|
|
1669
|
+
runWithAuth,
|
|
1670
|
+
getAgentSessionId,
|
|
1671
|
+
isSessionOriented,
|
|
1672
|
+
setSessionOriented,
|
|
1673
|
+
startAgentSession,
|
|
1674
|
+
closeAgentSession,
|
|
1675
|
+
orphanAgentSession,
|
|
1676
|
+
recordSessionActivity,
|
|
1677
|
+
bootstrap,
|
|
1678
|
+
bootstrapHttp,
|
|
1679
|
+
getAuditLog,
|
|
1680
|
+
mcpCall,
|
|
1681
|
+
getWorkspaceId,
|
|
1682
|
+
getWorkspaceContext,
|
|
1683
|
+
mcpQuery,
|
|
1684
|
+
mcpMutation,
|
|
1685
|
+
requireActiveSession,
|
|
1686
|
+
requireWriteAccess,
|
|
1687
|
+
recoverSessionState,
|
|
1688
|
+
formatQualityReport,
|
|
1689
|
+
checkEntryQuality,
|
|
1690
|
+
registerSmartCaptureTools,
|
|
1691
|
+
runContradictionCheck,
|
|
1692
|
+
formatRubricCoaching
|
|
1693
|
+
};
|
|
1694
|
+
//# sourceMappingURL=chunk-P5KVCIYN.js.map
|