@productbrain/mcp 0.0.1-beta.11217 → 0.0.1-beta.11229

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.
@@ -32,21 +32,16 @@ import {
32
32
  runWithToolContext,
33
33
  setSessionOriented,
34
34
  startAgentSession,
35
- trackCaptureClassifierAutoRouted,
36
- trackCaptureClassifierEvaluated,
37
- trackCaptureClassifierFallback,
38
35
  trackCaptureContractMiss,
39
36
  trackCaptureQualityHints,
40
37
  trackChainEntryCommitted,
41
- trackClassifierDivergence,
42
- trackCollectionClassified,
43
38
  trackCommitErrorByCode,
44
39
  trackKnowledgeGap,
45
40
  trackQualityCheck,
46
41
  trackQualityVerdict,
47
42
  trackSessionCaptureRate,
48
43
  trackZeroCaptureAuditFired
49
- } from "./chunk-OSTJZIKJ.js";
44
+ } from "./chunk-Z4MDUM6J.js";
50
45
 
51
46
  // src/server.ts
52
47
  import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -578,10 +573,6 @@ function clearSessionGaps() {
578
573
  const sessionKey = currentSessionKey();
579
574
  sessionGaps.delete(sessionKey);
580
575
  }
581
- function resolveGapsForEntry(entryName, entryId) {
582
- kernelMutation("gaps.resolve", { entryName, entryId }).catch(() => {
583
- });
584
- }
585
576
 
586
577
  // src/compound-tool.ts
587
578
  function backpressureResult(toolName, action, spec, detail) {
@@ -699,226 +690,6 @@ Read it with \`context action=get\`.`
699
690
  };
700
691
  }
701
692
 
702
- // src/lib/scopedCache.ts
703
- var ScopedCache = class {
704
- constructor(ttlMs, maxScopes = 100) {
705
- this.ttlMs = ttlMs;
706
- this.maxScopes = maxScopes;
707
- }
708
- store = /* @__PURE__ */ new Map();
709
- get(scope) {
710
- const e = this.store.get(scope);
711
- if (!e) return void 0;
712
- if (Date.now() > e.expiresAt) {
713
- this.store.delete(scope);
714
- return void 0;
715
- }
716
- return e.value;
717
- }
718
- set(scope, value) {
719
- this.store.set(scope, { value, expiresAt: Date.now() + this.ttlMs });
720
- if (this.store.size > this.maxScopes) this.evict();
721
- }
722
- delete(scope) {
723
- this.store.delete(scope);
724
- }
725
- clear() {
726
- this.store.clear();
727
- }
728
- size() {
729
- return this.store.size;
730
- }
731
- /** Drop expired entries first; if still over budget, drop the oldest-expiring. */
732
- evict() {
733
- const now = Date.now();
734
- for (const [k, e] of this.store) {
735
- if (now > e.expiresAt) this.store.delete(k);
736
- }
737
- if (this.store.size > this.maxScopes) {
738
- const sorted = [...this.store.entries()].sort((a, b) => a[1].expiresAt - b[1].expiresAt);
739
- const overBy = sorted.length - this.maxScopes;
740
- for (let i = 0; i < overBy; i++) {
741
- this.store.delete(sorted[i][0]);
742
- }
743
- }
744
- }
745
- };
746
- var COLLECTION_CACHE_TTL_MS = 6e4;
747
-
748
- // src/lib/smartProfileCollections.ts
749
- var SMART_PROFILE_COLLECTIONS = [
750
- "tensions",
751
- "glossary",
752
- "decisions",
753
- "strategy",
754
- "insights"
755
- ];
756
-
757
- // src/lib/resolveCollection.ts
758
- async function resolveCollection(params) {
759
- const { name, description, typeHint, allowReviewRouting } = params;
760
- const sessionId = getAgentSessionId();
761
- const result2 = await kernelCall("chain.resolveCollection", {
762
- entryName: name,
763
- entryDescription: description,
764
- ...typeHint ? { typeHint } : {},
765
- ...allowReviewRouting ? { allowReviewRouting } : {},
766
- ...sessionId ? { agentSessionId: sessionId } : {}
767
- });
768
- return result2 ?? null;
769
- }
770
-
771
- // src/lib/inferSourceDate.ts
772
- function inferSourceDate(...values) {
773
- const combined = values.filter((value) => typeof value === "string" && value.trim().length > 0).join("\n");
774
- if (!combined) return void 0;
775
- const isoMatch = combined.match(/\b(20\d{2}-\d{2}-\d{2})\b/);
776
- if (isoMatch) return isoMatch[1];
777
- const monthMatch = combined.match(
778
- /\b(january|february|march|april|may|june|july|august|september|october|november|december)\s+(\d{1,2})(?:st|nd|rd|th)?(?:,?\s+(20\d{2}))?\b/i
779
- );
780
- if (!monthMatch) return void 0;
781
- const monthIndex = [
782
- "january",
783
- "february",
784
- "march",
785
- "april",
786
- "may",
787
- "june",
788
- "july",
789
- "august",
790
- "september",
791
- "october",
792
- "november",
793
- "december"
794
- ].indexOf(monthMatch[1].toLowerCase());
795
- if (monthIndex < 0) return void 0;
796
- const inferredYear = monthMatch[3] ?? combined.match(/\b(20\d{2})\b/)?.[1] ?? String((/* @__PURE__ */ new Date()).getUTCFullYear());
797
- const month = String(monthIndex + 1).padStart(2, "0");
798
- const day = monthMatch[2].padStart(2, "0");
799
- return `${inferredYear}-${month}-${day}`;
800
- }
801
-
802
- // src/lib/collectionCache.ts
803
- var collectionCache = new ScopedCache(COLLECTION_CACHE_TTL_MS);
804
- async function getCollections() {
805
- const scope = cacheScope();
806
- const hit = collectionCache.get(scope);
807
- if (hit) return hit.collections;
808
- const result2 = await kernelQuery("chain.listCollections") ?? [];
809
- const bySlug = new Map(result2.map((c) => [c.slug, c]));
810
- collectionCache.set(scope, { collections: result2, bySlug });
811
- return result2;
812
- }
813
- async function getCollectionBySlug(slug) {
814
- await getCollections();
815
- return collectionCache.get(cacheScope())?.bySlug.get(slug);
816
- }
817
- function invalidateCollectionCacheForScope() {
818
- collectionCache.delete(cacheScope());
819
- }
820
- async function isGoverned(slug) {
821
- const col = await getCollectionBySlug(slug);
822
- return !!col?.governed;
823
- }
824
-
825
- // src/lib/entryDataNormalization.ts
826
- function buildDataFromFields(fields, descriptionField, descriptionValue) {
827
- if (!descriptionField) return null;
828
- const data = {};
829
- for (const field of fields) {
830
- if (field.key === descriptionField) {
831
- data[field.key] = descriptionValue;
832
- }
833
- }
834
- return data;
835
- }
836
- function resolveDescriptionField(fields, descriptionFieldKey) {
837
- const verifiedDescriptionFieldKey = descriptionFieldKey && fields.some((field) => field.key === descriptionFieldKey) ? descriptionFieldKey : void 0;
838
- const literalDescriptionField = fields.find(
839
- (field) => field.key === "description" && (field.type === "text" || field.type === "string")
840
- )?.key;
841
- const layoutDescriptionField = fields.find(
842
- (field) => field.zone === "body" || field.displayHint === "textarea" || field.displayHint === "section"
843
- )?.key;
844
- const freeTextCandidates = fields.filter((field) => field.zone !== "meta" && field.zone !== "header");
845
- const firstTextField = freeTextCandidates.find((field) => field.type === "text")?.key;
846
- const firstStringField = freeTextCandidates.find((field) => field.type === "string")?.key;
847
- return verifiedDescriptionFieldKey || literalDescriptionField || layoutDescriptionField || firstTextField || firstStringField || null;
848
- }
849
- function noDescriptionFieldMessage(collectionSlug) {
850
- return `\`${collectionSlug}\` has no field for captured free text \u2014 no descriptionFieldKey is declared, no layout body field, and no text/string field outside its meta/header zone. Ask a platform admin to declare a descriptionFieldKey (or a body-zoned text field) on this collection before capturing into it.`;
851
- }
852
- function resolveProfileDescriptionField(col, fieldsOverride) {
853
- const fields = fieldsOverride ?? col.fields ?? [];
854
- return resolveDescriptionField(fields, col.descriptionFieldKey);
855
- }
856
- function resolveGenericDescriptionField(fieldsOverride) {
857
- return fieldsOverride ? resolveDescriptionField(fieldsOverride, void 0) : null;
858
- }
859
- function buildDataFromFieldsOrRefuse(fields, descriptionField, descriptionValue, collectionSlug) {
860
- if (!descriptionField) {
861
- const msg = noDescriptionFieldMessage(collectionSlug);
862
- const recovery = `Pass the text via an existing field in \`data\` (see \`collections action=get\` for ${collectionSlug}'s field list), or ask a platform admin to declare a descriptionFieldKey (or a body-zoned text field) on ${collectionSlug}.`;
863
- return {
864
- refusal: failureResult(msg, "VALIDATION_FAILED", `Collection '${collectionSlug}' has no field for free text.`, recovery)
865
- };
866
- }
867
- return { data: buildDataFromFields(fields, descriptionField, descriptionValue), descriptionField };
868
- }
869
- function isEmptyValue(v) {
870
- return v == null || (Array.isArray(v) ? v.length === 0 : String(v).trim() === "");
871
- }
872
- function isNonEmptyValue(v) {
873
- return v != null && (Array.isArray(v) ? v.length > 0 : String(v).trim() !== "");
874
- }
875
- function levenshtein(a, b) {
876
- const m = a.length;
877
- const n = b.length;
878
- const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
879
- for (let i = 0; i <= m; i++) dp[i][0] = i;
880
- for (let j = 0; j <= n; j++) dp[0][j] = j;
881
- for (let i = 1; i <= m; i++) {
882
- for (let j = 1; j <= n; j++) {
883
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
884
- dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
885
- }
886
- }
887
- return dp[m][n];
888
- }
889
- var APPETITE_OPTIONS = ["small", "medium", "large"];
890
- function findClosestOption(value, options) {
891
- const lower = value.toLowerCase();
892
- const exact = options.find((o) => o.toLowerCase() === lower);
893
- if (exact) return { opt: exact, dist: 0 };
894
- return options.reduce(
895
- (acc, opt) => {
896
- const d = levenshtein(lower, opt.toLowerCase());
897
- return d < acc.dist ? { opt, dist: d } : acc;
898
- },
899
- { opt: options[0], dist: Infinity }
900
- );
901
- }
902
- function canonicalizeSelects(data, fields, isBet) {
903
- if (isBet && data.appetite != null && typeof data.appetite === "string") {
904
- const val = data.appetite.trim();
905
- if (val) {
906
- const { opt, dist } = findClosestOption(val, APPETITE_OPTIONS);
907
- if (dist <= 4) data.appetite = opt;
908
- }
909
- }
910
- for (const field of fields) {
911
- if (field.type !== "select" || !field.options?.length) continue;
912
- if (field.key === "appetite" && isBet) continue;
913
- const val = data[field.key];
914
- if (val == null || typeof val !== "string") continue;
915
- const trimmed = val.trim();
916
- if (!trimmed) continue;
917
- const { opt, dist } = findClosestOption(trimmed, field.options);
918
- if (dist <= 4) data[field.key] = opt;
919
- }
920
- }
921
-
922
693
  // src/lib/captureForRealAction.ts
923
694
  function buildCaptureForRealNextAction(payload) {
924
695
  const {
@@ -1130,9 +901,6 @@ var captureSinglePreviewOutputSchema = z3.object({
1130
901
  taskAlignment: taskAlignmentSchema.optional()
1131
902
  }).strict();
1132
903
 
1133
- // src/lib/readinessNotices.ts
1134
- var READINESS_PROVISIONAL_LINE = "Readiness is provisional: its sample omitted accepted entries. Counts are lower bounds, apparent gaps may already be covered.";
1135
-
1136
904
  // src/lib/limits.ts
1137
905
  var ENTRY_BODY_MAX_CHARS = 15e4;
1138
906
  var ENTRY_NAME_MAX_CHARS = 250;
@@ -1142,46 +910,13 @@ var RELATION_BATCH_MAX = 25;
1142
910
 
1143
911
  // src/tools/smart-capture.ts
1144
912
  var CAPTURE_LINK_OUTCOMES = ["written", "refused"];
1145
- var GENERIC_PROFILE = {
1146
- governedDraft: false,
1147
- descriptionField: null,
1148
- // TEN-3704 F4: never a hard-coded "description" — see resolveGenericDescriptionField
1149
- defaults: []
1150
- };
1151
- async function buildRuntimeProfile(slug, fieldsOverride) {
1152
- const col = await getCollectionBySlug(slug);
1153
- if (!col) return { ...GENERIC_PROFILE, descriptionField: resolveGenericDescriptionField(fieldsOverride) };
1154
- const descriptionField = resolveProfileDescriptionField(col, fieldsOverride);
1155
- return {
1156
- governedDraft: col.governed === true,
1157
- descriptionField,
1158
- defaults: []
1159
- };
1160
- }
1161
- var profileCache = new ScopedCache(COLLECTION_CACHE_TTL_MS);
1162
- async function getProfile(slug, fieldsOverride) {
1163
- const scope = cacheScope();
1164
- let map = profileCache.get(scope);
1165
- if (!map) {
1166
- map = /* @__PURE__ */ new Map();
1167
- profileCache.set(scope, map);
1168
- }
1169
- const cached = map.get(slug);
1170
- if (cached) return cached;
1171
- const profile = await buildRuntimeProfile(slug, fieldsOverride);
1172
- map.set(slug, profile);
1173
- return profile;
1174
- }
1175
- function invalidateProfileCacheForScope() {
1176
- profileCache.delete(cacheScope());
1177
- }
1178
913
  var SPINE_FLOOR_SENTENCE = "kept as a draft; it can be accepted once its spine above is accepted \u2014 the promote door checks the whole spine and names the first missing rung.";
1179
914
  var SPINE_FLOOR_EXEMPT_SENTENCE = "kept as a draft, though this workspace is not yet enforcing the spine above it \u2014 it can be accepted as-is.";
1180
915
  var captureSchema = z4.object({
1181
- collection: z4.string().optional().describe("Collection slug, e.g. 'tensions', 'rules', 'glossary', 'decisions'. Optional \u2014 classifier auto-routes when omitted."),
916
+ collection: z4.string().optional().describe("Collection slug, e.g. 'tensions', 'rules', 'glossary', 'decisions'. Optional \u2014 the kernel classifies when omitted."),
1182
917
  name: z4.string().describe("Display name \u2014 be specific (e.g. 'Convex adjacency list won't scale for graph traversal')"),
1183
918
  description: z4.string().describe("Full context \u2014 what's happening, why it matters, what you observed"),
1184
- context: z4.string().optional().describe("Optional additional context (e.g. 'Observed during context gather calls taking 700ms+')"),
919
+ context: z4.string().optional().describe("Optional additional context (e.g. 'Observed during context gather calls taking 700ms+'); appended to the description, stored only when the description is."),
1185
920
  canonicalKey: z4.string().optional().describe("Semantic type (e.g. 'decision', 'tension', 'vision'). Auto-assigned from collection if omitted."),
1186
921
  data: z4.record(z4.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."),
1187
922
  why: z4.string().optional().describe("Reasoning behind this entry \u2014 hard-required for purpose/vision/strategy/objective/decision/rule/key_result/product."),
@@ -1218,236 +953,10 @@ var captureClassifierSchema = z4.object({
1218
953
  ),
1219
954
  agentProvidedCollection: z4.string().optional(),
1220
955
  overrideCommand: z4.string().optional(),
1221
- classifiedBy: z4.enum(["llm", "heuristic", "explicit"]).optional(),
1222
- confidenceTier: z4.enum(["high", "medium", "low"]).optional(),
956
+ classifiedBy: z4.string().optional(),
957
+ confidenceTier: z4.string().optional(),
1223
958
  reasoning: z4.string().optional()
1224
959
  });
1225
- function trackClassifierTelemetry(params) {
1226
- const telemetry = {
1227
- predicted_collection: params.predictedCollection,
1228
- confidence: params.confidence,
1229
- auto_routed: params.autoRouted,
1230
- reason_category: params.reasonCategory,
1231
- explicit_collection_provided: params.explicitCollectionProvided
1232
- };
1233
- trackCaptureClassifierEvaluated(params.workspaceId, telemetry);
1234
- if (params.outcome === "auto-routed") {
1235
- trackCaptureClassifierAutoRouted(params.workspaceId, telemetry);
1236
- return;
1237
- }
1238
- trackCaptureClassifierFallback(params.workspaceId, telemetry);
1239
- }
1240
- function buildClassifierUnknownResult() {
1241
- return {
1242
- content: [{
1243
- type: "text",
1244
- text: "I could not infer a collection confidently from this input.\n\nPlease provide `collection`, or rewrite with clearer intent (decision/problem/definition/insight/work-package)."
1245
- }],
1246
- structuredContent: failure(
1247
- "VALIDATION_ERROR",
1248
- "Could not infer collection from input.",
1249
- "Provide collection explicitly, or rewrite with clearer intent.",
1250
- [{ tool: "collections", description: "List available collections", parameters: { action: "list" } }],
1251
- { classifier: { enabled: true, autoRouted: false, agrees: false, abstained: true, topConfidence: 0, confidence: 0, reasons: [], candidates: [] } }
1252
- )
1253
- };
1254
- }
1255
- function buildLowConfidenceResult(resolved, classifierMeta) {
1256
- const allCandidates = [
1257
- { collection: resolved.collection, confidence: resolved.confidence },
1258
- ...resolved.alternatives
1259
- ];
1260
- const candidateFieldCounts = /* @__PURE__ */ new Map();
1261
- for (const c of classifierMeta.candidates) {
1262
- if (c.requiredFieldCount !== void 0) {
1263
- candidateFieldCounts.set(c.collection, c.requiredFieldCount);
1264
- }
1265
- }
1266
- const suggestions = allCandidates.map((c) => {
1267
- const fieldCost = candidateFieldCounts.get(c.collection);
1268
- const fieldNote = fieldCost !== void 0 ? `, ${fieldCost} required field${fieldCost === 1 ? "" : "s"}` : "";
1269
- return `- \`${c.collection}\` (${c.confidence}% confidence${fieldNote})`;
1270
- }).join("\n");
1271
- const textBody = `Low confidence classification (${resolved.confidence}%, classified by ${resolved.classifiedBy}).
1272
-
1273
- Predicted: \`${resolved.collection}\`
1274
- Reason: ${resolved.reasoning || "low signal"}
1275
-
1276
- Choose one of these and retry with \`collection\`:
1277
- ${suggestions}
1278
-
1279
- Correction path: rerun with your chosen \`collection\`, or capture and use \`entries action=move\` later.`;
1280
- return {
1281
- content: [{ type: "text", text: textBody }],
1282
- structuredContent: failure(
1283
- "VALIDATION_ERROR",
1284
- `Low confidence routing to '${resolved.collection}' (${resolved.confidence}%, ${resolved.classifiedBy}).`,
1285
- "Rerun with explicit collection.",
1286
- allCandidates.map((c) => ({
1287
- tool: "capture",
1288
- description: `Capture to ${c.collection}`,
1289
- parameters: { collection: c.collection }
1290
- })),
1291
- { classifier: classifierMeta }
1292
- )
1293
- };
1294
- }
1295
- function buildClassifierMeta(resolved, overrides = {}) {
1296
- return {
1297
- enabled: true,
1298
- autoRouted: false,
1299
- agrees: true,
1300
- abstained: false,
1301
- topConfidence: resolved.confidence,
1302
- confidence: resolved.confidence,
1303
- reasons: resolved.reasoning ? [resolved.reasoning] : [],
1304
- candidates: [
1305
- { collection: resolved.collection, confidence: resolved.confidence },
1306
- ...resolved.alternatives.map((a) => ({ collection: a.collection, confidence: a.confidence }))
1307
- ].slice(0, 3),
1308
- classifiedBy: resolved.classifiedBy,
1309
- confidenceTier: resolved.tier,
1310
- reasoning: resolved.reasoning,
1311
- ...overrides
1312
- };
1313
- }
1314
- async function enrichCandidatesWithFieldCounts(meta) {
1315
- try {
1316
- const cols = await getCollections();
1317
- const reqBySlug = new Map(
1318
- cols.map((c) => [c.slug, (c.fields ?? []).filter((f) => f.required === true).length])
1319
- );
1320
- meta.candidates = meta.candidates.map((c) => ({
1321
- ...c,
1322
- requiredFieldCount: reqBySlug.get(c.collection) ?? 0
1323
- }));
1324
- } catch {
1325
- }
1326
- }
1327
- function emitClassificationTelemetry(resolved, workspaceId, opts) {
1328
- const topAlt = resolved.alternatives?.[0] ?? null;
1329
- trackCollectionClassified(workspaceId, {
1330
- collection_slug: resolved.collection,
1331
- thinking_layer: resolved.thinkingLayer ?? null,
1332
- confidence: resolved.confidence,
1333
- classified_by: resolved.classifiedBy,
1334
- confidence_tier: resolved.tier,
1335
- alternative_slug: topAlt?.collection ?? null,
1336
- alternative_confidence: topAlt?.confidence ?? null,
1337
- explicit_collection_provided: opts.explicitCollectionProvided,
1338
- auto_routed: opts.autoRouted
1339
- });
1340
- }
1341
- async function resolveCaptureCollection(params) {
1342
- const {
1343
- collection,
1344
- name,
1345
- description,
1346
- workspaceId,
1347
- explicitCollectionProvided
1348
- } = params;
1349
- if (collection) {
1350
- const resolved2 = await resolveCollection({ name, description });
1351
- if (resolved2) {
1352
- const agrees = resolved2.collection === collection;
1353
- const classifierMeta2 = buildClassifierMeta(resolved2, {
1354
- autoRouted: false,
1355
- agrees,
1356
- agentProvidedCollection: collection,
1357
- ...!agrees && {
1358
- overrideCommand: `capture collection="${resolved2.collection}"`
1359
- }
1360
- });
1361
- await enrichCandidatesWithFieldCounts(classifierMeta2);
1362
- emitClassificationTelemetry(resolved2, workspaceId, {
1363
- explicitCollectionProvided: true,
1364
- autoRouted: false
1365
- });
1366
- if (!agrees) {
1367
- trackClassifierTelemetry({
1368
- workspaceId,
1369
- predictedCollection: resolved2.collection,
1370
- confidence: resolved2.confidence,
1371
- autoRouted: false,
1372
- reasonCategory: "low-confidence",
1373
- explicitCollectionProvided: true,
1374
- outcome: "fallback"
1375
- });
1376
- const agentInCandidates = resolved2.alternatives.some((a) => a.collection === collection) || resolved2.collection === collection;
1377
- trackClassifierDivergence(workspaceId, {
1378
- classifier_collection: resolved2.collection,
1379
- agent_collection: collection,
1380
- classifier_confidence: resolved2.confidence,
1381
- classifier_tier: resolved2.tier,
1382
- agent_in_candidates: agentInCandidates
1383
- });
1384
- }
1385
- return { resolvedCollection: collection, classifierMeta: classifierMeta2 };
1386
- }
1387
- return {
1388
- resolvedCollection: collection,
1389
- classifierMeta: {
1390
- enabled: true,
1391
- autoRouted: false,
1392
- agrees: false,
1393
- abstained: true,
1394
- topConfidence: 0,
1395
- confidence: 0,
1396
- reasons: [],
1397
- candidates: [],
1398
- agentProvidedCollection: collection
1399
- }
1400
- };
1401
- }
1402
- const resolved = await resolveCollection({ name, description });
1403
- if (!resolved) {
1404
- trackClassifierTelemetry({
1405
- workspaceId,
1406
- predictedCollection: "unknown",
1407
- confidence: 0,
1408
- autoRouted: false,
1409
- reasonCategory: "low-confidence",
1410
- explicitCollectionProvided,
1411
- outcome: "fallback"
1412
- });
1413
- return { earlyResult: buildClassifierUnknownResult() };
1414
- }
1415
- const autoRoute = resolved.tier !== "low";
1416
- const classifierMeta = buildClassifierMeta(resolved, { autoRouted: autoRoute });
1417
- await enrichCandidatesWithFieldCounts(classifierMeta);
1418
- emitClassificationTelemetry(resolved, workspaceId, {
1419
- explicitCollectionProvided,
1420
- autoRouted: autoRoute
1421
- });
1422
- if (!autoRoute) {
1423
- trackClassifierTelemetry({
1424
- workspaceId,
1425
- predictedCollection: resolved.collection,
1426
- confidence: resolved.confidence,
1427
- autoRouted: false,
1428
- reasonCategory: "low-confidence",
1429
- explicitCollectionProvided,
1430
- outcome: "fallback"
1431
- });
1432
- return {
1433
- classifierMeta,
1434
- earlyResult: buildLowConfidenceResult(resolved, classifierMeta)
1435
- };
1436
- }
1437
- trackClassifierTelemetry({
1438
- workspaceId,
1439
- predictedCollection: resolved.collection,
1440
- confidence: resolved.confidence,
1441
- autoRouted: true,
1442
- reasonCategory: "auto-routed",
1443
- explicitCollectionProvided,
1444
- outcome: "auto-routed"
1445
- });
1446
- return {
1447
- resolvedCollection: resolved.collection,
1448
- classifierMeta
1449
- };
1450
- }
1451
960
  var captureSuccessOutputSchema = z4.object({
1452
961
  entryId: z4.string(),
1453
962
  collection: z4.string(),
@@ -1459,6 +968,7 @@ var captureSuccessOutputSchema = z4.object({
1459
968
  groundingReport: captureSinglePreviewOutputSchema.shape.groundingReport.optional(),
1460
969
  classifier: captureClassifierSchema.optional(),
1461
970
  studioUrl: z4.string().optional(),
971
+ // never set since TASK-964 (the chains-only Cortex link left with the retired drawer); kept, never narrowed (DEC-1831)
1462
972
  warnings: z4.array(z4.string()).optional(),
1463
973
  normalization: z4.object({
1464
974
  remapped: z4.record(z4.string()).optional(),
@@ -1478,24 +988,20 @@ var captureSuccessOutputSchema = z4.object({
1478
988
  // TASK-897: the kernel's outcome per link, on a create and on a replay.
1479
989
  links: z4.array(z4.object({ toEntryId: z4.string(), type: z4.string(), outcome: z4.enum(CAPTURE_LINK_OUTCOMES), reason: z4.string().optional(), line: z4.string() }).passthrough()).optional(),
1480
990
  fallbackApplied: z4.boolean().optional()
1481
- // round 2 (PRRT_kwDORS4ils6cTNbF): classifier field above still names the original (pre-substitution) prediction
1482
- }).strict();
1483
- var captureClassifierOnlyOutputSchema = z4.object({
1484
- classifier: captureClassifierSchema
991
+ // never set since TASK-964 (the kernel classifies among live collections only); kept, never narrowed (DEC-1831)
1485
992
  }).strict();
1486
993
  var captureOutputSchema = z4.union([
1487
994
  captureSuccessOutputSchema,
1488
- captureSinglePreviewOutputSchema,
1489
- captureClassifierOnlyOutputSchema
995
+ captureSinglePreviewOutputSchema
1490
996
  ]);
1491
997
  var CAPTURE_ACTIONS = ["capture"];
1492
998
  var SUGGEST_ONLY_REMOVED = "suggestOnly was removed: call capture with preview: true to check a capture without writing.";
1493
999
  var captureCompoundSchema = z4.object({
1494
1000
  action: z4.enum(CAPTURE_ACTIONS).optional().default("capture").describe("Create a single knowledge entry."),
1495
- collection: z4.string().max(200).optional().describe("Collection slug, e.g. 'tensions', 'rules', 'glossary', 'decisions'. Optional \u2014 classifier auto-routes when omitted."),
1001
+ collection: z4.string().max(200).optional().describe("Collection slug, e.g. 'tensions', 'rules', 'glossary', 'decisions'. Optional \u2014 the kernel classifies when omitted."),
1496
1002
  name: z4.string().max(ENTRY_NAME_MAX_CHARS).optional().describe("Display name \u2014 required."),
1497
1003
  description: z4.string().max(ENTRY_BODY_MAX_CHARS).optional().describe("Full context, in Markdown \u2014 required."),
1498
- context: z4.string().max(5e3).optional().describe("Optional additional context."),
1004
+ context: z4.string().max(5e3).optional().describe("Optional additional context; appended to the description, so it is stored only when the description is (not when data carries the prose field)."),
1499
1005
  canonicalKey: z4.string().max(200).optional().describe("Semantic type (e.g. 'decision', 'tension', 'vision'). Auto-assigned from collection if omitted."),
1500
1006
  data: z4.record(z4.unknown()).optional().describe("Explicit field values; user-provided wins over inferred."),
1501
1007
  why: z4.string().max(ENTRY_BODY_MAX_CHARS).optional().describe("Why this entry exists."),
@@ -1532,14 +1038,7 @@ function registerSmartCaptureTools(server) {
1532
1038
  "capture",
1533
1039
  {
1534
1040
  title: "Capture",
1535
- description: `The tool for creating a single knowledge entry. Creates an entry and returns a quality scorecard \u2014 all in one call. Provide a name and description; \`collection\` is optional \u2014 the classifier auto-routes when omitted. An agent capturing several atoms from one text calls this once per atom.
1536
-
1537
- Supported collections with smart profiles: ${SMART_PROFILE_COLLECTIONS.join(", ")}.
1538
- All other collections get an ENT-{random} ID and sensible defaults.
1539
-
1540
- **Explicit data:** When you know the schema, pass \`data: { field: value }\` to set fields directly. Top-level \`name\`/\`description\` win for those fields; \`data\` wins over inference for the rest. Governance entries also require a top-level \`why\`.
1541
-
1542
- **Compound capture:** Pass \`links\` to create relations in the same call; a capture writes no other relations. A capture lands as a \`draft\` to accept with \`entries action=commit\`; a question is born open. The reply names possible duplicates; only an exact name in the collection is refused.`,
1041
+ description: "The tool for creating a single knowledge entry. Creates an entry and returns a quality scorecard \u2014 all in one call. Provide a name and description; `collection` is optional \u2014 the kernel classifies when omitted and files a low-confidence pick as a draft with one line saying so. An agent capturing several atoms from one text calls this once per atom.\n\n**Explicit data:** When you know the schema, pass `data: { field: value }` to set fields directly. A value in `data` is kept as sent; `description` is the entry's text and fills the collection's prose field only when `data` leaves it empty (the same rule as `pb capture`); when `data` carries the prose field, `description` and `context` are used for classification and judging only and are not stored. Governance entries also require a top-level `why`.\n\n**Compound capture:** Pass `links` to create relations in the same call; a capture writes no other relations. A capture lands as a `draft` to accept with `entries action=commit`; a question is born open. The reply names possible duplicates; only an exact name in the collection is refused.",
1543
1042
  inputSchema: captureCompoundSchema,
1544
1043
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
1545
1044
  },
@@ -1554,129 +1053,46 @@ All other collections get an ENT-{random} ID and sensible defaults.
1554
1053
  })
1555
1054
  );
1556
1055
  }
1557
- async function handleCapture(server, { collection, name, description, context, canonicalKey, data: userData, why, links, sourceRef, sourceExcerpt, preview, ownerRoleEntryId, team }) {
1056
+ function classifierFromReply(classification) {
1057
+ return {
1058
+ enabled: true,
1059
+ autoRouted: true,
1060
+ agrees: true,
1061
+ abstained: false,
1062
+ topConfidence: classification.confidence,
1063
+ confidence: classification.confidence,
1064
+ reasons: classification.reasoning ? [classification.reasoning] : [],
1065
+ candidates: [
1066
+ { collection: classification.collection, confidence: classification.confidence },
1067
+ ...classification.alternatives.map((a) => ({ collection: a.collection, confidence: a.confidence }))
1068
+ ].slice(0, 3),
1069
+ classifiedBy: classification.classifiedBy,
1070
+ confidenceTier: classification.tier,
1071
+ reasoning: classification.reasoning
1072
+ };
1073
+ }
1074
+ async function handleCapture(_server, { collection, name, description, context, canonicalKey, data: userData, why, links, sourceRef, sourceExcerpt, preview, ownerRoleEntryId, team }) {
1558
1075
  requireWriteAccess();
1559
1076
  const timingStart = Date.now();
1560
1077
  const wsCtx = await getWorkspaceContext();
1561
- const explicitCollectionProvided = typeof collection === "string" && collection.trim().length > 0;
1562
- const resolution = await resolveCaptureCollection({
1563
- collection,
1564
- name,
1565
- description,
1566
- workspaceId: wsCtx.workspaceId,
1567
- explicitCollectionProvided
1568
- });
1569
- if (resolution.earlyResult) {
1570
- return resolution.earlyResult;
1571
- }
1572
- let resolvedCollection = resolution.resolvedCollection;
1573
- const classifierMeta = resolution.classifierMeta;
1574
- if (!resolvedCollection) {
1575
- return buildClassifierUnknownResult();
1576
- }
1577
- let canonicalKeyAutoInjected = false;
1578
- if (resolvedCollection === "chains" && !canonicalKey && !explicitCollectionProvided && classifierMeta?.candidates?.some((c) => c.collection === "work-packages")) {
1579
- canonicalKey = "work_package";
1580
- canonicalKeyAutoInjected = true;
1078
+ const collectionSlug = typeof collection === "string" && collection.trim().length > 0 ? collection.trim() : void 0;
1079
+ const text = typeof context === "string" && context.trim().length > 0 ? `${description}
1080
+
1081
+ ${context.trim()}` : description;
1082
+ if (text.length > ENTRY_BODY_MAX_CHARS) {
1083
+ const msg = `description plus context is ${text.length} characters; the entry text is capped at ${ENTRY_BODY_MAX_CHARS}. Shorten one of them.`;
1084
+ return { content: [{ type: "text", text: msg }], structuredContent: failure("VALIDATION_ERROR", msg, "Shorten description or context.") };
1581
1085
  }
1582
1086
  const tAfterClassify = Date.now();
1583
- let fallbackApplied = false;
1584
- let col = await kernelQuery("chain.getCollection", { slug: resolvedCollection });
1585
- if (!col && !explicitCollectionProvided) {
1586
- const allCollections = await getCollections();
1587
- const fallback = allCollections.find((c) => c.slug === "insights") ?? allCollections.find((c) => c.stratum === "system");
1588
- if (fallback && canonicalKey === "work_package" && !canonicalKeyAutoInjected && fallback.slug !== "chains") {
1589
- const msg = `\`work_package\` entries must be in the \`chains\` collection, not \`${fallback.slug}\`. \`${resolvedCollection}\` doesn't exist here, so there's no chains-equivalent drawer to fall back into.`;
1590
- return { content: [{ type: "text", text: msg }], structuredContent: failure("VALIDATION_FAILED", `canonicalKey 'work_package' requires 'chains'; fallback target '${fallback.slug}' doesn't qualify.`, msg) };
1591
- }
1592
- if (fallback) {
1593
- resolvedCollection = fallback.slug;
1594
- col = fallback;
1595
- fallbackApplied = true;
1596
- if (canonicalKeyAutoInjected) canonicalKey = void 0;
1597
- }
1598
- }
1599
- if (!col) {
1600
- const msg = `Collection \`${resolvedCollection}\` not found. Creating a collection is not self-serve \u2014 ask a platform admin to provision it, or enable the module ontology pack that owns it. Use \`collections action=list\` to see what exists.`;
1601
- return {
1602
- content: [{ type: "text", text: msg }],
1603
- structuredContent: failure(
1604
- "NOT_FOUND",
1605
- `Collection '${resolvedCollection}' not found.`,
1606
- msg,
1607
- [{ tool: "collections", description: "List collections", parameters: { action: "list" } }]
1608
- )
1609
- };
1610
- }
1611
- const profile = await getProfile(resolvedCollection, col.fields);
1612
1087
  const groundingReport = { scanTruncated: null, duplicates: [] };
1613
1088
  let groundingCaveat = "";
1614
- const built = buildDataFromFieldsOrRefuse(col.fields ?? [], profile.descriptionField, description, resolvedCollection);
1615
- if ("refusal" in built) return built.refusal;
1616
- const { data, descriptionField } = built;
1617
- for (const def of profile.defaults) {
1618
- if (def.value !== "infer" && def.value !== "today") {
1619
- data[def.key] = def.value;
1620
- }
1621
- }
1622
- if (profile.inferField) {
1623
- const inferred = profile.inferField({
1624
- collection: resolvedCollection,
1625
- name,
1626
- description,
1627
- context,
1628
- data,
1629
- entryId: "",
1630
- collectionFields: col.fields ?? []
1631
- });
1632
- for (const [key, val] of Object.entries(inferred)) {
1633
- if (val !== void 0 && val !== "") {
1634
- data[key] = val;
1635
- }
1636
- }
1637
- }
1638
- if (isEmptyValue(data.date)) {
1639
- const inferredSourceDate = inferSourceDate(name, description, context, sourceRef, sourceExcerpt);
1640
- if (inferredSourceDate && (col.fields ?? []).some((field) => field.key === "date")) {
1641
- data.date = inferredSourceDate;
1642
- }
1643
- }
1644
- if (userData && typeof userData === "object") Object.assign(data, userData);
1645
- data[descriptionField] = description;
1646
- const colMeta = await getCollectionBySlug(resolvedCollection);
1647
- const isWorkPackageCapture = canonicalKey === "work_package" || colMeta?.defaultCanonicalKey === "work_package";
1648
- let entryWarnings = [];
1649
- const isStrategyCapture = colMeta?.thinkingLayer === "strategy";
1650
- const shouldDecompose = (isStrategyCapture || isWorkPackageCapture) && description.trim().length > 0;
1651
- if (shouldDecompose) {
1652
- try {
1653
- const decomposed = await kernelCall(
1654
- "chain.decomposeContent",
1655
- {
1656
- collectionSlug: resolvedCollection,
1657
- entryName: name,
1658
- description,
1659
- existingData: data,
1660
- ...isWorkPackageCapture ? { canonicalKey: "work_package" } : {},
1661
- ...colMeta?.thinkingLayer ? { thinkingLayer: colMeta.thinkingLayer } : {}
1662
- }
1663
- );
1664
- if (decomposed?.decomposed) {
1665
- for (const [key, val] of Object.entries(decomposed.decomposed)) {
1666
- if (isEmptyValue(data[key]) && isNonEmptyValue(val)) {
1667
- data[key] = val;
1668
- }
1669
- }
1670
- }
1671
- } catch {
1672
- entryWarnings.push("Content decomposition skipped (transient error). Entry saved with description only.");
1673
- }
1674
- }
1675
- canonicalizeSelects(data, col.fields ?? [], isWorkPackageCapture);
1676
1089
  const agentId = getAgentSessionId();
1677
1090
  let finalEntryId;
1678
- let internalId;
1091
+ let landedCollection;
1092
+ let replyFields = [];
1093
+ let classifierMeta;
1679
1094
  let normalizationMeta;
1095
+ const entryWarnings = [];
1680
1096
  let formativeHints = [];
1681
1097
  let coachingFromCreate;
1682
1098
  let wasAutoCommittedServerSide = false;
@@ -1684,11 +1100,16 @@ async function handleCapture(server, { collection, name, description, context, c
1684
1100
  let linkOutcomes = [];
1685
1101
  try {
1686
1102
  const result2 = await kernelMutation("chain.createEntry", {
1687
- collectionSlug: resolvedCollection,
1103
+ // TASK-964: the slug only when the caller named one; otherwise the kernel classifies (entries.ts, the auto branch).
1104
+ ...collectionSlug ? { collectionSlug } : {},
1688
1105
  name,
1106
+ description: text,
1107
+ // the classifier's, the judge's and the shaper's text; the kernel files it in the collection's prose field when that is empty
1689
1108
  // DEC-896 / BR-76: omit status — server resolves from governanceMode (Open→active, else draft).
1690
- data,
1691
- canonicalKey,
1109
+ data: userData ?? {},
1110
+ // as sent (WP-574/TEN-2916: no key is filtered); the kernel shapes it (DEC-2172)
1111
+ ...canonicalKey ? { canonicalKey } : {},
1112
+ // the type hint (entries.ts: typeHint ?? canonicalKey)
1692
1113
  ...typeof why === "string" && why.trim() ? { why } : {},
1693
1114
  // TASK-894: the kernel files it under the collection's why field
1694
1115
  createdBy: agentId ? `agent:${agentId}` : "capture",
@@ -1706,22 +1127,20 @@ async function handleCapture(server, { collection, name, description, context, c
1706
1127
  if (result2 && "replayed" in result2) return resumeReplayedCapture(result2, name);
1707
1128
  if (result2 && isPreviewCreateEntryResult(result2)) {
1708
1129
  const preflight = await resolvePreviewPreflight(result2, links);
1130
+ const previewCollection = resolveEffectiveCollection(result2, preview, collectionSlug ?? "");
1709
1131
  const dupCount = groundingReport.duplicates.length;
1710
1132
  const groundingSummary = dupCount > 0 ? `
1711
1133
 
1712
1134
  **Possible duplicates:** ${dupCount}.` : "";
1713
1135
  const wouldBlock = result2.wouldBlock === true;
1714
- const headline = wouldBlock ? `Preview: would be BLOCKED \u2014 "${name}" [${resolvedCollection}]` : `Preview: would capture "${name}" [${resolvedCollection}]`;
1136
+ const headline = wouldBlock ? `Preview: would be BLOCKED \u2014 "${name}" [${previewCollection}]` : `Preview: would capture "${name}" [${previewCollection}]`;
1715
1137
  const previewEnvelope = success(
1716
1138
  wouldBlock ? `Preview: would be BLOCKED \u2014 "${name}" \u2014 no DB writes` : `Preview: would capture "${name}" \u2014 no DB writes`,
1717
1139
  {
1718
1140
  entryId: result2.entryId,
1719
1141
  preflight,
1720
1142
  name,
1721
- // P0 round 4 (fix 1/3, root): see resolveEffectiveCollection's doc comment
1722
- // (lib/createEntryPreviewResult.ts) for why this must be the server's EFFECTIVE
1723
- // post-routing collection, never the caller's pre-routing `resolvedCollection`.
1724
- collection: resolveEffectiveCollection(result2, preview, resolvedCollection),
1143
+ collection: previewCollection,
1725
1144
  outcome: wouldBlock ? "blocked" : "preview",
1726
1145
  ...wouldBlock && result2.blockingReason ? { blockingReason: result2.blockingReason } : {},
1727
1146
  warnings: result2.warnings ?? [],
@@ -1735,7 +1154,7 @@ async function handleCapture(server, { collection, name, description, context, c
1735
1154
  // this closes (a bare {collection,name,description} dropped `data`/`links`/etc.,
1736
1155
  // so a required WHY supplied via `data` silently vanished on retry).
1737
1156
  buildCaptureForRealNextAction({
1738
- collection: resolvedCollection,
1157
+ collection: previewCollection,
1739
1158
  name,
1740
1159
  description,
1741
1160
  context,
@@ -1767,14 +1186,15 @@ No DB writes \u2014 call without \`preview:true\` to capture for real.${result2.
1767
1186
  };
1768
1187
  }
1769
1188
  const created = result2;
1770
- internalId = created.docId;
1771
1189
  finalEntryId = created.entryId;
1190
+ landedCollection = created.collection ?? collectionSlug ?? "";
1191
+ replyFields = created.fields ?? [];
1192
+ classifierMeta = created.classification ? classifierFromReply(created.classification) : void 0;
1772
1193
  entryWarnings.push(...created.warnings ?? []);
1773
1194
  normalizationMeta = created.normalization;
1774
1195
  formativeHints = created.qualityHints ?? [];
1775
1196
  coachingFromCreate = created.coaching;
1776
1197
  linkOutcomes = created.links ?? [];
1777
- resolveGapsForEntry(name, created.entryId);
1778
1198
  const createdStatus = created.status;
1779
1199
  if (createdStatus !== void 0 && isTrustAccepted(createdStatus)) wasAutoCommittedServerSide = true;
1780
1200
  spineFloor = created.spineFloor;
@@ -1817,12 +1237,11 @@ Use \`context action=get\` to inspect the existing entry, or \`entries action=up
1817
1237
  }
1818
1238
  if (verdictResult?.verdict) {
1819
1239
  try {
1820
- const wsForTracking = await getWorkspaceContext();
1821
1240
  const v = verdictResult.verdict;
1822
1241
  const failedCount = (v.criteria ?? []).filter((c) => !c.passed).length;
1823
- trackQualityVerdict(wsForTracking.workspaceId, {
1242
+ trackQualityVerdict(wsCtx.workspaceId, {
1824
1243
  entry_id: finalEntryId,
1825
- entry_type: v.canonicalKey ?? resolvedCollection,
1244
+ entry_type: v.canonicalKey ?? landedCollection,
1826
1245
  tier: v.tier,
1827
1246
  context: "capture",
1828
1247
  passed: v.passed,
@@ -1838,50 +1257,26 @@ Use \`context action=get\` to inspect the existing entry, or \`entries action=up
1838
1257
  let finalStatus = "draft";
1839
1258
  if (wasAutoCommittedServerSide) {
1840
1259
  finalStatus = "committed";
1841
- await recordSessionActivity({ entryModified: internalId });
1842
1260
  trackChainEntryCommitted(wsCtx.workspaceId, {
1843
1261
  entry_id: finalEntryId,
1844
- collection: resolvedCollection,
1262
+ collection: landedCollection,
1845
1263
  commit_method: "auto",
1846
1264
  surface: "mcp_capture"
1847
1265
  });
1848
1266
  }
1849
1267
  const lines = [
1850
1268
  `# Captured: ${finalEntryId || name}`,
1851
- `**${name}** added to \`${resolvedCollection}\` as \`${finalStatus}\``,
1269
+ `**${name}** added to \`${landedCollection}\` as \`${finalStatus}\``,
1852
1270
  `**Workspace:** ${wsCtx.workspaceSlug} (${wsCtx.workspaceId})`
1853
1271
  ];
1854
1272
  if (classifierMeta) {
1855
1273
  lines.push("");
1856
1274
  lines.push("## Classification");
1857
- if (fallbackApplied) {
1858
- lines.push(`Filed into \`${resolvedCollection}\` \u2014 the classifier's original pick was unavailable; routed here as a fallback, not a classifier decision.`);
1859
- } else if (classifierMeta.abstained) {
1860
- lines.push(`No classifier coverage for \`${resolvedCollection}\` \u2014 entry accepted as-is.`);
1861
- } else if (classifierMeta.autoRouted) {
1862
- lines.push(`Auto-routed to \`${resolvedCollection}\` (${classifierMeta.topConfidence}% confidence).`);
1863
- if (classifierMeta.reasons.length > 0) {
1864
- lines.push(`Reason: ${classifierMeta.reasons.join("; ")}.`);
1865
- }
1866
- lines.push(`Override: rerun with explicit \`collection\` if wrong.`);
1867
- } else if (classifierMeta.agrees) {
1868
- lines.push(`Classifier confirms \`${resolvedCollection}\` (${classifierMeta.topConfidence}% confidence).`);
1869
- } else {
1870
- const suggested = classifierMeta.candidates[0]?.collection ?? "unknown";
1871
- lines.push(`Classifier suggests \`${suggested}\` (${classifierMeta.topConfidence}% confidence) \u2014 review classification.`);
1872
- if (classifierMeta.reasons.length > 0) {
1873
- lines.push(`Reason: ${classifierMeta.reasons.join("; ")}.`);
1874
- }
1875
- if (classifierMeta.overrideCommand) {
1876
- lines.push(`Override: \`${classifierMeta.overrideCommand}\``);
1877
- }
1275
+ lines.push(`Auto-routed to \`${landedCollection}\` (${classifierMeta.topConfidence}% confidence, ${classifierMeta.classifiedBy ?? "classifier"}).`);
1276
+ if (classifierMeta.reasons.length > 0) {
1277
+ lines.push(`Reason: ${classifierMeta.reasons.join("; ")}.`);
1878
1278
  }
1879
1279
  }
1880
- const studioUrl = resolvedCollection === "chains" ? `${appUrl()}/w/${wsCtx.workspaceSlug}/chain?entry=${encodeURIComponent(internalId)}&railTab=context` : void 0;
1881
- if (studioUrl) {
1882
- lines.push("");
1883
- lines.push(`**View in Cortex:** ${studioUrl}`);
1884
- }
1885
1280
  if (linkOutcomes.length > 0) {
1886
1281
  lines.push("");
1887
1282
  lines.push("## Requested links");
@@ -1891,9 +1286,6 @@ Use \`context action=get\` to inspect the existing entry, or \`entries action=up
1891
1286
  lines.push("");
1892
1287
  lines.push(`## Accepted: ${finalEntryId}`);
1893
1288
  lines.push(`**${name}** added to your knowledge base.`);
1894
- if (await isGoverned(resolvedCollection)) {
1895
- lines.push(`_Note: \`${resolvedCollection}\` is a governed collection \u2014 ensure this entry has been reviewed._`);
1896
- }
1897
1289
  } else if (spineFloor) {
1898
1290
  lines.push("");
1899
1291
  lines.push("## Saved as draft");
@@ -1913,28 +1305,11 @@ Use \`context action=get\` to inspect the existing entry, or \`entries action=up
1913
1305
  }
1914
1306
  if (formativeHints.length > 0) {
1915
1307
  trackCaptureQualityHints(wsCtx.workspaceId, {
1916
- collection: resolvedCollection,
1308
+ collection: landedCollection,
1917
1309
  hint_count: formativeHints.length,
1918
1310
  hint_fields: formativeHints.map((h) => h.field)
1919
1311
  });
1920
1312
  }
1921
- const isWorkPackageOrBetCapture = isWorkPackageCapture || canonicalKey === "bet";
1922
- const isBetOrGoal = isWorkPackageOrBetCapture || canonicalKey === "goal";
1923
- let hasStrategyLink = false;
1924
- if (isBetOrGoal) {
1925
- const written = linkOutcomes.filter((l) => l.outcome === "written");
1926
- if (written.length > 0) {
1927
- const slugByPrefix = new Map((await getCollections()).filter((c) => c.idPrefix).map((c) => [c.idPrefix, c.slug]));
1928
- hasStrategyLink = written.some((l) => slugByPrefix.get(l.toEntryId.split("-")[0]) === "strategy");
1929
- }
1930
- }
1931
- if (isBetOrGoal && !hasStrategyLink) {
1932
- lines.push("");
1933
- lines.push(
1934
- `**Strategy link:** This ${isWorkPackageOrBetCapture ? "work package" : "goal"} doesn't connect to any strategy entry. Consider linking before accepting. Find one with \`context action=search\` and link it with \`relations action=create\`.`
1935
- );
1936
- await recordSessionActivity({ strategyLinkWarnedForEntryId: internalId });
1937
- }
1938
1313
  if (entryWarnings.length > 0) {
1939
1314
  lines.push("");
1940
1315
  lines.push("## Validation Warnings");
@@ -1964,20 +1339,6 @@ Use \`context action=get\` to inspect the existing entry, or \`entries action=up
1964
1339
  lines.push(`${linkOutcomes.length === 0 ? "3" : "2"}. **Improve quality:** \`entries action=update entryId="${eid}"\` \u2014 fill missing fields`);
1965
1340
  }
1966
1341
  }
1967
- try {
1968
- const readiness = await kernelQuery("chain.workspaceReadiness");
1969
- if (readiness?.sampleTruncated) {
1970
- lines.push("", `## Workspace Readiness (provisional sample)`, READINESS_PROVISIONAL_LINE);
1971
- } else if (readiness && readiness.gaps && readiness.gaps.length > 0) {
1972
- const topGaps = readiness.gaps.slice(0, 2);
1973
- lines.push("");
1974
- lines.push(`## Workspace Readiness`);
1975
- for (const gap of topGaps) {
1976
- lines.push(`- _${gap.label}:_ ${gap.guidance}`);
1977
- }
1978
- }
1979
- } catch {
1980
- }
1981
1342
  const next = [];
1982
1343
  if (finalStatus === "committed") {
1983
1344
  next.push({ tool: "context", description: "See its connections", parameters: { action: "find", entryId: finalEntryId } });
@@ -1986,19 +1347,11 @@ Use \`context action=get\` to inspect the existing entry, or \`entries action=up
1986
1347
  next.push({ tool: "entries", description: "Accept onto Chain", parameters: { action: "commit", entryId: finalEntryId } });
1987
1348
  }
1988
1349
  const qualityPhrase = verdictQualityPhrase(verdictResult);
1989
- const summary = finalStatus === "committed" ? `Captured and accepted ${finalEntryId} (${name}) onto ${resolvedCollection}. ${qualityPhrase}` : `Captured ${finalEntryId} (${name}) as draft in ${resolvedCollection}. ${qualityPhrase}`;
1990
- const expectedFields = (col.fields ?? []).map((f) => ({
1991
- key: f.key,
1992
- type: f.type,
1993
- ...f.required && { required: true }
1994
- }));
1350
+ const summary = finalStatus === "committed" ? `Captured and accepted ${finalEntryId} (${name}) onto ${landedCollection}. ${qualityPhrase}` : `Captured ${finalEntryId} (${name}) as draft in ${landedCollection}. ${qualityPhrase}`;
1351
+ const expectedFields = replyFields.map((f) => ({ key: f.key, type: f.type, ...f.required && { required: true } }));
1995
1352
  const captureContractForEnvelope = {
1996
- requiredFields: (col.fields ?? []).filter((f) => f.required === true).map((f) => f.key),
1997
- allFields: (col.fields ?? []).map((f) => ({
1998
- key: f.key,
1999
- required: f.required === true,
2000
- type: f.type
2001
- }))
1353
+ requiredFields: replyFields.filter((f) => f.required === true).map((f) => f.key),
1354
+ allFields: replyFields.map((f) => ({ key: f.key, required: f.required === true, type: f.type }))
2002
1355
  };
2003
1356
  const totalMs = Date.now() - timingStart;
2004
1357
  const timing = {
@@ -2014,15 +1367,13 @@ Use \`context action=get\` to inspect the existing entry, or \`entries action=up
2014
1367
  summary,
2015
1368
  {
2016
1369
  entryId: finalEntryId,
2017
- collection: resolvedCollection,
1370
+ collection: landedCollection,
2018
1371
  name,
2019
1372
  status: finalStatus,
2020
1373
  // WP-480 S1: the server verdict rides `qualityVerdict` (named breaking
2021
1374
  // output-contract change, pre-launch).
2022
1375
  qualityVerdict: verdictResult?.verdict ? { ...verdictResult.verdict, source: verdictResult.source ?? "heuristic" } : void 0,
2023
1376
  ...classifierMeta && { classifier: classifierMeta },
2024
- ...fallbackApplied && { fallbackApplied: true },
2025
- ...studioUrl && { studioUrl },
2026
1377
  ...entryWarnings.length > 0 && { warnings: entryWarnings },
2027
1378
  ...normalizationMeta && (Object.keys(normalizationMeta.remapped).length > 0 || normalizationMeta.rejected.length > 0) && {
2028
1379
  normalization: {
@@ -4013,6 +3364,9 @@ async function handleDelete(from, to, type) {
4013
3364
  // src/tools/context.ts
4014
3365
  import { z as z18 } from "zod/v3";
4015
3366
 
3367
+ // src/lib/readinessNotices.ts
3368
+ var READINESS_PROVISIONAL_LINE = "Readiness is provisional: its sample omitted accepted entries. Counts are lower bounds, apparent gaps may already be covered.";
3369
+
4016
3370
  // src/tools/documents.ts
4017
3371
  import { z as z13 } from "zod/v3";
4018
3372
  var DOCUMENTS_ACTIONS = ["get-last-verified-brief"];
@@ -5819,6 +5173,58 @@ function formatTimeAgo(ms) {
5819
5173
  // src/tools/collections.ts
5820
5174
  import { z as z20 } from "zod/v3";
5821
5175
 
5176
+ // src/lib/scopedCache.ts
5177
+ var ScopedCache = class {
5178
+ constructor(ttlMs, maxScopes = 100) {
5179
+ this.ttlMs = ttlMs;
5180
+ this.maxScopes = maxScopes;
5181
+ }
5182
+ store = /* @__PURE__ */ new Map();
5183
+ get(scope) {
5184
+ const e = this.store.get(scope);
5185
+ if (!e) return void 0;
5186
+ if (Date.now() > e.expiresAt) {
5187
+ this.store.delete(scope);
5188
+ return void 0;
5189
+ }
5190
+ return e.value;
5191
+ }
5192
+ set(scope, value) {
5193
+ this.store.set(scope, { value, expiresAt: Date.now() + this.ttlMs });
5194
+ if (this.store.size > this.maxScopes) this.evict();
5195
+ }
5196
+ delete(scope) {
5197
+ this.store.delete(scope);
5198
+ }
5199
+ clear() {
5200
+ this.store.clear();
5201
+ }
5202
+ size() {
5203
+ return this.store.size;
5204
+ }
5205
+ /** Drop expired entries first; if still over budget, drop the oldest-expiring. */
5206
+ evict() {
5207
+ const now = Date.now();
5208
+ for (const [k, e] of this.store) {
5209
+ if (now > e.expiresAt) this.store.delete(k);
5210
+ }
5211
+ if (this.store.size > this.maxScopes) {
5212
+ const sorted = [...this.store.entries()].sort((a, b) => a[1].expiresAt - b[1].expiresAt);
5213
+ const overBy = sorted.length - this.maxScopes;
5214
+ for (let i = 0; i < overBy; i++) {
5215
+ this.store.delete(sorted[i][0]);
5216
+ }
5217
+ }
5218
+ }
5219
+ };
5220
+ var COLLECTION_CACHE_TTL_MS = 6e4;
5221
+
5222
+ // src/lib/collectionCache.ts
5223
+ var collectionCache = new ScopedCache(COLLECTION_CACHE_TTL_MS);
5224
+ function invalidateCollectionCacheForScope() {
5225
+ collectionCache.delete(cacheScope());
5226
+ }
5227
+
5822
5228
  // src/lib/bodyTemplate.ts
5823
5229
  function readBodyTemplateGuidance(bodyTemplate) {
5824
5230
  if (!bodyTemplate || bodyTemplate.length === 0) return void 0;
@@ -6285,7 +5691,6 @@ async function handleUpdate(slug, name, description, purpose, icon, navGroup, fi
6285
5691
  ...metaFields(meta)
6286
5692
  });
6287
5693
  invalidateCollectionCacheForScope();
6288
- invalidateProfileCacheForScope();
6289
5694
  const changes = [name && "name", description && "description", purpose && "purpose", icon && "icon", navGroup && "navGroup", fields && "fields", meta.axis !== void 0 && "axis", meta.stageLabelMap !== void 0 && "stageLabelMap", meta.defaultCanonicalKey !== void 0 && "defaultCanonicalKey", meta.defaultWorkflowStatus !== void 0 && "defaultWorkflowStatus", meta.validWorkflowStatuses !== void 0 && "validWorkflowStatuses", meta.classificationCheck !== void 0 && "classificationCheck", meta.classificationPriority !== void 0 && "classificationPriority", meta.qualityCriteria !== void 0 && "qualityCriteria", meta.statusList !== void 0 && "statusList"].filter(Boolean).join(", ");
6290
5695
  return {
6291
5696
  content: [{
@@ -7923,6 +7328,20 @@ async function _handleOrient({ mode: mode2 = "full", tier, task, startupSignals,
7923
7328
  // src/tools/workflows.ts
7924
7329
  import { z as z22 } from "zod/v3";
7925
7330
 
7331
+ // src/lib/resolveCollection.ts
7332
+ async function resolveCollection(params) {
7333
+ const { name, description, typeHint, allowReviewRouting } = params;
7334
+ const sessionId = getAgentSessionId();
7335
+ const result2 = await kernelCall("chain.resolveCollection", {
7336
+ entryName: name,
7337
+ entryDescription: description,
7338
+ ...typeHint ? { typeHint } : {},
7339
+ ...allowReviewRouting ? { allowReviewRouting } : {},
7340
+ ...sessionId ? { agentSessionId: sessionId } : {}
7341
+ });
7342
+ return result2 ?? null;
7343
+ }
7344
+
7926
7345
  // src/workflows/descriptor.ts
7927
7346
  function cloneWorkflowQuestion(question) {
7928
7347
  return {
@@ -8867,7 +8286,7 @@ function validateFinalWorkflowCheckpoint(workflow, roundId) {
8867
8286
  }
8868
8287
  return null;
8869
8288
  }
8870
- function levenshtein2(a, b) {
8289
+ function levenshtein(a, b) {
8871
8290
  const m = a.length;
8872
8291
  const n = b.length;
8873
8292
  const dp = Array.from(
@@ -8888,9 +8307,9 @@ function findClosestRound(input, rounds) {
8888
8307
  );
8889
8308
  if (substringMatch) return substringMatch;
8890
8309
  let best = rounds[0];
8891
- let bestDist = levenshtein2(input, best.id);
8310
+ let bestDist = levenshtein(input, best.id);
8892
8311
  for (const r of rounds.slice(1)) {
8893
- const dist = levenshtein2(input, r.id);
8312
+ const dist = levenshtein(input, r.id);
8894
8313
  if (dist < bestDist) {
8895
8314
  bestDist = dist;
8896
8315
  best = r;
@@ -12836,4 +12255,4 @@ export {
12836
12255
  createProductBrainServer,
12837
12256
  initFeatureFlags
12838
12257
  };
12839
- //# sourceMappingURL=chunk-Y7YDQNWU.js.map
12258
+ //# sourceMappingURL=chunk-RMVLWNVO.js.map