@productbrain/mcp 0.0.1-beta.39 → 0.0.1-beta.40

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.
@@ -1,7 +1,10 @@
1
1
  import {
2
+ trackCaptureClassifierAutoRouted,
3
+ trackCaptureClassifierEvaluated,
4
+ trackCaptureClassifierFallback,
2
5
  trackQualityVerdict,
3
6
  trackToolCall
4
- } from "./chunk-TB24VJ4Z.js";
7
+ } from "./chunk-P7ABQEFK.js";
5
8
 
6
9
  // src/tools/smart-capture.ts
7
10
  import { z } from "zod";
@@ -27,6 +30,7 @@ function newKeyState() {
27
30
  workspaceSlug: null,
28
31
  workspaceName: null,
29
32
  workspaceCreatedAt: null,
33
+ workspaceGovernanceMode: null,
30
34
  agentSessionId: null,
31
35
  apiKeyId: null,
32
36
  apiKeyScope: "readwrite",
@@ -112,6 +116,7 @@ var _stdioState = {
112
116
  workspaceSlug: null,
113
117
  workspaceName: null,
114
118
  workspaceCreatedAt: null,
119
+ workspaceGovernanceMode: null,
115
120
  agentSessionId: null,
116
121
  apiKeyId: null,
117
122
  apiKeyScope: "readwrite",
@@ -328,6 +333,7 @@ async function resolveWorkspaceWithRetry(maxRetries = 2) {
328
333
  s.workspaceSlug = workspace.slug;
329
334
  s.workspaceName = workspace.name;
330
335
  s.workspaceCreatedAt = workspace.createdAt ?? null;
336
+ s.workspaceGovernanceMode = workspace.governanceMode ?? "open";
331
337
  if (workspace.keyScope) s.apiKeyScope = workspace.keyScope;
332
338
  if (workspace.keyId) s.apiKeyId = workspace.keyId;
333
339
  return s.workspaceId;
@@ -352,7 +358,8 @@ async function getWorkspaceContext() {
352
358
  workspaceId,
353
359
  workspaceSlug: s.workspaceSlug ?? "unknown",
354
360
  workspaceName: s.workspaceName ?? "unknown",
355
- createdAt: s.workspaceCreatedAt
361
+ createdAt: s.workspaceCreatedAt,
362
+ governanceMode: s.workspaceGovernanceMode ?? "open"
356
363
  };
357
364
  }
358
365
  async function mcpQuery(fn, args = {}) {
@@ -475,6 +482,189 @@ function initToolSurface(_server) {
475
482
  function trackWriteTool(_tool) {
476
483
  }
477
484
 
485
+ // src/featureFlags.ts
486
+ import { PostHog } from "posthog-node";
487
+ var client = null;
488
+ var LOCAL_OVERRIDES = {
489
+ // Uncomment for local dev without PostHog:
490
+ // "workspace-full-surface": false,
491
+ // "chainwork-enabled": true,
492
+ // "active-intelligence-shaping": true, // ENT-59: Opus-powered investigation during shaping
493
+ // "capture-without-thinking": true, // BET-73: collection-optional capture classifier
494
+ };
495
+ function initFeatureFlags(posthogClient) {
496
+ if (posthogClient) {
497
+ client = posthogClient;
498
+ return;
499
+ }
500
+ const apiKey = process.env.POSTHOG_MCP_KEY || process.env.PUBLIC_POSTHOG_KEY;
501
+ if (!apiKey) {
502
+ client = null;
503
+ return;
504
+ }
505
+ client = new PostHog(apiKey, {
506
+ host: process.env.PUBLIC_POSTHOG_HOST || "https://eu.i.posthog.com",
507
+ flushAt: 1,
508
+ flushInterval: 5e3,
509
+ featureFlagsPollingInterval: 3e4
510
+ });
511
+ }
512
+ async function isFeatureEnabled(flag, workspaceId, workspaceSlug) {
513
+ if (process.env.FEATURE_KILL_SWITCH === "true") return false;
514
+ if (flag in LOCAL_OVERRIDES) return LOCAL_OVERRIDES[flag];
515
+ if (!client) return false;
516
+ try {
517
+ const primary = await client.isFeatureEnabled(flag, workspaceId, {
518
+ groups: { workspace: workspaceId },
519
+ groupProperties: {
520
+ workspace: {
521
+ workspace_id: workspaceId,
522
+ slug: workspaceSlug ?? ""
523
+ }
524
+ }
525
+ });
526
+ if (primary) return true;
527
+ if (workspaceSlug && workspaceSlug !== workspaceId) {
528
+ const secondary = await client.isFeatureEnabled(flag, workspaceSlug, {
529
+ groups: { workspace: workspaceSlug },
530
+ groupProperties: {
531
+ workspace: {
532
+ workspace_id: workspaceId,
533
+ slug: workspaceSlug
534
+ }
535
+ }
536
+ });
537
+ return secondary ?? false;
538
+ }
539
+ return primary ?? false;
540
+ } catch {
541
+ return false;
542
+ }
543
+ }
544
+
545
+ // src/tools/smart-capture-routing.ts
546
+ var CLASSIFIER_AUTO_ROUTE_THRESHOLD = 70;
547
+ var CLASSIFIER_AMBIGUITY_MARGIN = 15;
548
+ var STARTER_COLLECTIONS = ["decisions", "tensions", "glossary", "insights", "bets"];
549
+ var SIGNAL_WEIGHT = 10;
550
+ var MAX_MATCHES_PER_SIGNAL = 2;
551
+ var MAX_REASON_COUNT = 3;
552
+ var STARTER_COLLECTION_SIGNALS = {
553
+ decisions: [
554
+ "decide",
555
+ "decision",
556
+ "chose",
557
+ "chosen",
558
+ "choice",
559
+ "resolved",
560
+ "we will",
561
+ "we should",
562
+ "approved"
563
+ ],
564
+ tensions: [
565
+ "problem",
566
+ "issue",
567
+ "blocked",
568
+ "blocker",
569
+ "friction",
570
+ "pain",
571
+ "risk",
572
+ "constraint",
573
+ "bottleneck",
574
+ "struggle"
575
+ ],
576
+ glossary: [
577
+ "definition",
578
+ "define",
579
+ "term",
580
+ "means",
581
+ "refers to",
582
+ "is called",
583
+ "vocabulary",
584
+ "terminology"
585
+ ],
586
+ insights: [
587
+ "insight",
588
+ "learned",
589
+ "observed",
590
+ "pattern",
591
+ "trend",
592
+ "signal",
593
+ "found that",
594
+ "evidence"
595
+ ],
596
+ bets: [
597
+ "bet",
598
+ "appetite",
599
+ "scope",
600
+ "elements",
601
+ "rabbit hole",
602
+ "no-go",
603
+ "shape",
604
+ "shaping",
605
+ "done when"
606
+ ]
607
+ };
608
+ function escapeRegExp(text) {
609
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
610
+ }
611
+ function countSignalMatches(text, signal) {
612
+ const trimmed = signal.trim().toLowerCase();
613
+ if (!trimmed) return 0;
614
+ const words = trimmed.split(/\s+/).map(escapeRegExp);
615
+ const pattern = `\\b${words.join("\\s+")}\\b`;
616
+ const regex = new RegExp(pattern, "g");
617
+ const matches = text.match(regex);
618
+ return matches?.length ?? 0;
619
+ }
620
+ function classifyStarterCollection(name, description) {
621
+ const text = `${name} ${description}`.toLowerCase();
622
+ const rawScores = [];
623
+ for (const collection of STARTER_COLLECTIONS) {
624
+ const signals = STARTER_COLLECTION_SIGNALS[collection];
625
+ const reasons = [];
626
+ let score = 0;
627
+ for (const signal of signals) {
628
+ const matches = countSignalMatches(text, signal);
629
+ if (matches <= 0) continue;
630
+ const cappedMatches = Math.min(matches, MAX_MATCHES_PER_SIGNAL);
631
+ score += cappedMatches * SIGNAL_WEIGHT;
632
+ if (reasons.length < MAX_REASON_COUNT) {
633
+ const capNote = matches > cappedMatches ? ` (capped at ${cappedMatches})` : "";
634
+ reasons.push(`matched "${signal}" x${matches}${capNote}`);
635
+ }
636
+ }
637
+ rawScores.push({ collection, score, reasons });
638
+ }
639
+ rawScores.sort((a, b) => b.score - a.score);
640
+ const top = rawScores[0];
641
+ const second = rawScores[1];
642
+ if (!top || top.score <= 0) return null;
643
+ const margin = Math.max(0, top.score - (second?.score ?? 0));
644
+ const baseConfidence = Math.min(90, top.score);
645
+ const confidence = Math.min(99, baseConfidence + Math.min(20, margin));
646
+ return {
647
+ collection: top.collection,
648
+ topConfidence: confidence,
649
+ confidence,
650
+ reasons: top.reasons,
651
+ scoreMargin: margin,
652
+ candidates: rawScores.filter((candidate) => candidate.score > 0).slice(0, 3).map((candidate) => ({
653
+ collection: candidate.collection,
654
+ signalScore: Math.min(99, candidate.score),
655
+ confidence: Math.min(99, candidate.score)
656
+ }))
657
+ };
658
+ }
659
+ function shouldAutoRouteClassification(result) {
660
+ if (result.confidence < CLASSIFIER_AUTO_ROUTE_THRESHOLD) return false;
661
+ if (result.scoreMargin < CLASSIFIER_AMBIGUITY_MARGIN) return false;
662
+ return true;
663
+ }
664
+ function isClassificationAmbiguous(result) {
665
+ return result.scoreMargin < CLASSIFIER_AMBIGUITY_MARGIN;
666
+ }
667
+
478
668
  // src/tools/smart-capture.ts
479
669
  var AREA_KEYWORDS = {
480
670
  "Architecture": ["convex", "schema", "database", "migration", "api", "backend", "infrastructure", "scaling", "performance"],
@@ -1020,8 +1210,9 @@ var GOVERNED_COLLECTIONS = /* @__PURE__ */ new Set([
1020
1210
  var AUTO_LINK_CONFIDENCE_THRESHOLD = 35;
1021
1211
  var MAX_AUTO_LINKS = 5;
1022
1212
  var MAX_SUGGESTIONS = 5;
1213
+ var CAPTURE_WITHOUT_THINKING_FLAG = "capture-without-thinking";
1023
1214
  var captureSchema = z.object({
1024
- collection: z.string().describe("Collection slug, e.g. 'tensions', 'business-rules', 'glossary', 'decisions'"),
1215
+ collection: z.string().optional().describe("Collection slug, e.g. 'tensions', 'business-rules', 'glossary', 'decisions'. Optional when `capture-without-thinking` is enabled."),
1025
1216
  name: z.string().describe("Display name \u2014 be specific (e.g. 'Convex adjacency list won't scale for graph traversal')"),
1026
1217
  description: z.string().describe("Full context \u2014 what's happening, why it matters, what you observed"),
1027
1218
  context: z.string().optional().describe("Optional additional context (e.g. 'Observed during context gather calls taking 700ms+')"),
@@ -1042,14 +1233,223 @@ var batchCaptureSchema = z.object({
1042
1233
  entryId: z.string().optional().describe("Optional custom entry ID")
1043
1234
  })).min(1).max(50).describe("Array of entries to capture")
1044
1235
  });
1045
- var captureOutputSchema = z.object({
1236
+ var captureClassifierSchema = z.object({
1237
+ enabled: z.boolean(),
1238
+ autoRouted: z.boolean(),
1239
+ topConfidence: z.number(),
1240
+ // Backward-compatible alias for topConfidence.
1241
+ confidence: z.number(),
1242
+ reasons: z.array(z.string()),
1243
+ candidates: z.array(
1244
+ z.object({
1245
+ collection: z.enum(STARTER_COLLECTIONS),
1246
+ signalScore: z.number(),
1247
+ // Backward-compatible alias for signalScore.
1248
+ confidence: z.number()
1249
+ })
1250
+ )
1251
+ });
1252
+ function trackClassifierTelemetry(params) {
1253
+ const telemetry = {
1254
+ predicted_collection: params.predictedCollection,
1255
+ confidence: params.confidence,
1256
+ auto_routed: params.autoRouted,
1257
+ reason_category: params.reasonCategory,
1258
+ explicit_collection_provided: params.explicitCollectionProvided
1259
+ };
1260
+ trackCaptureClassifierEvaluated(params.workspaceId, telemetry);
1261
+ if (params.outcome === "auto-routed") {
1262
+ trackCaptureClassifierAutoRouted(params.workspaceId, telemetry);
1263
+ return;
1264
+ }
1265
+ trackCaptureClassifierFallback(params.workspaceId, telemetry);
1266
+ }
1267
+ function buildCollectionRequiredResult() {
1268
+ return {
1269
+ content: [{
1270
+ type: "text",
1271
+ text: "Collection is required unless `capture-without-thinking` is enabled.\n\nProvide `collection` explicitly, or enable the feature flag for this workspace."
1272
+ }]
1273
+ };
1274
+ }
1275
+ function buildClassifierUnknownResult() {
1276
+ return {
1277
+ content: [{
1278
+ type: "text",
1279
+ text: "I could not infer a collection confidently from this input.\n\nPlease provide `collection`, or rewrite with clearer intent (decision/problem/definition/insight/bet)."
1280
+ }],
1281
+ structuredContent: {
1282
+ classifier: {
1283
+ enabled: true,
1284
+ autoRouted: false,
1285
+ topConfidence: 0,
1286
+ confidence: 0,
1287
+ reasons: [],
1288
+ candidates: []
1289
+ }
1290
+ }
1291
+ };
1292
+ }
1293
+ function buildProvisionedCollectionSuggestions(candidates) {
1294
+ return candidates.length ? candidates.map((c) => `- \`${c.collection}\` (${c.signalScore}% signal score)`).join("\n") : "- No provisioned starter collection candidates were inferred confidently.";
1295
+ }
1296
+ function buildUnsupportedProvisioningResult(classified, provisionedCandidates) {
1297
+ const suggestions = buildProvisionedCollectionSuggestions(provisionedCandidates);
1298
+ return {
1299
+ content: [{
1300
+ type: "text",
1301
+ text: `Collection inference is not safe to auto-route yet.
1302
+
1303
+ Predicted collection \`${classified.collection}\` is not provisioned/supported for auto-routing in this workspace.
1304
+ Reason: ${classified.reasons.join("; ") || "low signal"}
1305
+
1306
+ Choose one of these provisioned starter collections and retry with \`collection\`:
1307
+ ${suggestions}
1308
+
1309
+ Correction path: rerun with explicit \`collection\`.`
1310
+ }],
1311
+ structuredContent: {
1312
+ classifier: {
1313
+ enabled: true,
1314
+ autoRouted: false,
1315
+ topConfidence: classified.topConfidence,
1316
+ confidence: classified.confidence,
1317
+ reasons: classified.reasons,
1318
+ candidates: provisionedCandidates
1319
+ }
1320
+ }
1321
+ };
1322
+ }
1323
+ function buildAmbiguousRouteResult(classified, classifierMeta, ambiguousRoute) {
1324
+ const suggestions = buildProvisionedCollectionSuggestions(classifierMeta.candidates);
1325
+ return {
1326
+ content: [{
1327
+ type: "text",
1328
+ text: "Collection inference is not safe to auto-route yet.\n\n" + (ambiguousRoute ? "Routing held because intent is ambiguous across top candidates.\n\n" : "") + `Predicted: \`${classified.collection}\` (${classified.topConfidence}% top confidence)
1329
+ Reason: ${classified.reasons.join("; ") || "low signal"}
1330
+
1331
+ Choose one of these and retry with \`collection\`:
1332
+ ${suggestions}
1333
+
1334
+ Correction path: if this was close, rerun with your chosen \`collection\`.`
1335
+ }],
1336
+ structuredContent: {
1337
+ classifier: classifierMeta
1338
+ }
1339
+ };
1340
+ }
1341
+ async function getProvisionedStarterCollectionCandidates(classified, supportedStarterCollections) {
1342
+ const allCollections = await mcpQuery("chain.listCollections");
1343
+ const provisionedStarterCollections = new Set(
1344
+ (allCollections ?? []).map((collection) => collection.slug).filter(
1345
+ (slug) => supportedStarterCollections.has(slug)
1346
+ )
1347
+ );
1348
+ const provisionedCandidates = classified.candidates.filter(
1349
+ (candidate) => provisionedStarterCollections.has(candidate.collection)
1350
+ );
1351
+ return { provisionedStarterCollections, provisionedCandidates };
1352
+ }
1353
+ async function resolveCaptureCollection(params) {
1354
+ const {
1355
+ collection,
1356
+ name,
1357
+ description,
1358
+ classifierFlagOn,
1359
+ supportedStarterCollections,
1360
+ workspaceId,
1361
+ explicitCollectionProvided
1362
+ } = params;
1363
+ if (collection) {
1364
+ return { resolvedCollection: collection };
1365
+ }
1366
+ if (!classifierFlagOn) {
1367
+ return { earlyResult: buildCollectionRequiredResult() };
1368
+ }
1369
+ const classified = classifyStarterCollection(name, description);
1370
+ if (!classified) {
1371
+ trackClassifierTelemetry({
1372
+ workspaceId,
1373
+ predictedCollection: "unknown",
1374
+ confidence: 0,
1375
+ autoRouted: false,
1376
+ reasonCategory: "low-confidence",
1377
+ explicitCollectionProvided,
1378
+ outcome: "fallback"
1379
+ });
1380
+ return { earlyResult: buildClassifierUnknownResult() };
1381
+ }
1382
+ const { provisionedStarterCollections, provisionedCandidates } = await getProvisionedStarterCollectionCandidates(classified, supportedStarterCollections);
1383
+ if (!provisionedStarterCollections.has(classified.collection)) {
1384
+ trackClassifierTelemetry({
1385
+ workspaceId,
1386
+ predictedCollection: classified.collection,
1387
+ confidence: classified.confidence,
1388
+ autoRouted: false,
1389
+ reasonCategory: "non-provisioned",
1390
+ explicitCollectionProvided,
1391
+ outcome: "fallback"
1392
+ });
1393
+ return {
1394
+ earlyResult: buildUnsupportedProvisioningResult(classified, provisionedCandidates)
1395
+ };
1396
+ }
1397
+ const autoRoute = shouldAutoRouteClassification(classified);
1398
+ const ambiguousRoute = isClassificationAmbiguous(classified);
1399
+ const classifierMeta = {
1400
+ enabled: true,
1401
+ autoRouted: autoRoute,
1402
+ topConfidence: classified.topConfidence,
1403
+ confidence: classified.confidence,
1404
+ reasons: classified.reasons,
1405
+ candidates: provisionedCandidates
1406
+ };
1407
+ if (!autoRoute) {
1408
+ trackClassifierTelemetry({
1409
+ workspaceId,
1410
+ predictedCollection: classified.collection,
1411
+ confidence: classified.confidence,
1412
+ autoRouted: false,
1413
+ reasonCategory: ambiguousRoute ? "ambiguous" : "low-confidence",
1414
+ explicitCollectionProvided,
1415
+ outcome: "fallback"
1416
+ });
1417
+ return {
1418
+ classifierMeta,
1419
+ earlyResult: buildAmbiguousRouteResult(classified, classifierMeta, ambiguousRoute)
1420
+ };
1421
+ }
1422
+ trackClassifierTelemetry({
1423
+ workspaceId,
1424
+ predictedCollection: classified.collection,
1425
+ confidence: classified.confidence,
1426
+ autoRouted: true,
1427
+ reasonCategory: "auto-routed",
1428
+ explicitCollectionProvided,
1429
+ outcome: "auto-routed"
1430
+ });
1431
+ return {
1432
+ resolvedCollection: classified.collection,
1433
+ classifierMeta
1434
+ };
1435
+ }
1436
+ var captureSuccessOutputSchema = z.object({
1046
1437
  entryId: z.string(),
1047
1438
  collection: z.string(),
1048
1439
  name: z.string(),
1049
1440
  status: z.enum(["draft", "committed"]),
1050
- qualityScore: z.number().optional(),
1051
- qualityVerdict: z.record(z.unknown()).optional()
1052
- });
1441
+ qualityScore: z.number(),
1442
+ qualityVerdict: z.record(z.unknown()).optional(),
1443
+ classifier: captureClassifierSchema.optional(),
1444
+ studioUrl: z.string().optional()
1445
+ }).strict();
1446
+ var captureClassifierOnlyOutputSchema = z.object({
1447
+ classifier: captureClassifierSchema
1448
+ }).strict();
1449
+ var captureOutputSchema = z.union([
1450
+ captureSuccessOutputSchema,
1451
+ captureClassifierOnlyOutputSchema
1452
+ ]);
1053
1453
  var batchCaptureOutputSchema = z.object({
1054
1454
  captured: z.array(z.object({
1055
1455
  entryId: z.string(),
@@ -1057,31 +1457,64 @@ var batchCaptureOutputSchema = z.object({
1057
1457
  name: z.string()
1058
1458
  })),
1059
1459
  total: z.number(),
1060
- failed: z.number()
1460
+ failed: z.number(),
1461
+ failedEntries: z.array(z.object({
1462
+ index: z.number(),
1463
+ collection: z.string(),
1464
+ name: z.string(),
1465
+ error: z.string()
1466
+ })).optional()
1061
1467
  });
1062
1468
  function registerSmartCaptureTools(server) {
1469
+ const supportedStarterCollections = new Set(
1470
+ STARTER_COLLECTIONS.filter((slug) => PROFILES.has(slug))
1471
+ );
1063
1472
  const captureTool = server.registerTool(
1064
1473
  "capture",
1065
1474
  {
1066
1475
  title: "Capture",
1067
- 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\n**Compound capture:** Pass `links` to create relations in the same call (skips auto-link discovery). Pass `autoCommit: true` to promote the entry from draft to SSOT immediately after linking. Governed collections (glossary, business-rules, principles, standards, strategy, features) will warn but still commit \u2014 use only when you're certain.\n\nAlways creates as 'draft' unless `autoCommit` is true. Use `update-entry` for post-creation adjustments.",
1476
+ description: "The single tool for creating knowledge entries. Creates an entry, auto-links related entries, and returns a quality scorecard \u2014 all in one call. Provide a name and description; `collection` is optional when `capture-without-thinking` is enabled.\n\nSupported collections with smart profiles: tensions, business-rules, glossary, decisions, features, audiences, strategy, standards, maps, chains, tracking-events.\nAll other collections get an ENT-{random} ID and sensible defaults.\n\n**Explicit data:** When you know the schema, pass `data: { field: value }` to set fields directly. Top-level `name` and `description` always win for those fields. `data` wins over inference for all other fields.\n\n**Compound capture:** Pass `links` to create relations in the same call (skips auto-link discovery). Pass `autoCommit: true` to promote the entry from draft to SSOT immediately after linking. Governed collections (glossary, business-rules, principles, standards, strategy, features) will warn but still commit \u2014 use only when you're certain.\n\nAlways creates as 'draft' unless `autoCommit` is true. Use `update-entry` for post-creation adjustments.",
1068
1477
  inputSchema: captureSchema.shape,
1069
1478
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
1070
1479
  },
1071
1480
  async ({ collection, name, description, context, entryId, canonicalKey, data: userData, links, autoCommit }) => {
1072
1481
  requireWriteAccess();
1073
- const profile = PROFILES.get(collection) ?? FALLBACK_PROFILE;
1074
- const col = await mcpQuery("chain.getCollection", { slug: collection });
1482
+ const wsCtx = await getWorkspaceContext();
1483
+ const explicitCollectionProvided = typeof collection === "string" && collection.trim().length > 0;
1484
+ const classifierFlagOn = await isFeatureEnabled(
1485
+ CAPTURE_WITHOUT_THINKING_FLAG,
1486
+ wsCtx.workspaceId,
1487
+ wsCtx.workspaceSlug
1488
+ );
1489
+ const resolution = await resolveCaptureCollection({
1490
+ collection,
1491
+ name,
1492
+ description,
1493
+ classifierFlagOn,
1494
+ supportedStarterCollections,
1495
+ workspaceId: wsCtx.workspaceId,
1496
+ explicitCollectionProvided
1497
+ });
1498
+ if (resolution.earlyResult) {
1499
+ return resolution.earlyResult;
1500
+ }
1501
+ const resolvedCollection = resolution.resolvedCollection;
1502
+ const classifierMeta = resolution.classifierMeta;
1503
+ if (!resolvedCollection) {
1504
+ return buildCollectionRequiredResult();
1505
+ }
1506
+ const profile = PROFILES.get(resolvedCollection) ?? FALLBACK_PROFILE;
1507
+ const col = await mcpQuery("chain.getCollection", { slug: resolvedCollection });
1075
1508
  if (!col) {
1076
- const displayName = collection.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
1509
+ const displayName = resolvedCollection.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
1077
1510
  return {
1078
1511
  content: [{
1079
1512
  type: "text",
1080
- text: `Collection \`${collection}\` not found.
1513
+ text: `Collection \`${resolvedCollection}\` not found.
1081
1514
 
1082
1515
  **To create it**, run:
1083
1516
  \`\`\`
1084
- collections action=create slug="${collection}" name="${displayName}" description="..."
1517
+ collections action=create slug="${resolvedCollection}" name="${displayName}" description="..."
1085
1518
  \`\`\`
1086
1519
 
1087
1520
  Or use \`collections action=list\` to see available collections.`
@@ -1110,7 +1543,7 @@ Or use \`collections action=list\` to see available collections.`
1110
1543
  }
1111
1544
  if (profile.inferField) {
1112
1545
  const inferred = profile.inferField({
1113
- collection,
1546
+ collection: resolvedCollection,
1114
1547
  name,
1115
1548
  description,
1116
1549
  context,
@@ -1138,7 +1571,7 @@ Or use \`collections action=list\` to see available collections.`
1138
1571
  try {
1139
1572
  const agentId = getAgentSessionId();
1140
1573
  const result = await mcpMutation("chain.createEntry", {
1141
- collectionSlug: collection,
1574
+ collectionSlug: resolvedCollection,
1142
1575
  entryId: entryId ?? void 0,
1143
1576
  name,
1144
1577
  status,
@@ -1179,7 +1612,7 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
1179
1612
  const collMap = /* @__PURE__ */ new Map();
1180
1613
  for (const c of allCollections) collMap.set(c._id, c.slug);
1181
1614
  const candidates = (searchResults ?? []).filter((r) => r.entryId !== finalEntryId && r._id !== internalId).map((r) => {
1182
- const conf = computeLinkConfidence(r, name, description, collection, collMap.get(r.collectionId) ?? "unknown");
1615
+ const conf = computeLinkConfidence(r, name, description, resolvedCollection, collMap.get(r.collectionId) ?? "unknown");
1183
1616
  return {
1184
1617
  ...r,
1185
1618
  collSlug: collMap.get(r.collectionId) ?? "unknown",
@@ -1191,7 +1624,7 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
1191
1624
  if (linksCreated.length >= MAX_AUTO_LINKS) break;
1192
1625
  if (c.confidence < AUTO_LINK_CONFIDENCE_THRESHOLD) break;
1193
1626
  if (!c.entryId || !finalEntryId) continue;
1194
- const { type: relationType, reason: relationReason } = inferRelationType(collection, c.collSlug, profile);
1627
+ const { type: relationType, reason: relationReason } = inferRelationType(resolvedCollection, c.collSlug, profile);
1195
1628
  try {
1196
1629
  await mcpMutation("chain.createEntryRelation", {
1197
1630
  fromEntryId: finalEntryId,
@@ -1246,7 +1679,7 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
1246
1679
  }
1247
1680
  }
1248
1681
  const captureCtx = {
1249
- collection,
1682
+ collection: resolvedCollection,
1250
1683
  name,
1251
1684
  description,
1252
1685
  context,
@@ -1294,7 +1727,7 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
1294
1727
  const failedCount = (v.criteria ?? []).filter((c) => !c.passed).length;
1295
1728
  trackQualityVerdict(wsForTracking.workspaceId, {
1296
1729
  entry_id: finalEntryId,
1297
- entry_type: v.canonicalKey ?? collection,
1730
+ entry_type: v.canonicalKey ?? resolvedCollection,
1298
1731
  tier: v.tier,
1299
1732
  context: "capture",
1300
1733
  passed: v.passed,
@@ -1306,9 +1739,10 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
1306
1739
  } catch {
1307
1740
  }
1308
1741
  }
1742
+ const shouldAutoCommit = autoCommit === true || autoCommit === void 0 && wsCtx.governanceMode === "open";
1309
1743
  let finalStatus = "draft";
1310
1744
  let commitError = null;
1311
- if (autoCommit && finalEntryId) {
1745
+ if (shouldAutoCommit && finalEntryId) {
1312
1746
  try {
1313
1747
  await mcpMutation("chain.commitEntry", { entryId: finalEntryId });
1314
1748
  finalStatus = "committed";
@@ -1317,14 +1751,22 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
1317
1751
  commitError = e instanceof Error ? e.message : "unknown error";
1318
1752
  }
1319
1753
  }
1320
- const wsCtx = await getWorkspaceContext();
1321
1754
  const lines = [
1322
1755
  `# Captured: ${finalEntryId || name}`,
1323
- `**${name}** added to \`${collection}\` as \`${finalStatus}\``,
1756
+ `**${name}** added to \`${resolvedCollection}\` as \`${finalStatus}\``,
1324
1757
  `**Workspace:** ${wsCtx.workspaceSlug} (${wsCtx.workspaceId})`
1325
1758
  ];
1759
+ if (classifierMeta?.autoRouted) {
1760
+ lines.push("");
1761
+ lines.push("## Collection routing");
1762
+ lines.push(`Auto-routed to \`${resolvedCollection}\` (${classifierMeta.topConfidence}% top confidence).`);
1763
+ if (classifierMeta.reasons.length > 0) {
1764
+ lines.push(`Reason: ${classifierMeta.reasons.join("; ")}.`);
1765
+ }
1766
+ lines.push("Correction path: rerun capture with explicit `collection` if this routing is wrong.");
1767
+ }
1326
1768
  const appUrl = process.env.PRODUCTBRAIN_APP_URL ?? "https://productbrain.io";
1327
- const studioUrl = collection === "bets" ? `${appUrl.replace(/\/$/, "")}/w/${wsCtx.workspaceSlug}/studio/${internalId}` : void 0;
1769
+ const studioUrl = resolvedCollection === "bets" ? `${appUrl.replace(/\/$/, "")}/w/${wsCtx.workspaceSlug}/studio/${internalId}` : void 0;
1328
1770
  if (studioUrl) {
1329
1771
  lines.push("");
1330
1772
  lines.push(`**View in Studio:** ${studioUrl}`);
@@ -1343,16 +1785,21 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
1343
1785
  for (const r of userLinkResults) lines.push(`- ${r.label}`);
1344
1786
  }
1345
1787
  if (finalStatus === "committed") {
1788
+ const wasAutoCommitted = autoCommit === void 0 && wsCtx.governanceMode === "open";
1346
1789
  lines.push("");
1347
1790
  lines.push(`## Committed: ${finalEntryId}`);
1348
- lines.push(`**${name}** promoted to SSOT on the Chain.`);
1349
- if (GOVERNED_COLLECTIONS.has(collection)) {
1350
- lines.push(`_Note: \`${collection}\` is a governed collection \u2014 ensure this entry has been reviewed._`);
1791
+ if (wasAutoCommitted) {
1792
+ lines.push(`**${name}** added to your knowledge base.`);
1793
+ } else {
1794
+ lines.push(`**${name}** promoted to SSOT on the Chain.`);
1795
+ }
1796
+ if (GOVERNED_COLLECTIONS.has(resolvedCollection)) {
1797
+ lines.push(`_Note: \`${resolvedCollection}\` is a governed collection \u2014 ensure this entry has been reviewed._`);
1351
1798
  }
1352
1799
  } else if (commitError) {
1353
1800
  lines.push("");
1354
1801
  lines.push("## Commit failed");
1355
- lines.push(`Error: ${commitError}. Entry remains as draft.`);
1802
+ lines.push(`Error: ${commitError}. Entry saved as draft \u2014 use \`commit-entry entryId="${finalEntryId}"\` to promote when ready.`);
1356
1803
  }
1357
1804
  if (linksSuggested.length > 0) {
1358
1805
  lines.push("");
@@ -1370,12 +1817,12 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
1370
1817
  lines.push("");
1371
1818
  lines.push(`_To improve: \`update-entry entryId="${finalEntryId}"\` to fill missing fields._`);
1372
1819
  }
1373
- const isBetOrGoal = collection === "bets" || resolvedCK === "bet" || resolvedCK === "goal";
1820
+ const isBetOrGoal = resolvedCollection === "bets" || resolvedCK === "bet" || resolvedCK === "goal";
1374
1821
  const hasStrategyLink = linksCreated.some((l) => l.targetCollection === "strategy");
1375
1822
  if (isBetOrGoal && !hasStrategyLink) {
1376
1823
  lines.push("");
1377
1824
  lines.push(
1378
- `**Strategy link:** This ${collection === "bets" ? "bet" : "goal"} doesn't connect to any strategy entry. Consider linking before commit. Use \`graph action=suggest entryId="${finalEntryId}"\` to find strategy entries to connect to.`
1825
+ `**Strategy link:** This ${resolvedCollection === "bets" ? "bet" : "goal"} doesn't connect to any strategy entry. Consider linking before commit. Use \`graph action=suggest entryId="${finalEntryId}"\` to find strategy entries to connect to.`
1379
1826
  );
1380
1827
  await recordSessionActivity({ strategyLinkWarnedForEntryId: internalId });
1381
1828
  }
@@ -1428,11 +1875,12 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
1428
1875
  content: [{ type: "text", text: lines.join("\n") }],
1429
1876
  structuredContent: {
1430
1877
  entryId: finalEntryId,
1431
- collection,
1878
+ collection: resolvedCollection,
1432
1879
  name,
1433
1880
  status: finalStatus,
1434
1881
  qualityScore: quality.score,
1435
1882
  qualityVerdict: verdictResult?.verdict ? { ...verdictResult.verdict, source: verdictResult.source ?? "heuristic" } : void 0,
1883
+ ...classifierMeta && { classifier: classifierMeta },
1436
1884
  ...studioUrl && { studioUrl }
1437
1885
  }
1438
1886
  };
@@ -1717,11 +2165,13 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
1717
2165
  "still",
1718
2166
  "where"
1719
2167
  ]);
2168
+ function tokenizeText(input) {
2169
+ return input.toLowerCase().replace(/[^\p{L}\p{N}\s]+/gu, " ").split(/\s+/).filter(Boolean);
2170
+ }
1720
2171
  async function runContradictionCheck(name, description) {
1721
2172
  const warnings = [];
1722
2173
  try {
1723
- const text = `${name} ${description}`.toLowerCase();
1724
- const keyTerms = text.split(/\s+/).filter((w) => w.length >= 4 && !STOP_WORDS.has(w)).slice(0, 8);
2174
+ const keyTerms = tokenizeText(`${name} ${description}`).filter((w) => w.length >= 4 && !STOP_WORDS.has(w)).slice(0, 8);
1725
2175
  if (keyTerms.length === 0) return warnings;
1726
2176
  const searchQuery = keyTerms.slice(0, 5).join(" ");
1727
2177
  const [govResults, archResults] = await Promise.all([
@@ -1730,8 +2180,8 @@ async function runContradictionCheck(name, description) {
1730
2180
  ]);
1731
2181
  const allGov = [...govResults ?? [], ...archResults ?? []].slice(0, 5);
1732
2182
  for (const entry of allGov) {
1733
- const entryText = `${entry.name} ${entry.data?.description ?? ""}`.toLowerCase();
1734
- const matched = keyTerms.filter((t) => entryText.includes(t));
2183
+ const entryTokens = new Set(tokenizeText(`${entry.name} ${entry.data?.description ?? ""}`));
2184
+ const matched = keyTerms.filter((t) => entryTokens.has(t));
1735
2185
  if (matched.length < 3) continue;
1736
2186
  let governsCount = 0;
1737
2187
  try {
@@ -1854,10 +2304,16 @@ export {
1854
2304
  translateStaleToolNames,
1855
2305
  initToolSurface,
1856
2306
  trackWriteTool,
2307
+ initFeatureFlags,
2308
+ CLASSIFIER_AUTO_ROUTE_THRESHOLD,
2309
+ STARTER_COLLECTIONS,
2310
+ classifyStarterCollection,
2311
+ isClassificationAmbiguous,
1857
2312
  formatQualityReport,
1858
2313
  checkEntryQuality,
1859
2314
  captureSchema,
1860
2315
  batchCaptureSchema,
2316
+ captureClassifierSchema,
1861
2317
  captureOutputSchema,
1862
2318
  batchCaptureOutputSchema,
1863
2319
  registerSmartCaptureTools,
@@ -1865,4 +2321,4 @@ export {
1865
2321
  formatRubricCoaching,
1866
2322
  formatRubricVerdictSection
1867
2323
  };
1868
- //# sourceMappingURL=chunk-7VJP2IMS.js.map
2324
+ //# sourceMappingURL=chunk-M264FY2V.js.map