@productbrain/mcp 0.0.1-beta.66 → 0.0.1-beta.67

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.
@@ -939,17 +939,108 @@ function classifyCollection(name, description) {
939
939
  }))
940
940
  };
941
941
  }
942
- function shouldAutoRouteClassification(result) {
943
- if (result.confidence < CLASSIFIER_AUTO_ROUTE_THRESHOLD) return false;
944
- if (result.scoreMargin < CLASSIFIER_AMBIGUITY_MARGIN) return false;
945
- return true;
946
- }
947
942
  function isClassificationAmbiguous(result) {
948
943
  return result.scoreMargin < CLASSIFIER_AMBIGUITY_MARGIN;
949
944
  }
950
945
 
951
- // src/tools/smart-capture-routing.ts
952
- var STARTER_COLLECTIONS = CLASSIFIABLE_COLLECTIONS;
946
+ // src/lib/resolveCollection.ts
947
+ var HIGH_THRESHOLD = 80;
948
+ var MEDIUM_THRESHOLD = 50;
949
+ function getTier(confidence) {
950
+ if (confidence >= HIGH_THRESHOLD) return "high";
951
+ if (confidence >= MEDIUM_THRESHOLD) return "medium";
952
+ return "low";
953
+ }
954
+ var TYPE_HINT_COLLECTION_MAP = {
955
+ element: "features",
956
+ risk: "tensions",
957
+ decision: "decisions"
958
+ };
959
+ async function resolveCollection(params) {
960
+ const { name, description, typeHint } = params;
961
+ const llmResult = await tryLlmClassifier(name, description);
962
+ if (llmResult) {
963
+ return typeHint ? applyTypeHintBias(llmResult, typeHint) : llmResult;
964
+ }
965
+ const heuristic = classifyCollection(name, description);
966
+ if (heuristic) {
967
+ const resolved = heuristicToResolved(heuristic);
968
+ return typeHint ? applyTypeHintBias(resolved, typeHint) : resolved;
969
+ }
970
+ if (typeHint) {
971
+ const mapped = TYPE_HINT_COLLECTION_MAP[typeHint];
972
+ if (mapped) {
973
+ return {
974
+ collection: mapped,
975
+ confidence: 60,
976
+ tier: "medium",
977
+ alternatives: [],
978
+ classifiedBy: "heuristic",
979
+ reasoning: `Fallback from type hint: ${typeHint} \u2192 ${mapped}`
980
+ };
981
+ }
982
+ }
983
+ return null;
984
+ }
985
+ async function tryLlmClassifier(name, description) {
986
+ try {
987
+ const sessionId = getAgentSessionId();
988
+ const result = await mcpCall(
989
+ "chain.classifyCollection",
990
+ {
991
+ entryName: name,
992
+ entryDescription: description,
993
+ ...sessionId ? { agentSessionId: sessionId } : {}
994
+ }
995
+ );
996
+ if (!result || typeof result.collection !== "string" || typeof result.confidence !== "number") {
997
+ return null;
998
+ }
999
+ return {
1000
+ collection: result.collection,
1001
+ confidence: result.confidence,
1002
+ tier: getTier(result.confidence),
1003
+ alternatives: Array.isArray(result.alternatives) ? result.alternatives : [],
1004
+ classifiedBy: "llm",
1005
+ reasoning: typeof result.reasoning === "string" ? result.reasoning : ""
1006
+ };
1007
+ } catch (err) {
1008
+ process.stderr.write(
1009
+ `[resolveCollection] LLM classifier failed, falling back to heuristic: ${err instanceof Error ? err.message : String(err)}
1010
+ `
1011
+ );
1012
+ return null;
1013
+ }
1014
+ }
1015
+ function applyTypeHintBias(result, typeHint) {
1016
+ const hintedCollection = TYPE_HINT_COLLECTION_MAP[typeHint];
1017
+ if (!hintedCollection) return result;
1018
+ if (result.collection === hintedCollection) {
1019
+ const boosted = Math.min(99, result.confidence + 10);
1020
+ return { ...result, confidence: boosted, tier: getTier(boosted) };
1021
+ }
1022
+ if (result.tier === "high") return result;
1023
+ return {
1024
+ ...result,
1025
+ collection: hintedCollection,
1026
+ confidence: 65,
1027
+ tier: "medium",
1028
+ reasoning: `Type hint '${typeHint}' overrode ${result.classifiedBy} classification (${result.collection} at ${result.confidence}%)`
1029
+ };
1030
+ }
1031
+ function heuristicToResolved(result) {
1032
+ return {
1033
+ collection: result.collection,
1034
+ confidence: result.confidence,
1035
+ tier: getTier(result.confidence),
1036
+ alternatives: result.candidates.slice(0, 3).map((c) => ({
1037
+ collection: c.collection,
1038
+ confidence: c.confidence
1039
+ })),
1040
+ classifiedBy: "heuristic",
1041
+ reasoning: result.reasons.join("; ")
1042
+ };
1043
+ }
953
1044
 
954
1045
  // src/envelope.ts
955
1046
  import { z } from "zod";
@@ -1159,6 +1250,9 @@ function withEnvelope(handler) {
1159
1250
  };
1160
1251
  }
1161
1252
 
1253
+ // src/tools/smart-capture-routing.ts
1254
+ var STARTER_COLLECTIONS = CLASSIFIABLE_COLLECTIONS;
1255
+
1162
1256
  // src/tools/smart-capture.ts
1163
1257
  var AREA_KEYWORDS = {
1164
1258
  "Architecture": ["convex", "schema", "database", "migration", "api", "backend", "infrastructure", "scaling", "performance"],
@@ -1838,7 +1932,7 @@ var captureSchema = z2.object({
1838
1932
  });
1839
1933
  var batchCaptureSchema = z2.object({
1840
1934
  entries: z2.array(z2.object({
1841
- collection: z2.string().describe("Collection slug"),
1935
+ collection: z2.string().optional().describe("Collection slug. Optional \u2014 auto-classified via LLM when omitted (FEAT-160)."),
1842
1936
  name: z2.string().describe("Display name"),
1843
1937
  description: z2.string().describe("Full context / definition"),
1844
1938
  entryId: z2.string().optional().describe("Optional custom entry ID")
@@ -1857,13 +1951,16 @@ var captureClassifierSchema = z2.object({
1857
1951
  reasons: z2.array(z2.string()),
1858
1952
  candidates: z2.array(
1859
1953
  z2.object({
1860
- collection: z2.enum(CLASSIFIABLE_COLLECTIONS),
1861
- signalScore: z2.number(),
1954
+ collection: z2.string(),
1955
+ signalScore: z2.number().optional(),
1862
1956
  confidence: z2.number()
1863
1957
  })
1864
1958
  ),
1865
1959
  agentProvidedCollection: z2.string().optional(),
1866
- overrideCommand: z2.string().optional()
1960
+ overrideCommand: z2.string().optional(),
1961
+ classifiedBy: z2.enum(["llm", "heuristic", "explicit"]).optional(),
1962
+ confidenceTier: z2.enum(["high", "medium", "low"]).optional(),
1963
+ reasoning: z2.string().optional()
1867
1964
  });
1868
1965
  function trackClassifierTelemetry(params) {
1869
1966
  const telemetry = {
@@ -1909,58 +2006,28 @@ function buildClassifierUnknownResult() {
1909
2006
  )
1910
2007
  };
1911
2008
  }
1912
- function buildProvisionedCollectionSuggestions(candidates) {
1913
- return candidates.length ? candidates.map((c) => `- \`${c.collection}\` (${c.signalScore}% signal score)`).join("\n") : "- No provisioned collection candidates were inferred confidently.";
1914
- }
1915
- function buildUnsupportedProvisioningResult(classified, provisionedCandidates) {
1916
- const suggestions = buildProvisionedCollectionSuggestions(provisionedCandidates);
1917
- const textBody = `Collection inference is not safe to auto-route yet.
1918
-
1919
- Predicted collection \`${classified.collection}\` is not provisioned/supported for auto-routing in this workspace.
1920
- Reason: ${classified.reasons.join("; ") || "low signal"}
1921
-
1922
- Choose one of these provisioned starter collections and retry with \`collection\`:
1923
- ${suggestions}
2009
+ function buildLowConfidenceResult(resolved, classifierMeta) {
2010
+ const allCandidates = [
2011
+ { collection: resolved.collection, confidence: resolved.confidence },
2012
+ ...resolved.alternatives
2013
+ ];
2014
+ const suggestions = allCandidates.map((c) => `- \`${c.collection}\` (${c.confidence}% confidence)`).join("\n");
2015
+ const textBody = `Low confidence classification (${resolved.confidence}%, classified by ${resolved.classifiedBy}).
1924
2016
 
1925
- Correction path: rerun with explicit \`collection\`.`;
1926
- return {
1927
- content: [{ type: "text", text: textBody }],
1928
- structuredContent: failure(
1929
- "VALIDATION_ERROR",
1930
- `Collection '${classified.collection}' is not provisioned for auto-routing.`,
1931
- "Rerun with explicit collection.",
1932
- [{ tool: "collections", description: "List available collections", parameters: { action: "list" } }],
1933
- {
1934
- classifier: {
1935
- enabled: true,
1936
- autoRouted: false,
1937
- agrees: false,
1938
- abstained: false,
1939
- topConfidence: classified.topConfidence,
1940
- confidence: classified.confidence,
1941
- reasons: classified.reasons,
1942
- candidates: provisionedCandidates.map((c) => ({ collection: c.collection, signalScore: c.signalScore, confidence: c.confidence }))
1943
- }
1944
- }
1945
- )
1946
- };
1947
- }
1948
- function buildAmbiguousRouteResult(classified, classifierMeta, ambiguousRoute) {
1949
- const suggestions = buildProvisionedCollectionSuggestions(classifierMeta.candidates);
1950
- const textBody = "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)
1951
- Reason: ${classified.reasons.join("; ") || "low signal"}
2017
+ Predicted: \`${resolved.collection}\`
2018
+ Reason: ${resolved.reasoning || "low signal"}
1952
2019
 
1953
2020
  Choose one of these and retry with \`collection\`:
1954
2021
  ${suggestions}
1955
2022
 
1956
- Correction path: if this was close, rerun with your chosen \`collection\`.`;
2023
+ Correction path: rerun with your chosen \`collection\`, or capture and use \`entries action=move\` later.`;
1957
2024
  return {
1958
2025
  content: [{ type: "text", text: textBody }],
1959
2026
  structuredContent: failure(
1960
2027
  "VALIDATION_ERROR",
1961
- ambiguousRoute ? `Ambiguous routing \u2014 top candidates too close (${classified.topConfidence}% confidence).` : `Low confidence routing to '${classified.collection}' (${classified.topConfidence}%).`,
2028
+ `Low confidence routing to '${resolved.collection}' (${resolved.confidence}%, ${resolved.classifiedBy}).`,
1962
2029
  "Rerun with explicit collection.",
1963
- classifierMeta.candidates.map((c) => ({
2030
+ allCandidates.map((c) => ({
1964
2031
  tool: "capture",
1965
2032
  description: `Capture to ${c.collection}`,
1966
2033
  parameters: { collection: c.collection }
@@ -1969,17 +2036,24 @@ Correction path: if this was close, rerun with your chosen \`collection\`.`;
1969
2036
  )
1970
2037
  };
1971
2038
  }
1972
- async function getProvisionedCollectionCandidates(classified, supportedCollections) {
1973
- const allCollections = await mcpQuery("chain.listCollections");
1974
- const provisionedCollections = new Set(
1975
- (allCollections ?? []).map((collection) => collection.slug).filter(
1976
- (slug) => supportedCollections.has(slug)
1977
- )
1978
- );
1979
- const provisionedCandidates = classified.candidates.filter(
1980
- (candidate) => provisionedCollections.has(candidate.collection)
1981
- );
1982
- return { provisionedCollections, provisionedCandidates };
2039
+ function buildClassifierMeta(resolved, overrides = {}) {
2040
+ return {
2041
+ enabled: true,
2042
+ autoRouted: false,
2043
+ agrees: true,
2044
+ abstained: false,
2045
+ topConfidence: resolved.confidence,
2046
+ confidence: resolved.confidence,
2047
+ reasons: resolved.reasoning ? [resolved.reasoning] : [],
2048
+ candidates: [
2049
+ { collection: resolved.collection, confidence: resolved.confidence },
2050
+ ...resolved.alternatives.map((a) => ({ collection: a.collection, confidence: a.confidence }))
2051
+ ].slice(0, 3),
2052
+ classifiedBy: resolved.classifiedBy,
2053
+ confidenceTier: resolved.tier,
2054
+ reasoning: resolved.reasoning,
2055
+ ...overrides
2056
+ };
1983
2057
  }
1984
2058
  async function resolveCaptureCollection(params) {
1985
2059
  const {
@@ -1987,33 +2061,26 @@ async function resolveCaptureCollection(params) {
1987
2061
  name,
1988
2062
  description,
1989
2063
  classifierFlagOn,
1990
- supportedCollections,
1991
2064
  workspaceId,
1992
2065
  explicitCollectionProvided
1993
2066
  } = params;
1994
2067
  if (collection) {
1995
- const classified2 = classifyCollection(name, description);
1996
- if (classified2) {
1997
- const agrees = classified2.collection === collection;
1998
- const classifierMeta2 = {
1999
- enabled: true,
2068
+ const resolved2 = await resolveCollection({ name, description });
2069
+ if (resolved2) {
2070
+ const agrees = resolved2.collection === collection;
2071
+ const classifierMeta2 = buildClassifierMeta(resolved2, {
2000
2072
  autoRouted: false,
2001
2073
  agrees,
2002
- abstained: false,
2003
- topConfidence: classified2.topConfidence,
2004
- confidence: classified2.confidence,
2005
- reasons: classified2.reasons,
2006
- candidates: classified2.candidates.slice(0, 3),
2007
2074
  agentProvidedCollection: collection,
2008
2075
  ...!agrees && {
2009
- overrideCommand: `capture collection="${classified2.collection}"`
2076
+ overrideCommand: `capture collection="${resolved2.collection}"`
2010
2077
  }
2011
- };
2078
+ });
2012
2079
  if (!agrees) {
2013
2080
  trackClassifierTelemetry({
2014
2081
  workspaceId,
2015
- predictedCollection: classified2.collection,
2016
- confidence: classified2.confidence,
2082
+ predictedCollection: resolved2.collection,
2083
+ confidence: resolved2.confidence,
2017
2084
  autoRouted: false,
2018
2085
  reasonCategory: "low-confidence",
2019
2086
  explicitCollectionProvided: true,
@@ -2040,8 +2107,8 @@ async function resolveCaptureCollection(params) {
2040
2107
  if (!classifierFlagOn) {
2041
2108
  return { earlyResult: buildCollectionRequiredResult() };
2042
2109
  }
2043
- const classified = classifyCollection(name, description);
2044
- if (!classified) {
2110
+ const resolved = await resolveCollection({ name, description });
2111
+ if (!resolved) {
2045
2112
  trackClassifierTelemetry({
2046
2113
  workspaceId,
2047
2114
  predictedCollection: "unknown",
@@ -2053,59 +2120,34 @@ async function resolveCaptureCollection(params) {
2053
2120
  });
2054
2121
  return { earlyResult: buildClassifierUnknownResult() };
2055
2122
  }
2056
- const { provisionedCollections, provisionedCandidates } = await getProvisionedCollectionCandidates(classified, supportedCollections);
2057
- if (!provisionedCollections.has(classified.collection)) {
2058
- trackClassifierTelemetry({
2059
- workspaceId,
2060
- predictedCollection: classified.collection,
2061
- confidence: classified.confidence,
2062
- autoRouted: false,
2063
- reasonCategory: "non-provisioned",
2064
- explicitCollectionProvided,
2065
- outcome: "fallback"
2066
- });
2067
- return {
2068
- earlyResult: buildUnsupportedProvisioningResult(classified, provisionedCandidates)
2069
- };
2070
- }
2071
- const autoRoute = shouldAutoRouteClassification(classified);
2072
- const ambiguousRoute = isClassificationAmbiguous(classified);
2073
- const classifierMeta = {
2074
- enabled: true,
2075
- autoRouted: autoRoute,
2076
- agrees: true,
2077
- abstained: false,
2078
- topConfidence: classified.topConfidence,
2079
- confidence: classified.confidence,
2080
- reasons: classified.reasons,
2081
- candidates: provisionedCandidates
2082
- };
2123
+ const autoRoute = resolved.tier !== "low";
2124
+ const classifierMeta = buildClassifierMeta(resolved, { autoRouted: autoRoute });
2083
2125
  if (!autoRoute) {
2084
2126
  trackClassifierTelemetry({
2085
2127
  workspaceId,
2086
- predictedCollection: classified.collection,
2087
- confidence: classified.confidence,
2128
+ predictedCollection: resolved.collection,
2129
+ confidence: resolved.confidence,
2088
2130
  autoRouted: false,
2089
- reasonCategory: ambiguousRoute ? "ambiguous" : "low-confidence",
2131
+ reasonCategory: "low-confidence",
2090
2132
  explicitCollectionProvided,
2091
2133
  outcome: "fallback"
2092
2134
  });
2093
2135
  return {
2094
2136
  classifierMeta,
2095
- earlyResult: buildAmbiguousRouteResult(classified, classifierMeta, ambiguousRoute)
2137
+ earlyResult: buildLowConfidenceResult(resolved, classifierMeta)
2096
2138
  };
2097
2139
  }
2098
2140
  trackClassifierTelemetry({
2099
2141
  workspaceId,
2100
- predictedCollection: classified.collection,
2101
- confidence: classified.confidence,
2142
+ predictedCollection: resolved.collection,
2143
+ confidence: resolved.confidence,
2102
2144
  autoRouted: true,
2103
2145
  reasonCategory: "auto-routed",
2104
2146
  explicitCollectionProvided,
2105
2147
  outcome: "auto-routed"
2106
2148
  });
2107
2149
  return {
2108
- resolvedCollection: classified.collection,
2150
+ resolvedCollection: resolved.collection,
2109
2151
  classifierMeta
2110
2152
  };
2111
2153
  }
@@ -2131,14 +2173,28 @@ var batchCaptureOutputSchema = z2.object({
2131
2173
  entryId: z2.string(),
2132
2174
  collection: z2.string(),
2133
2175
  name: z2.string(),
2134
- status: z2.enum(["draft", "committed", "proposed"])
2176
+ status: z2.enum(["draft", "committed", "proposed"]),
2177
+ classifiedBy: z2.enum(["llm", "heuristic", "explicit"]).optional(),
2178
+ confidence: z2.number().optional(),
2179
+ confidenceTier: z2.enum(["high", "medium", "low"]).optional()
2135
2180
  })),
2136
2181
  total: z2.number(),
2137
2182
  failed: z2.number(),
2138
2183
  committed: z2.number(),
2139
2184
  proposed: z2.number(),
2140
2185
  drafts: z2.number(),
2186
+ classified: z2.number().optional(),
2141
2187
  autoCommitApplied: z2.boolean(),
2188
+ skippedLowConfidence: z2.array(z2.object({
2189
+ index: z2.number(),
2190
+ name: z2.string(),
2191
+ suggestedCollection: z2.string().optional(),
2192
+ confidence: z2.number().optional(),
2193
+ alternatives: z2.array(z2.object({
2194
+ collection: z2.string(),
2195
+ confidence: z2.number()
2196
+ })).optional()
2197
+ })).optional(),
2142
2198
  failedEntries: z2.array(z2.object({
2143
2199
  index: z2.number(),
2144
2200
  collection: z2.string(),
@@ -2150,9 +2206,6 @@ function shouldAutoCommitCapture(autoCommit, governanceMode) {
2150
2206
  return autoCommit === true || autoCommit === void 0 && governanceMode === "open";
2151
2207
  }
2152
2208
  function registerSmartCaptureTools(server) {
2153
- const supportedCollections = new Set(
2154
- CLASSIFIABLE_COLLECTIONS.filter((slug) => PROFILES.has(slug))
2155
- );
2156
2209
  const captureTool = server.registerTool(
2157
2210
  "capture",
2158
2211
  {
@@ -2175,7 +2228,6 @@ function registerSmartCaptureTools(server) {
2175
2228
  name,
2176
2229
  description,
2177
2230
  classifierFlagOn,
2178
- supportedCollections,
2179
2231
  workspaceId: wsCtx.workspaceId,
2180
2232
  explicitCollectionProvided
2181
2233
  });
@@ -2642,7 +2694,7 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2642
2694
  "batch-capture",
2643
2695
  {
2644
2696
  title: "Batch Capture",
2645
- 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 action=check` on individual entries afterward if needed.\n\nPass `autoCommit: false` to keep the whole batch draft-first. If omitted, Open mode workspaces commit by default and consensus/role modes keep drafts.",
2697
+ description: "Create multiple knowledge entries in one call. Ideal for workspace setup, document ingestion, or any scenario where you need to capture many entries at once.\n\nCollection is optional per entry \u2014 omit it and the LLM classifier auto-routes (FEAT-160). High/medium confidence entries are created; low confidence entries are skipped and returned with suggested collections for manual review.\n\nEach entry is created independently \u2014 if one fails, the others still succeed. Returns a compact summary instead of per-entry quality scorecards.\n\nAuto-linking runs per entry but contradiction checks and readiness hints are skipped for speed. Use `quality action=check` on individual entries afterward if needed.\n\nPass `autoCommit: false` to keep the whole batch draft-first. If omitted, Open mode workspaces commit by default and consensus/role modes keep drafts.",
2646
2698
  inputSchema: batchCaptureSchema.shape,
2647
2699
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
2648
2700
  },
@@ -2652,7 +2704,25 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2652
2704
  const createdBy = agentId ? `agent:${agentId}` : "capture";
2653
2705
  const wsCtx = await getWorkspaceContext();
2654
2706
  const autoCommitApplied = shouldAutoCommitCapture(autoCommit, wsCtx.governanceMode);
2707
+ const needsClassification = entries.map((e, i) => ({ ...e, index: i })).filter((e) => !e.collection);
2708
+ let classificationMap = /* @__PURE__ */ new Map();
2709
+ if (needsClassification.length > 0) {
2710
+ await server.sendLoggingMessage({
2711
+ level: "info",
2712
+ data: `Classifying ${needsClassification.length}/${entries.length} entries via LLM...`,
2713
+ logger: "product-brain"
2714
+ });
2715
+ const classifications = await Promise.all(
2716
+ needsClassification.map(
2717
+ (e) => resolveCollection({ name: e.name, description: e.description }).catch(() => null)
2718
+ )
2719
+ );
2720
+ for (let i = 0; i < needsClassification.length; i++) {
2721
+ classificationMap.set(needsClassification[i].index, classifications[i]);
2722
+ }
2723
+ }
2655
2724
  const results = [];
2725
+ const skippedLowConfidence = [];
2656
2726
  await server.sendLoggingMessage({
2657
2727
  level: "info",
2658
2728
  data: `Batch capturing ${entries.length} entries...`,
@@ -2672,17 +2742,43 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2672
2742
  logger: "product-brain"
2673
2743
  });
2674
2744
  }
2675
- const profile = PROFILES.get(entry.collection) ?? FALLBACK_PROFILE;
2676
- const col = collCache.get(entry.collection);
2745
+ let resolvedSlug = entry.collection;
2746
+ let classifiedBy;
2747
+ let confidence;
2748
+ let confidenceTier;
2749
+ if (resolvedSlug) {
2750
+ classifiedBy = "explicit";
2751
+ } else {
2752
+ const resolved = classificationMap.get(entryIdx);
2753
+ if (!resolved || resolved.tier === "low") {
2754
+ skippedLowConfidence.push({
2755
+ index: entryIdx,
2756
+ name: entry.name,
2757
+ suggestedCollection: resolved?.collection,
2758
+ confidence: resolved?.confidence,
2759
+ alternatives: resolved?.alternatives.slice(0, 3)
2760
+ });
2761
+ continue;
2762
+ }
2763
+ resolvedSlug = resolved.collection;
2764
+ classifiedBy = resolved.classifiedBy;
2765
+ confidence = resolved.confidence;
2766
+ confidenceTier = resolved.tier;
2767
+ }
2768
+ const profile = PROFILES.get(resolvedSlug) ?? FALLBACK_PROFILE;
2769
+ const col = collCache.get(resolvedSlug);
2677
2770
  if (!col) {
2678
2771
  results.push({
2679
2772
  name: entry.name,
2680
- collection: entry.collection,
2773
+ collection: resolvedSlug,
2681
2774
  entryId: "",
2682
2775
  ok: false,
2683
2776
  autoLinks: 0,
2684
2777
  status: "draft",
2685
- error: `Collection "${entry.collection}" not found`
2778
+ classifiedBy,
2779
+ confidence,
2780
+ confidenceTier,
2781
+ error: `Collection "${resolvedSlug}" not found`
2686
2782
  });
2687
2783
  continue;
2688
2784
  }
@@ -2705,7 +2801,7 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2705
2801
  }
2706
2802
  if (profile.inferField) {
2707
2803
  const inferred = profile.inferField({
2708
- collection: entry.collection,
2804
+ collection: resolvedSlug,
2709
2805
  name: entry.name,
2710
2806
  description: entry.description,
2711
2807
  data,
@@ -2723,7 +2819,7 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2723
2819
  }
2724
2820
  try {
2725
2821
  const result = await mcpMutation("chain.createEntry", {
2726
- collectionSlug: entry.collection,
2822
+ collectionSlug: resolvedSlug,
2727
2823
  entryId: entry.entryId ?? void 0,
2728
2824
  name: entry.name,
2729
2825
  status: "draft",
@@ -2742,7 +2838,7 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2742
2838
  try {
2743
2839
  const searchResults = await mcpQuery("chain.searchEntries", { query: searchQuery });
2744
2840
  const candidates = (searchResults ?? []).filter((r) => r.entryId !== finalEntryId).map((r) => {
2745
- const conf = computeLinkConfidence(r, entry.name, entry.description, entry.collection, collIdToSlug.get(r.collectionId) ?? "unknown");
2841
+ const conf = computeLinkConfidence(r, entry.name, entry.description, resolvedSlug, collIdToSlug.get(r.collectionId) ?? "unknown");
2746
2842
  return {
2747
2843
  ...r,
2748
2844
  collSlug: collIdToSlug.get(r.collectionId) ?? "unknown",
@@ -2753,7 +2849,7 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2753
2849
  if (autoLinkCount >= MAX_AUTO_LINKS) break;
2754
2850
  if (c.confidence < AUTO_LINK_CONFIDENCE_THRESHOLD) break;
2755
2851
  if (!c.entryId) continue;
2756
- const { type: relationType } = inferRelationType(entry.collection, c.collSlug, profile);
2852
+ const { type: relationType } = inferRelationType(resolvedSlug, c.collSlug, profile);
2757
2853
  try {
2758
2854
  await mcpMutation("chain.createEntryRelation", {
2759
2855
  fromEntryId: finalEntryId,
@@ -2785,22 +2881,28 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2785
2881
  }
2786
2882
  results.push({
2787
2883
  name: entry.name,
2788
- collection: entry.collection,
2884
+ collection: resolvedSlug,
2789
2885
  entryId: finalEntryId,
2790
2886
  ok: true,
2791
2887
  autoLinks: autoLinkCount,
2792
2888
  status: finalStatus,
2889
+ classifiedBy,
2890
+ confidence,
2891
+ confidenceTier,
2793
2892
  ...commitError ? { commitError } : {}
2794
2893
  });
2795
2894
  } catch (error) {
2796
2895
  const msg = error instanceof Error ? error.message : String(error);
2797
2896
  results.push({
2798
2897
  name: entry.name,
2799
- collection: entry.collection,
2898
+ collection: resolvedSlug,
2800
2899
  entryId: "",
2801
2900
  ok: false,
2802
2901
  autoLinks: 0,
2803
2902
  status: "draft",
2903
+ classifiedBy,
2904
+ confidence,
2905
+ confidenceTier,
2804
2906
  error: msg
2805
2907
  });
2806
2908
  }
@@ -2811,9 +2913,10 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2811
2913
  const proposed = created.filter((r) => r.status === "proposed");
2812
2914
  const drafts = created.filter((r) => r.status === "draft");
2813
2915
  const commitFailures = created.filter((r) => r.commitError);
2916
+ const classifiedCount = created.filter((r) => r.classifiedBy && r.classifiedBy !== "explicit").length;
2814
2917
  await server.sendLoggingMessage({
2815
2918
  level: "info",
2816
- data: `Batch complete. ${created.length} succeeded, ${failed.length} failed.`,
2919
+ data: `Batch complete. ${created.length} succeeded, ${failed.length} failed, ${skippedLowConfidence.length} skipped (low confidence).`,
2817
2920
  logger: "product-brain"
2818
2921
  });
2819
2922
  const totalAutoLinks = created.reduce((sum, r) => sum + r.autoLinks, 0);
@@ -2823,10 +2926,14 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2823
2926
  }
2824
2927
  const lines = [
2825
2928
  `# Batch Capture Complete`,
2826
- `**${created.length}** created, **${failed.length}** failed out of ${entries.length} total.`,
2929
+ `**${created.length}** created, **${failed.length}** failed, **${skippedLowConfidence.length}** skipped out of ${entries.length} total.`,
2827
2930
  `**Auto-links created:** ${totalAutoLinks}`,
2828
2931
  ""
2829
2932
  ];
2933
+ if (classifiedCount > 0) {
2934
+ lines.push(`**Auto-classified:** ${classifiedCount} entries routed by LLM/heuristic.`);
2935
+ lines.push("");
2936
+ }
2830
2937
  if (created.length > 0) {
2831
2938
  lines.push(
2832
2939
  `**Statuses:** ${committed.length} committed, ${proposed.length} proposed, ${drafts.length} draft.`
@@ -2841,12 +2948,38 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2841
2948
  lines.push("");
2842
2949
  }
2843
2950
  if (created.length > 0) {
2844
- lines.push("## Created");
2845
- for (const r of created) {
2846
- const linkNote = r.autoLinks > 0 ? `, ${r.autoLinks} auto-links` : "";
2847
- lines.push(`- **${r.entryId}**: ${r.name} [${r.collection}] \u2014 \`${r.status}\`${linkNote}`);
2951
+ const highConfidence = created.filter((r) => r.confidenceTier === "high" || r.classifiedBy === "explicit");
2952
+ const mediumConfidence = created.filter((r) => r.confidenceTier === "medium");
2953
+ if (highConfidence.length > 0) {
2954
+ lines.push("## Created \u2014 High Confidence / Explicit");
2955
+ for (const r of highConfidence) {
2956
+ const linkNote = r.autoLinks > 0 ? `, ${r.autoLinks} auto-links` : "";
2957
+ const classNote = r.classifiedBy !== "explicit" ? ` (${r.classifiedBy} ${r.confidence}%)` : "";
2958
+ lines.push(`- **${r.entryId}**: ${r.name} [${r.collection}] \u2014 \`${r.status}\`${classNote}${linkNote}`);
2959
+ }
2960
+ }
2961
+ if (mediumConfidence.length > 0) {
2962
+ lines.push("");
2963
+ lines.push("## Created \u2014 Medium Confidence (review recommended)");
2964
+ for (const r of mediumConfidence) {
2965
+ const linkNote = r.autoLinks > 0 ? `, ${r.autoLinks} auto-links` : "";
2966
+ lines.push(`- **${r.entryId}**: ${r.name} [${r.collection}] \u2014 \`${r.status}\` (${r.classifiedBy} ${r.confidence}%)${linkNote}`);
2967
+ }
2968
+ lines.push(`
2969
+ _Use \`entries action=move\` to correct any misclassified entries._`);
2848
2970
  }
2849
2971
  }
2972
+ if (skippedLowConfidence.length > 0) {
2973
+ lines.push("");
2974
+ lines.push("## Skipped \u2014 Low Confidence (needs explicit collection)");
2975
+ for (const s of skippedLowConfidence) {
2976
+ const suggestion = s.suggestedCollection ? ` \u2014 best guess: \`${s.suggestedCollection}\` (${s.confidence}%)` : " \u2014 no classification available";
2977
+ const alts = s.alternatives?.length ? ` | alternatives: ${s.alternatives.map((a) => `\`${a.collection}\` (${a.confidence}%)`).join(", ")}` : "";
2978
+ lines.push(`- **[${s.index}]** ${s.name}${suggestion}${alts}`);
2979
+ }
2980
+ lines.push("");
2981
+ lines.push("_Re-capture these with an explicit `collection` or use the suggested collection._");
2982
+ }
2850
2983
  if (commitFailures.length > 0) {
2851
2984
  lines.push("");
2852
2985
  lines.push("## Saved as draft");
@@ -2864,21 +2997,36 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2864
2997
  lines.push(`_If failed > 0, inspect \`failedEntries\` in the structured response and retry individually._`);
2865
2998
  }
2866
2999
  const entryIds = created.map((r) => r.entryId);
2867
- if (entryIds.length > 0) {
3000
+ if (entryIds.length > 0 || skippedLowConfidence.length > 0) {
2868
3001
  lines.push("");
2869
3002
  lines.push("## Next Steps");
2870
- lines.push(`- **Connect:** Run \`graph action=suggest\` on key entries to build the knowledge graph`);
3003
+ if (entryIds.length > 0) {
3004
+ lines.push(`- **Connect:** Run \`graph action=suggest\` on key entries to build the knowledge graph`);
3005
+ }
2871
3006
  if (drafts.length > 0) {
2872
3007
  lines.push(`- **Commit:** Use \`commit-entry\` to promote remaining drafts to SSOT`);
2873
3008
  }
2874
- lines.push(`- **Quality:** Run \`quality action=check\` on individual entries to assess completeness`);
3009
+ if (skippedLowConfidence.length > 0) {
3010
+ lines.push(`- **Classify:** Re-capture ${skippedLowConfidence.length} skipped entries with explicit collections`);
3011
+ }
3012
+ if (entryIds.length > 0) {
3013
+ lines.push(`- **Quality:** Run \`quality action=check\` on individual entries to assess completeness`);
3014
+ }
2875
3015
  }
2876
- const summary = failed.length > 0 ? `Batch captured ${created.length}/${entries.length} entries (${failed.length} failed, ${committed.length} committed, ${proposed.length} proposed, ${drafts.length} draft).` : `Batch captured ${created.length} entries successfully (${committed.length} committed, ${proposed.length} proposed, ${drafts.length} draft).`;
3016
+ const skippedNote = skippedLowConfidence.length > 0 ? `, ${skippedLowConfidence.length} skipped (low confidence)` : "";
3017
+ const classifiedNote = classifiedCount > 0 ? `, ${classifiedCount} auto-classified` : "";
3018
+ const summary = failed.length > 0 || skippedLowConfidence.length > 0 ? `Batch captured ${created.length}/${entries.length} entries (${failed.length} failed${skippedNote}, ${committed.length} committed, ${proposed.length} proposed, ${drafts.length} draft${classifiedNote}).` : `Batch captured ${created.length} entries successfully (${committed.length} committed, ${proposed.length} proposed, ${drafts.length} draft${classifiedNote}).`;
2877
3019
  const firstDraft = drafts[0];
2878
- const next = created.length > 0 ? [
2879
- { tool: "graph", description: "Discover connections", parameters: { action: "suggest", entryId: created[0].entryId } },
2880
- ...firstDraft ? [{ tool: "commit-entry", description: "Commit first draft", parameters: { entryId: firstDraft.entryId } }] : []
2881
- ] : [];
3020
+ const next = [];
3021
+ if (created.length > 0) {
3022
+ next.push({ tool: "graph", description: "Discover connections", parameters: { action: "suggest", entryId: created[0].entryId } });
3023
+ }
3024
+ if (firstDraft) {
3025
+ next.push({ tool: "commit-entry", description: "Commit first draft", parameters: { entryId: firstDraft.entryId } });
3026
+ }
3027
+ if (skippedLowConfidence.length > 0) {
3028
+ next.push({ tool: "capture", description: `Capture skipped entry with explicit collection`, parameters: { name: skippedLowConfidence[0].name, collection: skippedLowConfidence[0].suggestedCollection ?? "" } });
3029
+ }
2882
3030
  return {
2883
3031
  content: [{ type: "text", text: lines.join("\n") }],
2884
3032
  structuredContent: success(
@@ -2888,14 +3036,27 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2888
3036
  entryId: r.entryId,
2889
3037
  collection: r.collection,
2890
3038
  name: r.name,
2891
- status: r.status
3039
+ status: r.status,
3040
+ ...r.classifiedBy ? { classifiedBy: r.classifiedBy } : {},
3041
+ ...r.confidence != null ? { confidence: r.confidence } : {},
3042
+ ...r.confidenceTier ? { confidenceTier: r.confidenceTier } : {}
2892
3043
  })),
2893
3044
  total: created.length,
2894
3045
  failed: failed.length,
2895
3046
  committed: committed.length,
2896
3047
  proposed: proposed.length,
2897
3048
  drafts: drafts.length,
3049
+ ...classifiedCount > 0 ? { classified: classifiedCount } : {},
2898
3050
  autoCommitApplied,
3051
+ ...skippedLowConfidence.length > 0 && {
3052
+ skippedLowConfidence: skippedLowConfidence.map((s) => ({
3053
+ index: s.index,
3054
+ name: s.name,
3055
+ ...s.suggestedCollection ? { suggestedCollection: s.suggestedCollection } : {},
3056
+ ...s.confidence != null ? { confidence: s.confidence } : {},
3057
+ ...s.alternatives?.length ? { alternatives: s.alternatives } : {}
3058
+ }))
3059
+ },
2899
3060
  ...failed.length > 0 && {
2900
3061
  failedEntries: failed.map((r) => ({
2901
3062
  index: results.indexOf(r),
@@ -3146,7 +3307,7 @@ export {
3146
3307
  CLASSIFIABLE_COLLECTIONS,
3147
3308
  classifyCollection,
3148
3309
  isClassificationAmbiguous,
3149
- STARTER_COLLECTIONS,
3310
+ resolveCollection,
3150
3311
  success,
3151
3312
  failure,
3152
3313
  parseOrFail,
@@ -3156,6 +3317,7 @@ export {
3156
3317
  notFoundResult,
3157
3318
  validationResult,
3158
3319
  withEnvelope,
3320
+ STARTER_COLLECTIONS,
3159
3321
  inferStrategyCategory,
3160
3322
  formatQualityReport,
3161
3323
  checkEntryQuality,
@@ -3169,4 +3331,4 @@ export {
3169
3331
  formatRubricCoaching,
3170
3332
  formatRubricVerdictSection
3171
3333
  };
3172
- //# sourceMappingURL=chunk-UWILSX73.js.map
3334
+ //# sourceMappingURL=chunk-TH5AUVVM.js.map