@productbrain/mcp 0.0.1-beta.142 → 0.0.1-beta.147

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.
@@ -996,6 +996,34 @@ var FIELD_TYPE_DEFAULTS = {
996
996
  "date": null
997
997
  };
998
998
 
999
+ // src/lib/collectionCache.ts
1000
+ var cachedCollections = null;
1001
+ var cachedBySlug = null;
1002
+ async function getCollections() {
1003
+ if (cachedCollections !== null) return cachedCollections;
1004
+ const result = await mcpQuery("chain.listCollections");
1005
+ cachedCollections = result ?? [];
1006
+ cachedBySlug = new Map(cachedCollections.map((c) => [c.slug, c]));
1007
+ return cachedCollections;
1008
+ }
1009
+ async function getCollectionBySlug(slug) {
1010
+ await getCollections();
1011
+ return cachedBySlug.get(slug);
1012
+ }
1013
+ async function isGoverned(slug) {
1014
+ const col = await getCollectionBySlug(slug);
1015
+ return !!col?.governed;
1016
+ }
1017
+ async function getByNavGroup(navGroup) {
1018
+ const cols = await getCollections();
1019
+ return cols.filter((c) => c.navGroup === navGroup);
1020
+ }
1021
+ async function isWorkPlanningCollection(slug) {
1022
+ const col = await getCollectionBySlug(slug);
1023
+ const layer = col?.thinkingLayer;
1024
+ return layer === "delivery-container" || layer === "delivery-grouping" || layer === "delivery-work";
1025
+ }
1026
+
999
1027
  // src/tools/smart-capture.ts
1000
1028
  function normalizeMatchText(value) {
1001
1029
  return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").replace(/\s+/g, " ").trim();
@@ -1123,7 +1151,7 @@ function inferStrategyCategory(name, description) {
1123
1151
  if (bestScore === 0) return "strategy";
1124
1152
  return null;
1125
1153
  }
1126
- var PROFILES = /* @__PURE__ */ new Map([
1154
+ var PROFILE_ENRICHMENTS = /* @__PURE__ */ new Map([
1127
1155
  ["tensions", {
1128
1156
  governedDraft: false,
1129
1157
  descriptionField: "description",
@@ -1515,10 +1543,77 @@ var FALLBACK_PROFILE = {
1515
1543
  COMMON_CHECKS.hasType
1516
1544
  ]
1517
1545
  };
1546
+ async function buildRuntimeProfile(slug) {
1547
+ const col = await getCollectionBySlug(slug);
1548
+ const enrichment = PROFILE_ENRICHMENTS.get(slug);
1549
+ if (!col && !enrichment) return FALLBACK_PROFILE;
1550
+ const governedDraft = col?.governed ?? enrichment?.governedDraft ?? false;
1551
+ let descriptionField = enrichment?.descriptionField ?? "description";
1552
+ if (!enrichment?.descriptionField && col) {
1553
+ const bodyField = col.fields.find(
1554
+ (f) => f.zone === "body" || f.displayHint === "textarea"
1555
+ );
1556
+ if (bodyField) descriptionField = bodyField.key;
1557
+ }
1558
+ let qualityChecks = [
1559
+ COMMON_CHECKS.clearName,
1560
+ COMMON_CHECKS.hasDescription,
1561
+ COMMON_CHECKS.hasRelations,
1562
+ COMMON_CHECKS.hasType
1563
+ ];
1564
+ if (enrichment?.qualityChecks) {
1565
+ qualityChecks = enrichment.qualityChecks;
1566
+ } else if (col?.qualityCriteria?.length) {
1567
+ const dbChecks = col.qualityCriteria.map((qc) => ({
1568
+ id: `db-${qc.field}-${qc.rule}`,
1569
+ label: `${qc.field} ${qc.rule === "required" ? "provided" : qc.rule === "min_length" ? `min ${qc.value} chars` : `matches ${qc.value}`}`,
1570
+ check: (ctx) => {
1571
+ const val = ctx.data[qc.field];
1572
+ if (qc.rule === "required") return val !== void 0 && val !== null && val !== "";
1573
+ if (qc.rule === "min_length") return typeof val === "string" && val.length >= Number(qc.value ?? 0);
1574
+ if (qc.rule === "pattern") return typeof val === "string" && new RegExp(qc.value ?? "").test(val);
1575
+ return true;
1576
+ },
1577
+ suggestion: () => `Fill in ${qc.field} (${qc.rule}).`
1578
+ }));
1579
+ qualityChecks = [
1580
+ COMMON_CHECKS.clearName,
1581
+ COMMON_CHECKS.hasDescription,
1582
+ ...dbChecks,
1583
+ COMMON_CHECKS.hasRelations,
1584
+ COMMON_CHECKS.hasType
1585
+ ];
1586
+ }
1587
+ return {
1588
+ governedDraft,
1589
+ descriptionField,
1590
+ defaults: enrichment?.defaults ?? [],
1591
+ recommendedRelationTypes: enrichment?.recommendedRelationTypes ?? FALLBACK_PROFILE.recommendedRelationTypes,
1592
+ qualityChecks,
1593
+ inferField: enrichment?.inferField
1594
+ };
1595
+ }
1596
+ var profileCache = null;
1597
+ async function getProfile(slug) {
1598
+ if (!profileCache) profileCache = /* @__PURE__ */ new Map();
1599
+ const cached = profileCache.get(slug);
1600
+ if (cached) return cached;
1601
+ const profile = await buildRuntimeProfile(slug);
1602
+ profileCache.set(slug, profile);
1603
+ return profile;
1604
+ }
1518
1605
  function extractSearchTerms(name, description) {
1519
1606
  const text = `${name} ${description}`;
1520
1607
  return text.replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length > 3).slice(0, 8).join(" ");
1521
1608
  }
1609
+ var hubCollectionCache = null;
1610
+ async function populateHubCache() {
1611
+ if (hubCollectionCache !== null) return;
1612
+ const cols = await getCollections();
1613
+ hubCollectionCache = new Set(
1614
+ cols.filter((c) => c.governanceRole === "steering" || c.thinkingLayer === "strategy").map((c) => c.slug)
1615
+ );
1616
+ }
1522
1617
  function computeLinkConfidence(candidate, sourceName, sourceDescription, sourceCollection, candidateCollection) {
1523
1618
  const text = `${sourceName} ${sourceDescription}`.toLowerCase();
1524
1619
  const candidateName = candidate.name.toLowerCase();
@@ -1535,8 +1630,7 @@ function computeLinkConfidence(candidate, sourceName, sourceDescription, sourceC
1535
1630
  if (matchingWords.length > 0) {
1536
1631
  reasons.push(`word overlap (${matchingWords.slice(0, 3).join(", ")})`);
1537
1632
  }
1538
- const HUB_COLLECTIONS = /* @__PURE__ */ new Set(["strategy", "features"]);
1539
- if (HUB_COLLECTIONS.has(candidateCollection)) {
1633
+ if (hubCollectionCache?.has(candidateCollection)) {
1540
1634
  score += 15;
1541
1635
  reasons.push("hub collection");
1542
1636
  }
@@ -1614,11 +1708,11 @@ async function checkEntryQuality(entryId) {
1614
1708
  quality: { score: 0, maxScore: 10, checks: [] }
1615
1709
  };
1616
1710
  }
1617
- const collections = await mcpQuery("chain.listCollections");
1711
+ const collections = await getCollections();
1618
1712
  const collMap = /* @__PURE__ */ new Map();
1619
1713
  for (const c of collections) collMap.set(c._id, c.slug);
1620
1714
  const collectionSlug = collMap.get(entry.collectionId) ?? "unknown";
1621
- const profile = PROFILES.get(collectionSlug) ?? FALLBACK_PROFILE;
1715
+ const profile = await getProfile(collectionSlug);
1622
1716
  const relations = await mcpQuery("chain.listEntryRelations", { entryId });
1623
1717
  const linksCreated = [];
1624
1718
  for (const r of relations) {
@@ -1659,15 +1753,6 @@ async function checkEntryQuality(entryId) {
1659
1753
  }
1660
1754
  return { text: lines.join("\n"), quality };
1661
1755
  }
1662
- var GOVERNED_COLLECTIONS = /* @__PURE__ */ new Set([
1663
- "glossary",
1664
- "business-rules",
1665
- "principles",
1666
- "standards",
1667
- "strategy",
1668
- "features",
1669
- "architecture"
1670
- ]);
1671
1756
  var AUTO_LINK_CONFIDENCE_THRESHOLD = 35;
1672
1757
  var MAX_AUTO_LINKS = 5;
1673
1758
  var MAX_SUGGESTIONS = 5;
@@ -2109,7 +2194,7 @@ function registerSmartCaptureTools(server) {
2109
2194
  )
2110
2195
  };
2111
2196
  }
2112
- const profile = PROFILES.get(resolvedCollection) ?? FALLBACK_PROFILE;
2197
+ const profile = await getProfile(resolvedCollection);
2113
2198
  const col = await mcpQuery("chain.getCollection", { slug: resolvedCollection });
2114
2199
  if (!col) {
2115
2200
  const displayName = resolvedCollection.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
@@ -2172,7 +2257,8 @@ Or use \`collections action=list\` to see available collections.`
2172
2257
  }
2173
2258
  }
2174
2259
  data[profile.descriptionField || "description"] = description;
2175
- const isBetCapture = canonicalKey === "work_package" || resolvedCollection === "work-packages";
2260
+ const colMeta = await getCollectionBySlug(resolvedCollection);
2261
+ const isBetCapture = canonicalKey === "work_package" || colMeta?.defaultCanonicalKey === "work_package";
2176
2262
  let entryWarnings = [];
2177
2263
  const shouldDecompose = (resolvedCollection === "strategy" || isBetCapture) && description.trim().length > 0;
2178
2264
  if (shouldDecompose) {
@@ -2260,9 +2346,10 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2260
2346
  const skipAutoDiscovery = links && links.length > 0;
2261
2347
  const searchQuery = extractSearchTerms(name, description);
2262
2348
  if (searchQuery && !skipAutoDiscovery) {
2349
+ await populateHubCache();
2263
2350
  const [searchResults, allCollections] = await Promise.all([
2264
2351
  mcpQuery("chain.searchEntries", { query: searchQuery }),
2265
- mcpQuery("chain.listCollections")
2352
+ getCollections()
2266
2353
  ]);
2267
2354
  const collMap = /* @__PURE__ */ new Map();
2268
2355
  for (const c of allCollections) collMap.set(c._id, c.slug);
@@ -2506,7 +2593,7 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2506
2593
  } else {
2507
2594
  lines.push(`**${name}** promoted to SSOT on the Chain.`);
2508
2595
  }
2509
- if (GOVERNED_COLLECTIONS.has(resolvedCollection)) {
2596
+ if (await isGoverned(resolvedCollection)) {
2510
2597
  lines.push(`_Note: \`${resolvedCollection}\` is a governed collection \u2014 ensure this entry has been reviewed._`);
2511
2598
  }
2512
2599
  } else if (finalStatus === "proposed") {
@@ -2742,7 +2829,8 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2742
2829
  data: `Batch capturing ${entries.length} entries...`,
2743
2830
  logger: "product-brain"
2744
2831
  });
2745
- const allCollections = await mcpQuery("chain.listCollections");
2832
+ const allCollections = await getCollections();
2833
+ await populateHubCache();
2746
2834
  const collCache = /* @__PURE__ */ new Map();
2747
2835
  for (const c of allCollections) collCache.set(c.slug, c);
2748
2836
  const collIdToSlug = /* @__PURE__ */ new Map();
@@ -2779,7 +2867,7 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2779
2867
  confidence = resolved.confidence;
2780
2868
  confidenceTier = resolved.tier;
2781
2869
  }
2782
- const profile = PROFILES.get(resolvedSlug) ?? FALLBACK_PROFILE;
2870
+ const profile = await getProfile(resolvedSlug);
2783
2871
  const col = collCache.get(resolvedSlug);
2784
2872
  if (!col) {
2785
2873
  results.push({
@@ -10141,7 +10229,8 @@ function buildPlannedWork(allEntries) {
10141
10229
  if (entry.status === "draft") {
10142
10230
  result.uncommittedDrafts.push({ name: entry.name, collection });
10143
10231
  }
10144
- if (collection === "tensions" && entry.workflowStatus === "open") {
10232
+ const isTension = entry.canonicalKey === "tension" || collection === "tensions";
10233
+ if (isTension && entry.workflowStatus === "open") {
10145
10234
  result.openTensions.push({ name: entry.name, entryId: entry.entryId ?? entry._id });
10146
10235
  }
10147
10236
  if (entry.workflowStatus === "in-progress") {
@@ -10339,7 +10428,7 @@ function buildDocCompletenessSection(collections) {
10339
10428
  const lines = renderDocCompletenessLines(report);
10340
10429
  return { lines, errorCount: report.summary.errors, warningCount: report.summary.warnings };
10341
10430
  }
10342
- function runAlignmentCheck(task, activeBets, taskContextHits) {
10431
+ async function runAlignmentCheck(task, activeBets, taskContextHits) {
10343
10432
  const betNames = activeBets.map((b) => b.name);
10344
10433
  const taskWords = extractKeywords(task);
10345
10434
  const matchingBet = activeBets.find((b) => {
@@ -10349,9 +10438,12 @@ function runAlignmentCheck(task, activeBets, taskContextHits) {
10349
10438
  if (matchingBet) {
10350
10439
  return { aligned: true, matchedBet: matchingBet.name, matchSource: "active_bet", betNames };
10351
10440
  }
10352
- const betHits = (taskContextHits ?? []).filter(
10353
- (e) => e.collectionSlug === "chains" || e.collectionSlug === "work-packages"
10354
- );
10441
+ const betHits = [];
10442
+ for (const e of taskContextHits ?? []) {
10443
+ if (e.collectionSlug && await isWorkPlanningCollection(e.collectionSlug)) {
10444
+ betHits.push(e);
10445
+ }
10446
+ }
10355
10447
  if (betHits.length > 0) {
10356
10448
  return { aligned: true, matchedBet: betHits[0]?.name ?? null, matchSource: "task_context", betNames };
10357
10449
  }
@@ -11111,12 +11203,16 @@ async function buildOrientResponse(wsCtx, agentSessionId, errors, task) {
11111
11203
  let wsStandards = [];
11112
11204
  let wsBusinessRules = [];
11113
11205
  try {
11114
- for (const slug of ["principles", "standards", "business-rules"]) {
11206
+ const govCollections = await getByNavGroup("governance");
11207
+ const govSlugs = govCollections.map((c) => c.slug);
11208
+ for (const slug of govSlugs) {
11115
11209
  const entries = await mcpQuery("chain.listEntries", { collectionSlug: slug });
11116
11210
  const active = (entries ?? []).filter((e) => e.status === "active");
11117
- if (slug === "principles") wsPrinciples = active;
11118
- if (slug === "standards") wsStandards = active;
11119
- if (slug === "business-rules") wsBusinessRules = active;
11211
+ const colDef = govCollections.find((c) => c.slug === slug);
11212
+ const ck = colDef?.defaultCanonicalKey ?? slug;
11213
+ if (ck === "principle" || slug === "principles") wsPrinciples = [...wsPrinciples, ...active];
11214
+ else if (ck === "standard" || slug === "standards") wsStandards = [...wsStandards, ...active];
11215
+ else if (ck === "business_rule" || slug === "business-rules") wsBusinessRules = [...wsBusinessRules, ...active];
11120
11216
  }
11121
11217
  } catch {
11122
11218
  }
@@ -13387,7 +13483,7 @@ function registerOrientTool(server) {
13387
13483
  ));
13388
13484
  }
13389
13485
  if (orientEntries) {
13390
- const result = runAlignmentCheck(
13486
+ const result = await runAlignmentCheck(
13391
13487
  task,
13392
13488
  orientEntries.activeBets ?? [],
13393
13489
  orientEntries.taskContext?.context
@@ -15130,4 +15226,4 @@ export {
15130
15226
  SERVER_VERSION,
15131
15227
  createProductBrainServer
15132
15228
  };
15133
- //# sourceMappingURL=chunk-ZBUYFPHG.js.map
15229
+ //# sourceMappingURL=chunk-TTZKY2OH.js.map