@productbrain/mcp 0.0.1-beta.11434 → 0.0.1-beta.11437

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.
@@ -729,7 +729,37 @@ function buildCaptureForRealNextAction(payload) {
729
729
  }];
730
730
  }
731
731
 
732
+ // src/lib/spineRungs.ts
733
+ var SPINE_RUNG_SEQUENCE = [
734
+ { relationType: "validates", targetKey: "key_result", gapIfMissing: "key_result_missing" },
735
+ { relationType: "part_of", targetKey: "objective", gapIfMissing: "objective_missing" },
736
+ { relationType: "part_of", targetKey: "strategy", gapIfMissing: "strategy_missing" },
737
+ { relationType: "part_of", targetKey: "vision", gapIfMissing: "vision_missing" },
738
+ { relationType: "part_of", targetKey: "purpose", gapIfMissing: "purpose_missing" }
739
+ ];
740
+
732
741
  // src/lib/checklist.ts
742
+ var FIRST_SPINE_SUBJECT = "work_package";
743
+ var kindWord = (kind) => kind.replace(/_/g, " ");
744
+ var withArticle = (word) => `${/^[aeiou]/.test(word) ? "an" : "a"} ${word}`;
745
+ var spineSubject = (kind) => {
746
+ const words = withArticle(kindWord(kind));
747
+ return words[0].toUpperCase() + words.slice(1);
748
+ };
749
+ function rungLine(rung) {
750
+ const index = SPINE_RUNG_SEQUENCE.findIndex((spec) => spec.targetKey === rung);
751
+ if (index < 0) return null;
752
+ const { relationType: relationType2, targetKey } = SPINE_RUNG_SEQUENCE[index];
753
+ const parent = `an accepted ${kindWord(targetKey)}`;
754
+ const subject = spineSubject(index === 0 ? FIRST_SPINE_SUBJECT : SPINE_RUNG_SEQUENCE[index - 1].targetKey);
755
+ return relationType2 === "part_of" ? `${subject} hangs on ${parent} with ${relationType2}.` : `${subject} ${relationType2} ${parent}.`;
756
+ }
757
+ var SPINE_RUNG_LINES = SPINE_RUNG_SEQUENCE.map((spec) => rungLine(spec.targetKey));
758
+ var SPINE_LINKS_LINES = [
759
+ `Spine links run child to parent, each ${SPINE_RUNG_SEQUENCE[SPINE_RUNG_SEQUENCE.length - 1].relationType} the level above.`,
760
+ `${spineSubject(FIRST_SPINE_SUBJECT)} ${SPINE_RUNG_SEQUENCE[0].relationType} ${withArticle(kindWord(SPINE_RUNG_SEQUENCE[0].targetKey))}; informs joins no levels.`,
761
+ "Before accepting a spine entry, preflight it: it names what blocks."
762
+ ];
733
763
  var CHECKLIST_LINES = {
734
764
  purpose: {
735
765
  stranger_test: "Explain it so a stranger knows why this company exists",
@@ -759,14 +789,8 @@ var CHECKLIST_LINES = {
759
789
  bounded: "Say by when, or how often, the measure is read",
760
790
  moves_the_objective: "Pick a result that shows the objective is happening, not that work was done"
761
791
  },
762
- /** One line per spine rung, keyed by the rung it reaches (kernel/strategy/spineTrace.ts order). */
763
- strategy_link: {
764
- key_result: "Link this work to an accepted key result it validates",
765
- objective: "Put the key result under an accepted objective",
766
- strategy: "Put the objective under an accepted strategy",
767
- vision: "Put the strategy under an accepted vision",
768
- purpose: "Put the vision under an accepted purpose"
769
- }
792
+ /** One line per spine rung, keyed by the rung it reaches: rungLine, in SPINE_RUNG_SEQUENCE order (TASK-1026). */
793
+ strategy_link: Object.fromEntries(SPINE_RUNG_SEQUENCE.map((spec) => [spec.targetKey, rungLine(spec.targetKey)]))
770
794
  };
771
795
  var MARK = { met: "[x]", not_yet: "[ ]", judging: "[~]" };
772
796
  function renderChecklist(canonicalKey, criteria, judging) {
@@ -911,8 +935,8 @@ var RELATION_BATCH_MAX = 25;
911
935
 
912
936
  // src/tools/smart-capture.ts
913
937
  var CAPTURE_LINK_OUTCOMES = ["written", "refused"];
914
- var SPINE_FLOOR_SENTENCE = "kept as a draft; it can be accepted once its spine above is accepted \u2014 the promote door checks the whole spine and names the first missing rung.";
915
938
  var SPINE_FLOOR_EXEMPT_SENTENCE = "kept as a draft, though this workspace is not yet enforcing the spine above it \u2014 it can be accepted as-is.";
939
+ var LINK_TYPE_DESCRIPTION = `Relation type. The spine links: ${SPINE_RUNG_LINES.join(" ")}`;
916
940
  var captureSchema = z5.object({
917
941
  collection: z5.string().optional().describe("Collection slug, e.g. 'tensions', 'rules', 'glossary', 'decisions'. Optional \u2014 the kernel classifies when omitted."),
918
942
  name: z5.string().describe("Display name \u2014 be specific (e.g. 'Convex adjacency list won't scale for graph traversal')"),
@@ -924,7 +948,7 @@ var captureSchema = z5.object({
924
948
  // TASK-380: mirrors whyPolicy.ts
925
949
  links: z5.array(z5.object({
926
950
  to: z5.string().describe("Target entry ID (e.g. '<PREFIX>-<n>')"),
927
- type: z5.string().describe("Relation type (e.g. 'governs', 'related_to', 'informs')")
951
+ type: z5.string().describe(LINK_TYPE_DESCRIPTION)
928
952
  })).optional().describe("Links written with the entry in one transaction; each returns written or refused with a reason; a refused link fails nothing."),
929
953
  sourceRef: z5.string().optional().describe("URI or path of the source document backing this entry (e.g. 'meeting-2026-03-28.md', 'import://batch-5'). Stored as top-level entry field, not in data."),
930
954
  sourceExcerpt: z5.string().optional().describe("Verbatim excerpt from the source that backs this entry's claims. Stored as top-level entry field, not in data."),
@@ -981,7 +1005,7 @@ var captureSuccessOutputSchema = z5.object({
981
1005
  required: z5.boolean().optional()
982
1006
  })).optional(),
983
1007
  // TASK-160: the server's birth-floor verdict must survive the strict output contract.
984
- spineFloor: z5.object({ relationType: z5.string(), parentKind: z5.string(), exempt: z5.boolean() }).optional(),
1008
+ spineFloor: z5.object({ relationType: z5.string(), parentKind: z5.string(), exempt: z5.boolean(), linkWritten: z5.boolean().optional() }).optional(),
985
1009
  // WP-751 M3a-1b (TASK-189, DEC-1700): `coherencyRefusal` and `contradictionAdvisory` are deleted with the
986
1010
  // client-side auto-commit attempt that alone populated them (its gate refusal, the commit's advisory).
987
1011
  replayed: z5.literal(true).optional(),
@@ -1008,7 +1032,7 @@ var captureCompoundSchema = z5.object({
1008
1032
  why: z5.string().max(ENTRY_BODY_MAX_CHARS).optional().describe("Why this entry exists."),
1009
1033
  links: z5.array(z5.object({
1010
1034
  to: z5.string().max(200).describe("Target entry ID (e.g. '<PREFIX>-<n>')"),
1011
- type: z5.string().max(200).describe("Relation type (e.g. 'governs', 'related_to', 'informs')")
1035
+ type: z5.string().max(200).describe(LINK_TYPE_DESCRIPTION)
1012
1036
  })).max(RELATION_BATCH_MAX).optional().describe("Links written with the entry in one transaction; each returns written or refused with a reason; a refused link fails nothing."),
1013
1037
  sourceRef: z5.string().max(2e3).optional().describe("URI or path of the source document backing this entry."),
1014
1038
  sourceExcerpt: z5.string().max(SOURCE_EXCERPT_MAX_CHARS).optional().describe("Verbatim excerpt from the source backing this entry's claims."),
@@ -1260,10 +1284,9 @@ Use \`context action=get\` to inspect the existing entry, or \`entries action=up
1260
1284
  lines.push(`## Accepted: ${finalEntryId}`);
1261
1285
  lines.push(`**${name}** added to your knowledge base.`);
1262
1286
  } else if (spineFloor) {
1263
- lines.push("");
1264
- lines.push("## Saved as draft");
1265
- lines.push(`**${name}** ${spineFloor.exempt ? SPINE_FLOOR_EXEMPT_SENTENCE : SPINE_FLOOR_SENTENCE}`);
1266
- lines.push(`Next: \`entries action=commit entryId="${finalEntryId}"\`.`);
1287
+ const { relationType: relationType2, parentKind, exempt, linkWritten } = spineFloor, id = finalEntryId, to = `<${parentKind.toUpperCase().replace(/_/g, "-")}-ID>`;
1288
+ lines.push("", "## Saved as draft", `**${name}** ${exempt ? SPINE_FLOOR_EXEMPT_SENTENCE : `kept as a draft. ${rungLine(parentKind)}`}`);
1289
+ lines.push(exempt || linkWritten ? `Next: \`entries action=commit entryId="${id}"\`.` : `Next: \`relations action=create from="${id}" type="${relationType2}" to="${to}"\`.`);
1267
1290
  }
1268
1291
  if (groundingCaveat) lines.push("", groundingCaveat);
1269
1292
  const named = groundingReport.duplicates.slice(0, 3).map((d) => `${d.entryId} "${d.name}"`).join(", ");
@@ -1310,7 +1333,7 @@ Use \`context action=get\` to inspect the existing entry, or \`entries action=up
1310
1333
  next.push({ tool: "context", description: "See its connections", parameters: { action: "find", entryId: finalEntryId } });
1311
1334
  } else {
1312
1335
  next.push({ tool: "context", description: "Review the draft alternative", parameters: { action: "get", entryId: finalEntryId } });
1313
- next.push({ tool: "entries", description: "Accept onto Chain", parameters: { action: "commit", entryId: finalEntryId } });
1336
+ next.push(spineFloor && !spineFloor.exempt && !spineFloor.linkWritten ? { tool: "relations", description: "Write the spine link it owes", parameters: { action: "create", from: finalEntryId, type: spineFloor.relationType } } : { tool: "entries", description: "Accept onto Chain", parameters: { action: "commit", entryId: finalEntryId } });
1314
1337
  }
1315
1338
  const qualityPhrase = verdictQualityPhrase(verdictResult);
1316
1339
  const summary = finalStatus === "committed" ? `Captured and accepted ${finalEntryId} (${name}) onto ${landedCollection}. ${qualityPhrase}` : `Captured ${finalEntryId} (${name}) as draft in ${landedCollection}. ${qualityPhrase}`;
@@ -3019,11 +3042,91 @@ ${footer}` }],
3019
3042
 
3020
3043
  // src/tools/relations.ts
3021
3044
  import { z as z13 } from "zod/v3";
3045
+
3046
+ // src/lib/relationTypes.ts
3047
+ var ALLOWED_RELATION_TYPES_LIST = [
3048
+ // Core semantic types
3049
+ "informs",
3050
+ "governs",
3051
+ "surfaces_tension_in",
3052
+ "defines_term_for",
3053
+ "belongs_to",
3054
+ "references",
3055
+ "related_to",
3056
+ "fills_slot",
3057
+ "commits_to",
3058
+ "informed_by",
3059
+ "depends_on",
3060
+ "conflicts_with",
3061
+ "confused_with",
3062
+ // WP-449: Glossary Foundation — negative assertion for agents (spec §4.5)
3063
+ "do_not_confuse_with",
3064
+ "replaces",
3065
+ "part_of",
3066
+ "constrains",
3067
+ // Epistemic types — evidence linking
3068
+ "validates",
3069
+ "invalidates",
3070
+ // Extended types
3071
+ "governed_by",
3072
+ "alternative_to",
3073
+ "has_proposal",
3074
+ "requests_promotion_of",
3075
+ "resolves",
3076
+ // WP-228 Slice 0: Playing Field relation types
3077
+ "experiences",
3078
+ "addresses",
3079
+ // Tier 1 Hierarchy: DEC-401 (experiments prereqs), DEC-402 (risk views)
3080
+ "threatens",
3081
+ "tests",
3082
+ "produces",
3083
+ // WP-274: Capability tier relation types
3084
+ "realizes",
3085
+ "requires",
3086
+ "invests_in",
3087
+ "enables",
3088
+ // PARKED (park note, 2026-08-23) — these 5 verbs are untouched by a separate
3089
+ // governance-verb-ownership review: no delete, no lock, no whitelist, no picker change,
3090
+ // and no relation of this note to that review. Live source-canonicalKey row counts, from
3091
+ // a read-only pass over an uncapped, exhaustive census of a real workspace (measured
3092
+ // 2026-08-23): follows = 11 rows (decision 2, milestone 5, work_package 4);
3093
+ // extracted_from = 4 rows (decision 1, insight 1, tension 1, work_package 1);
3094
+ // part_of_workstream = 0 rows; stated_by = 0 rows; discussed_in = 0 rows. Whether any of
3095
+ // these should be retired, merged, or promoted is a founder call, deliberately deferred —
3096
+ // not decided here, and not decided by silence either. Re-verify the counts before acting
3097
+ // on them; they will drift.
3098
+ // WP-344 S0a: Evolve-First Ingestion relation types
3099
+ "follows",
3100
+ // sequential/derived — new entry follows/continues from another
3101
+ "extracted_from",
3102
+ // provenance — this entry was extracted from a source document
3103
+ // WP-353 S0: scope-propagated commit relation types (DEC-A captured as exploring per RULE-187)
3104
+ "part_of_workstream",
3105
+ // entry belongs to a workstream anchor confirmed at scope step
3106
+ "stated_by",
3107
+ // entry was stated by a speaker in the source document
3108
+ "discussed_in",
3109
+ // entry was discussed within a particular meeting/doc context
3110
+ // Specification types — WP-363 E2: precision alternatives to governs for decision→governance relations
3111
+ "specializes",
3112
+ // DEC narrows or refines a broader STD/PRI — more specific application of the parent
3113
+ "implements",
3114
+ // DEC/WP realizes a BR/STD in concrete operational form
3115
+ // WP-641 S2a (WP-647): belongs_to vocabulary split — the strategy-scope successor.
3116
+ // The other two successors (authority_domain, domain_routed_to) and `blocks` are no longer
3117
+ // legal to write: a new workspace carries none of them (DEC-1845, TASK-331, TASK-425).
3118
+ "anchored_to"
3119
+ // strategy-scope anchor: a raw strategy entry belongs_to a product (DEC-1523)
3120
+ ];
3121
+ var ALLOWED_RELATION_TYPES = new Set(ALLOWED_RELATION_TYPES_LIST);
3122
+
3123
+ // src/tools/relations.ts
3022
3124
  var RELATIONS_ACTIONS = ["create", "batch-create", "delete"];
3125
+ var relationType = z13.enum(ALLOWED_RELATION_TYPES_LIST);
3023
3126
  var relationItemSchema = z13.object({
3024
3127
  from: z13.string().max(200),
3025
3128
  to: z13.string().max(200),
3026
- type: z13.string().max(200)
3129
+ type: relationType
3027
3130
  });
3028
3131
  var relationsSchema = z13.object({
3029
3132
  action: z13.enum(RELATIONS_ACTIONS).describe(
@@ -3031,16 +3134,17 @@ var relationsSchema = z13.object({
3031
3134
  ),
3032
3135
  from: z13.string().max(200).optional().describe("For 'create'/'delete': source entry ID."),
3033
3136
  to: z13.string().max(200).optional().describe("For 'create'/'delete': target entry ID."),
3034
- type: z13.string().max(200).optional().describe("For 'create'/'delete': relation type."),
3137
+ // TASK-1026: the batch item's type is served as a $ref to this one, so the description holds for both; a second enum instance costs 590 bytes of the 64 KB budget (DEC-1981).
3138
+ type: relationType.optional().describe("Relation type, for create, delete and each batch-create item."),
3035
3139
  relations: z13.array(relationItemSchema).min(1).max(RELATION_BATCH_MAX).optional().describe("For 'batch-create': array of {from, to, type}."),
3036
3140
  // WP-316 S3: Preview gate — dry-run mode for action=create.
3037
- preview: z13.boolean().optional().describe("For 'create': if true, validates the relation without writing. Returns what would happen. Default false.")
3141
+ preview: z13.boolean().optional().describe("For 'create': if true, checks the relation and writes nothing.")
3038
3142
  });
3039
3143
  var relationsCreateVariant = z13.object({
3040
3144
  action: z13.literal("create"),
3041
3145
  from: z13.string().max(200),
3042
3146
  to: z13.string().max(200),
3043
- type: z13.string().max(200),
3147
+ type: relationType,
3044
3148
  preview: z13.boolean().optional()
3045
3149
  });
3046
3150
  var relationsBatchCreateVariant = z13.object({
@@ -3051,7 +3155,7 @@ var relationsDeleteVariant = z13.object({
3051
3155
  action: z13.literal("delete"),
3052
3156
  from: z13.string().max(200),
3053
3157
  to: z13.string().max(200),
3054
- type: z13.string().max(200)
3158
+ type: relationType
3055
3159
  });
3056
3160
  var relationsActionUnion = z13.discriminatedUnion("action", [
3057
3161
  relationsCreateVariant,
@@ -3073,7 +3177,9 @@ function registerRelationsTools(server) {
3073
3177
  "relations",
3074
3178
  {
3075
3179
  title: "Relations",
3076
- description: "Create and delete relations between entries. A relation is written only when you ask for it, with exactly the verb you send. Read an entry's relations with the read-only `context action=find`.\n\n- **create**: Link two entries with a typed relation.\n- **batch-create**: Create multiple relations in one call (validates every item before writing any).\n- **delete**: Remove a relation by from, to, and type.",
3180
+ description: `Create and delete relations between entries, each written only on request with exactly the verb you send. Read relations with the read-only \`context action=find\`.
3181
+
3182
+ The spine links, child to parent: ${SPINE_RUNG_LINES.join(" ")}`,
3077
3183
  inputSchema: relationsSchema,
3078
3184
  // DEC-1894 / DEC-74: find lives on the read-only `context` tool; every action left here writes.
3079
3185
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
@@ -4909,27 +5015,27 @@ async function handleBuild(server, entryId, maxHops) {
4909
5015
  )
4910
5016
  };
4911
5017
  }
4912
- async function handleCrossCut(relationType) {
5018
+ async function handleCrossCut(relationType2) {
4913
5019
  const result2 = await kernelQuery("chain.structuralAggregation", {
4914
- relationType
5020
+ relationType: relationType2
4915
5021
  });
4916
5022
  const groupKeys = Object.keys(result2.groups);
4917
5023
  if (result2.totalCount === 0) {
4918
5024
  return {
4919
5025
  content: [{
4920
5026
  type: "text",
4921
- text: `# Cross-Cut: ${relationType}
5027
+ text: `# Cross-Cut: ${relationType2}
4922
5028
 
4923
- No relations of type '${relationType}' found in this workspace.`
5029
+ No relations of type '${relationType2}' found in this workspace.`
4924
5030
  }],
4925
5031
  structuredContent: success(
4926
- `No '${relationType}' relations found.`,
5032
+ `No '${relationType2}' relations found.`,
4927
5033
  { groups: {}, totalCount: 0, truncated: false }
4928
5034
  )
4929
5035
  };
4930
5036
  }
4931
5037
  const lines = [
4932
- `# Cross-Cut: ${relationType}`,
5038
+ `# Cross-Cut: ${relationType2}`,
4933
5039
  `_${result2.totalCount} relations across ${groupKeys.length} source collection${groupKeys.length === 1 ? "" : "s"}${result2.truncated ? " (TRUNCATED \u2014 more relations exist)" : ""}_`,
4934
5040
  ""
4935
5041
  ];
@@ -4948,18 +5054,18 @@ No relations of type '${relationType}' found in this workspace.`
4948
5054
  return {
4949
5055
  content: [{ type: "text", text: lines.join("\n") }],
4950
5056
  structuredContent: success(
4951
- `${result2.totalCount} '${relationType}' relations across ${groupKeys.length} collections.${result2.truncated ? " Truncated." : ""}`,
5057
+ `${result2.totalCount} '${relationType2}' relations across ${groupKeys.length} collections.${result2.truncated ? " Truncated." : ""}`,
4952
5058
  result2
4953
5059
  )
4954
5060
  };
4955
5061
  }
4956
- async function handleChainWalk(entryId, direction, maxHops, relationType) {
5062
+ async function handleChainWalk(entryId, direction, maxHops, relationType2) {
4957
5063
  const clampedHops = Math.max(1, Math.min(4, maxHops));
4958
5064
  const result2 = await kernelQuery("chain.deepChainWalk", {
4959
5065
  entryId,
4960
5066
  direction,
4961
5067
  maxHops: clampedHops,
4962
- ...relationType ? { relationType } : {}
5068
+ ...relationType2 ? { relationType: relationType2 } : {}
4963
5069
  });
4964
5070
  if (result2.error) {
4965
5071
  return notFoundResult(entryId, result2.error);
@@ -4970,7 +5076,7 @@ async function handleChainWalk(entryId, direction, maxHops, relationType) {
4970
5076
  type: "text",
4971
5077
  text: `# Chain Walk: ${entryId}
4972
5078
 
4973
- _No ${direction} relations found${relationType ? ` of type '${relationType}'` : ""}._
5079
+ _No ${direction} relations found${relationType2 ? ` of type '${relationType2}'` : ""}._
4974
5080
 
4975
5081
  Try \`context action=chain entryId="${entryId}" direction="${direction === "outgoing" ? "incoming" : "outgoing"}"\` or remove the relationType filter.`
4976
5082
  }],
@@ -4982,7 +5088,7 @@ Try \`context action=chain entryId="${entryId}" direction="${direction === "outg
4982
5088
  }
4983
5089
  const lines = [
4984
5090
  `# Chain Walk: ${entryId}`,
4985
- `**Direction:** ${direction} | **Depth:** ${clampedHops}${relationType ? ` | **Type filter:** ${relationType}` : ""}`,
5091
+ `**Direction:** ${direction} | **Depth:** ${clampedHops}${relationType2 ? ` | **Type filter:** ${relationType2}` : ""}`,
4986
5092
  `**Nodes:** ${result2.totalNodes}${result2.truncated ? " (TRUNCATED at 100)" : ""} | **Edges:** ${result2.edges.length} | **Cycles:** ${result2.cycles.length}`,
4987
5093
  ""
4988
5094
  ];
@@ -12102,4 +12208,4 @@ export {
12102
12208
  SERVER_VERSION,
12103
12209
  createProductBrainServer
12104
12210
  };
12105
- //# sourceMappingURL=chunk-KU3IBGI4.js.map
12211
+ //# sourceMappingURL=chunk-W3Y4VS6E.js.map