@productbrain/mcp 0.0.1-beta.7807 → 0.0.1-beta.7814

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.
@@ -400,6 +400,15 @@ function formatOwnerLine(owner, options = {}) {
400
400
  }
401
401
  }
402
402
 
403
+ // src/lib/proseBody.ts
404
+ function readEntryBody(data, proseKey) {
405
+ const body = data?.body;
406
+ if (typeof body === "string" && body.trim().length > 0) return body;
407
+ if (!data || !proseKey) return void 0;
408
+ const legacy = data[proseKey];
409
+ return typeof legacy === "string" ? legacy : void 0;
410
+ }
411
+
403
412
  // src/tools/knowledge-helpers.ts
404
413
  var EPISTEMIC_COLLECTIONS = /* @__PURE__ */ new Set(["insights", "assumptions"]);
405
414
  function deriveEpistemicStatus(entry) {
@@ -481,8 +490,8 @@ function formatOwnerLine2(owner) {
481
490
  function extractPreview(data, maxLen) {
482
491
  if (!data || typeof data !== "object") return "";
483
492
  const d = data;
484
- const raw = d.description ?? d.canonical ?? d.detail ?? d.rule ?? "";
485
- if (typeof raw !== "string" || !raw) return "";
493
+ const raw = readEntryBody(d, "description") ?? readEntryBody(d, "canonical") ?? readEntryBody(d, "detail") ?? readEntryBody(d, "rule") ?? "";
494
+ if (!raw) return "";
486
495
  return raw.length > maxLen ? raw.substring(0, maxLen) + "..." : raw;
487
496
  }
488
497
  var TOOL_NAME_MIGRATIONS = /* @__PURE__ */ new Map([
@@ -2731,7 +2740,7 @@ async function runContradictionCheck(name, description, excludeEntryId, excludeD
2731
2740
  return true;
2732
2741
  }).slice(0, 5);
2733
2742
  for (const entry of allGov) {
2734
- const entryTokens = new Set(tokenizeText(`${entry.name} ${entry.data?.description ?? ""}`));
2743
+ const entryTokens = new Set(tokenizeText(`${entry.name} ${readEntryBody(entry.data, "description") ?? ""}`));
2735
2744
  const matched = keyTerms.filter((t) => entryTokens.has(t));
2736
2745
  if (matched.length < 3) continue;
2737
2746
  let governsCount = 0;
@@ -2994,7 +3003,7 @@ async function handleCommitEntry({ entryId, preview, steeringOverrideReason, coh
2994
3003
  if (!entry) {
2995
3004
  return notFoundResult(entryId, `Entry '${entryId}' not found. Try search to find the right ID.`);
2996
3005
  }
2997
- const descField = entry.data?.description ?? entry.data?.why ?? entry.data?.rationale ?? entry.data?.canonical ?? "";
3006
+ const descField = readEntryBody(entry.data, "description") ?? readEntryBody(entry.data, "why") ?? readEntryBody(entry.data, "rationale") ?? readEntryBody(entry.data, "canonical") ?? "";
2998
3007
  const preflight = await runConflictPreflight(
2999
3008
  entry.name,
3000
3009
  descField,
@@ -14251,6 +14260,42 @@ async function buildOrientationResourceText(kernelQuery2, agentSessionId) {
14251
14260
  return buildOrientationMarkdown(collections, standards, businessRules, principles, directionLines, ruleLines);
14252
14261
  }
14253
14262
 
14263
+ // src/resources/terminologyResource.ts
14264
+ async function buildTerminologyResourceText() {
14265
+ const [glossaryResult, standardsResult] = await Promise.allSettled([
14266
+ kernelQuery("chain.listEntries", { collectionSlug: "glossary" }),
14267
+ kernelQuery("chain.listEntries", { collectionSlug: "standards" })
14268
+ ]);
14269
+ const lines = ["# Product Brain \u2014 Terminology"];
14270
+ if (glossaryResult.status === "fulfilled") {
14271
+ const glossary = glossaryResult.value ?? [];
14272
+ if (glossary.length > 0) {
14273
+ const terms = glossary.map((t) => `- **${t.name}** (${t.entryId ?? "\u2014"}) [${t.status}]: ${readEntryBody(t.data, "canonical") ?? readEntryBody(t.data, "description") ?? ""}`).join("\n");
14274
+ lines.push(`## Glossary (${glossary.length} terms)
14275
+
14276
+ ${terms}`);
14277
+ } else {
14278
+ lines.push("## Glossary\n\nNo glossary terms yet. Use `capture` with collection `glossary` to add terms.");
14279
+ }
14280
+ } else {
14281
+ lines.push("## Glossary\n\nCould not load glossary \u2014 use `entries action=list collection=glossary` to browse manually.");
14282
+ }
14283
+ if (standardsResult.status === "fulfilled") {
14284
+ const standards = standardsResult.value ?? [];
14285
+ if (standards.length > 0) {
14286
+ const stds = standards.map((s) => `- **${s.name}** (${s.entryId ?? "\u2014"}) [${s.status}]: ${readEntryBody(s.data, "description") ?? ""}`).join("\n");
14287
+ lines.push(`## Standards (${standards.length} entries)
14288
+
14289
+ ${stds}`);
14290
+ } else {
14291
+ lines.push("## Standards\n\nNo standards yet. Use `capture` with collection `standards` to add standards.");
14292
+ }
14293
+ } else {
14294
+ lines.push("## Standards\n\nCould not load standards \u2014 use `entries action=list collection=standards` to browse manually.");
14295
+ }
14296
+ return lines.join("\n\n---\n\n");
14297
+ }
14298
+
14254
14299
  // src/resources/index.ts
14255
14300
  var MODULE_DIR = dirname(fileURLToPath(import.meta.url));
14256
14301
  var UI_VIEW_BASE_CANDIDATES = [
@@ -14294,42 +14339,9 @@ function registerResources(server) {
14294
14339
  server.resource(
14295
14340
  "chain-terminology",
14296
14341
  "productbrain://terminology",
14297
- async (uri) => {
14298
- const [glossaryResult, standardsResult] = await Promise.allSettled([
14299
- kernelQuery("chain.listEntries", { collectionSlug: "glossary" }),
14300
- kernelQuery("chain.listEntries", { collectionSlug: "standards" })
14301
- ]);
14302
- const lines = ["# Product Brain \u2014 Terminology"];
14303
- if (glossaryResult.status === "fulfilled") {
14304
- const glossary = glossaryResult.value ?? [];
14305
- if (glossary.length > 0) {
14306
- const terms = glossary.map((t) => `- **${t.name}** (${t.entryId ?? "\u2014"}) [${t.status}]: ${t.data?.canonical ?? t.data?.description ?? ""}`).join("\n");
14307
- lines.push(`## Glossary (${glossary.length} terms)
14308
-
14309
- ${terms}`);
14310
- } else {
14311
- lines.push("## Glossary\n\nNo glossary terms yet. Use `capture` with collection `glossary` to add terms.");
14312
- }
14313
- } else {
14314
- lines.push("## Glossary\n\nCould not load glossary \u2014 use `entries action=list collection=glossary` to browse manually.");
14315
- }
14316
- if (standardsResult.status === "fulfilled") {
14317
- const standards = standardsResult.value ?? [];
14318
- if (standards.length > 0) {
14319
- const stds = standards.map((s) => `- **${s.name}** (${s.entryId ?? "\u2014"}) [${s.status}]: ${s.data?.description ?? ""}`).join("\n");
14320
- lines.push(`## Standards (${standards.length} entries)
14321
-
14322
- ${stds}`);
14323
- } else {
14324
- lines.push("## Standards\n\nNo standards yet. Use `capture` with collection `standards` to add standards.");
14325
- }
14326
- } else {
14327
- lines.push("## Standards\n\nCould not load standards \u2014 use `entries action=list collection=standards` to browse manually.");
14328
- }
14329
- return {
14330
- contents: [{ uri: uri.href, text: lines.join("\n\n---\n\n"), mimeType: "text/markdown" }]
14331
- };
14332
- }
14342
+ async (uri) => ({
14343
+ contents: [{ uri: uri.href, text: await buildTerminologyResourceText(), mimeType: "text/markdown" }]
14344
+ })
14333
14345
  );
14334
14346
  server.resource(
14335
14347
  "chain-collections",
@@ -14610,43 +14622,38 @@ ${entry.labels.map((l) => `- ${l.name ?? l.slug}`).join("\n")}`);
14610
14622
 
14611
14623
  // src/prompts/index.ts
14612
14624
  import { z as z34 } from "zod/v3";
14613
- function registerPrompts(server) {
14614
- server.prompt(
14615
- "review-against-rules",
14616
- "Review code or a design decision against all business rules for a given domain. Fetches the rules and asks you to do a structured compliance review.",
14617
- { domain: z34.string().describe("Business rule domain (e.g. 'Identity & Access', 'Governance & Decision-Making')") },
14618
- async ({ domain }) => {
14619
- const entries = await kernelQuery("chain.listEntries", { collectionSlug: "business-rules" });
14620
- const rules = entries.filter((e) => e.data?.domain === domain);
14621
- if (rules.length === 0) {
14622
- return {
14623
- messages: [
14624
- {
14625
- role: "user",
14626
- content: {
14627
- type: "text",
14628
- text: `No business rules found for domain "${domain}". Use \`entries action=list collection=business-rules\` to see available domains.`
14629
- }
14630
- }
14631
- ]
14632
- };
14633
- }
14634
- const rulesText = rules.map(
14635
- (r) => `### ${r.entryId ?? ""}: ${r.name}
14625
+ async function buildReviewAgainstRulesPrompt(domain) {
14626
+ const entries = await kernelQuery("chain.listEntries", { collectionSlug: "business-rules" });
14627
+ const rules = entries.filter((e) => e.data?.domain === domain);
14628
+ if (rules.length === 0) {
14629
+ return {
14630
+ messages: [
14631
+ {
14632
+ role: "user",
14633
+ content: {
14634
+ type: "text",
14635
+ text: `No business rules found for domain "${domain}". Use \`entries action=list collection=business-rules\` to see available domains.`
14636
+ }
14637
+ }
14638
+ ]
14639
+ };
14640
+ }
14641
+ const rulesText = rules.map(
14642
+ (r) => `### ${r.entryId ?? ""}: ${r.name}
14636
14643
  Status: ${r.status} | Severity: ${r.data?.severity ?? "unknown"}
14637
- Description: ${r.data?.description ?? ""}
14644
+ Description: ${readEntryBody(r.data, "description") ?? ""}
14638
14645
  Data Impact: ${r.data?.dataImpact ?? ""}
14639
14646
  Platforms: ${(r.data?.platforms ?? []).join(", ")}
14640
14647
  ` + (r.data?.conflictWith ? `CONFLICT: ${r.data.conflictWith.rule} \u2014 ${r.data.conflictWith.nature}
14641
14648
  ` : "")
14642
- ).join("\n---\n\n");
14643
- return {
14644
- messages: [
14645
- {
14646
- role: "user",
14647
- content: {
14648
- type: "text",
14649
- text: `Review the current code or design against the following business rules for the "${domain}" domain.
14649
+ ).join("\n---\n\n");
14650
+ return {
14651
+ messages: [
14652
+ {
14653
+ role: "user",
14654
+ content: {
14655
+ type: "text",
14656
+ text: `Review the current code or design against the following business rules for the "${domain}" domain.
14650
14657
 
14651
14658
  For each rule, assess:
14652
14659
  1. Is the current implementation compliant?
@@ -14658,29 +14665,24 @@ Business Rules:
14658
14665
  ${rulesText}
14659
14666
 
14660
14667
  Provide a structured review with a compliance status for each rule (COMPLIANT / AT RISK / VIOLATION / NOT APPLICABLE).`
14661
- }
14662
- }
14663
- ]
14664
- };
14665
- }
14666
- );
14667
- server.prompt(
14668
- "name-check",
14669
- "Check variable names, field names, or API names against the glossary for terminology alignment. Flags drift from canonical terms.",
14670
- { names: z34.string().describe("Comma-separated list of names to check (e.g. 'vendor_id, compliance_level, formulator_type')") },
14671
- async ({ names }) => {
14672
- const terms = await kernelQuery("chain.listEntries", { collectionSlug: "glossary" });
14673
- const glossaryContext = terms.map(
14674
- (t) => `${t.name} (${t.entryId ?? ""}) [${t.status}]: ${t.data?.canonical ?? ""}` + (t.data?.confusedWith?.length > 0 ? ` \u2014 Often confused with: ${t.data.confusedWith.join(", ")}` : "") + (t.data?.codeMapping?.length > 0 ? `
14668
+ }
14669
+ }
14670
+ ]
14671
+ };
14672
+ }
14673
+ async function buildNameCheckPrompt(names) {
14674
+ const terms = await kernelQuery("chain.listEntries", { collectionSlug: "glossary" });
14675
+ const glossaryContext = terms.map(
14676
+ (t) => `${t.name} (${t.entryId ?? ""}) [${t.status}]: ${readEntryBody(t.data, "canonical") ?? ""}` + (t.data?.confusedWith?.length > 0 ? ` \u2014 Often confused with: ${t.data.confusedWith.join(", ")}` : "") + (t.data?.codeMapping?.length > 0 ? `
14675
14677
  Code mappings: ${t.data.codeMapping.map((m) => `${m.platform}:${m.field}`).join(", ")}` : "")
14676
- ).join("\n");
14677
- return {
14678
- messages: [
14679
- {
14680
- role: "user",
14681
- content: {
14682
- type: "text",
14683
- text: `Check the following names against the glossary for terminology alignment:
14678
+ ).join("\n");
14679
+ return {
14680
+ messages: [
14681
+ {
14682
+ role: "user",
14683
+ content: {
14684
+ type: "text",
14685
+ text: `Check the following names against the glossary for terminology alignment:
14684
14686
 
14685
14687
  Names to check: ${names}
14686
14688
 
@@ -14694,11 +14696,23 @@ For each name:
14694
14696
  4. Flag any names that don't have a corresponding glossary term (might need one).
14695
14697
 
14696
14698
  Format as a table: Name | Status | Canonical Form | Action Needed`
14697
- }
14698
- }
14699
- ]
14700
- };
14701
- }
14699
+ }
14700
+ }
14701
+ ]
14702
+ };
14703
+ }
14704
+ function registerPrompts(server) {
14705
+ server.prompt(
14706
+ "review-against-rules",
14707
+ "Review code or a design decision against all business rules for a given domain. Fetches the rules and asks you to do a structured compliance review.",
14708
+ { domain: z34.string().describe("Business rule domain (e.g. 'Identity & Access', 'Governance & Decision-Making')") },
14709
+ async ({ domain }) => buildReviewAgainstRulesPrompt(domain)
14710
+ );
14711
+ server.prompt(
14712
+ "name-check",
14713
+ "Check variable names, field names, or API names against the glossary for terminology alignment. Flags drift from canonical terms.",
14714
+ { names: z34.string().describe("Comma-separated list of names to check (e.g. 'vendor_id, compliance_level, formulator_type')") },
14715
+ async ({ names }) => buildNameCheckPrompt(names)
14702
14716
  );
14703
14717
  server.prompt(
14704
14718
  "draft-decision-record",
@@ -14744,17 +14758,20 @@ After drafting, I can log it using the capture tool with collection "decisions".
14744
14758
  observation: z34.string().describe("What you observed or discovered (e.g. 'Suppliers can have multiple org types in Gateway')"),
14745
14759
  domain: z34.string().describe("Which domain this rule belongs to (e.g. 'Governance & Decision-Making')")
14746
14760
  },
14747
- async ({ observation, domain }) => {
14748
- const allRules = await kernelQuery("chain.listEntries", { collectionSlug: "business-rules" });
14749
- const existingRules = allRules.filter((r) => r.data?.domain === domain);
14750
- const existingContext = existingRules.length > 0 ? existingRules.map((r) => `${r.entryId ?? ""}: ${r.name} [${r.status}] \u2014 ${r.data?.description ?? ""}`).join("\n") : "No existing rules for this domain.";
14751
- return {
14752
- messages: [
14753
- {
14754
- role: "user",
14755
- content: {
14756
- type: "text",
14757
- text: `Draft a business rule based on this observation:
14761
+ async ({ observation, domain }) => buildDraftRuleFromContextPrompt(observation, domain)
14762
+ );
14763
+ }
14764
+ async function buildDraftRuleFromContextPrompt(observation, domain) {
14765
+ const allRules = await kernelQuery("chain.listEntries", { collectionSlug: "business-rules" });
14766
+ const existingRules = allRules.filter((r) => r.data?.domain === domain);
14767
+ const existingContext = existingRules.length > 0 ? existingRules.map((r) => `${r.entryId ?? ""}: ${r.name} [${r.status}] \u2014 ${readEntryBody(r.data, "description") ?? ""}`).join("\n") : "No existing rules for this domain.";
14768
+ return {
14769
+ messages: [
14770
+ {
14771
+ role: "user",
14772
+ content: {
14773
+ type: "text",
14774
+ text: `Draft a business rule based on this observation:
14758
14775
 
14759
14776
  "${observation}"
14760
14777
 
@@ -14773,12 +14790,10 @@ Draft the rule with these fields (do not specify entryId \u2014 Convex assigns s
14773
14790
  7. **data.relatedRules**: Any related existing rules
14774
14791
 
14775
14792
  Make sure the rule is consistent with existing rules and doesn't contradict them. After drafting, I can create it using the capture tool with collection "business-rules".`
14776
- }
14777
- }
14778
- ]
14779
- };
14780
- }
14781
- );
14793
+ }
14794
+ }
14795
+ ]
14796
+ };
14782
14797
  }
14783
14798
 
14784
14799
  // src/server.ts
@@ -14901,4 +14916,4 @@ export {
14901
14916
  createProductBrainServer,
14902
14917
  initFeatureFlags
14903
14918
  };
14904
- //# sourceMappingURL=chunk-NOEYON3B.js.map
14919
+ //# sourceMappingURL=chunk-5HVA3T22.js.map