@productbrain/mcp 0.0.1-beta.11174 → 0.0.1-beta.11180

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.
@@ -146,8 +146,6 @@ var metaSchema = z.object({
146
146
  timing: z.object({
147
147
  classifyMs: z.number().optional(),
148
148
  createMs: z.number().optional(),
149
- autoLinkMs: z.number().optional(),
150
- userLinksMs: z.number().optional(),
151
149
  qualityMs: z.number().optional(),
152
150
  totalMs: z.number()
153
151
  }).passthrough().optional(),
@@ -673,24 +671,11 @@ This client stopped waiting after ${error.elapsedMs}ms. ${recovery}`
673
671
  })
674
672
  };
675
673
  }
676
- async function resumeReplayedCapture(replay, name, links, sessionId) {
677
- const requested = (links ?? []).slice(0, 25);
678
- let replayedLinks = [];
679
- if (requested.length > 0) {
680
- const batch = await kernelMutation("chain.createEntryRelations", {
681
- relations: requested.map((l) => ({ fromEntryId: replay.entryId, toEntryId: l.to, type: l.type, proposedBy: "user" })),
682
- sessionId
683
- }).catch((err) => ({ results: requested.map((l) => ({ toEntryId: l.to, type: l.type, ok: false, error: err instanceof Error ? err.message : "unknown error" })) }));
684
- replayedLinks = requested.map((l) => {
685
- const row = batch.results.find((r) => r.toEntryId === l.to && r.type === l.type);
686
- if (row?.ok) return { to: l.to, type: l.type, outcome: "written" };
687
- const error = row?.error ?? "no answer for this link";
688
- return /^Duplicate relation/.test(error) ? { to: l.to, type: l.type, outcome: "existing" } : { to: l.to, type: l.type, outcome: "failed", error };
689
- });
690
- }
674
+ function resumeReplayedCapture(replay, name) {
675
+ const links = replay.links ?? [];
691
676
  const status2 = isTrustAccepted(replay.status) ? "committed" : "draft";
692
677
  const summary = `Already captured as ${replay.entryId}; this re-send returned that entry and wrote no second one.`;
693
- const linkLines = replayedLinks.map((l) => `- ${l.type} ${l.to}: ${l.outcome === "written" ? "written now" : l.outcome === "existing" ? "already there" : `not written (${l.error})`}`);
678
+ const linkLines = links.map((l) => `- ${l.line}`);
694
679
  return {
695
680
  content: [{
696
681
  type: "text",
@@ -709,7 +694,7 @@ Read it with \`context action=get\`.`
709
694
  name,
710
695
  status: status2,
711
696
  replayed: true,
712
- ...replayedLinks.length ? { replayedLinks } : {}
697
+ ...links.length ? { links } : {}
713
698
  })
714
699
  };
715
700
  }
@@ -1156,6 +1141,7 @@ var LABEL_DESCRIPTION_MAX_CHARS = 2e3;
1156
1141
  var RELATION_BATCH_MAX = 25;
1157
1142
 
1158
1143
  // src/tools/smart-capture.ts
1144
+ var CAPTURE_LINK_OUTCOMES = ["written", "refused"];
1159
1145
  var GENERIC_PROFILE = {
1160
1146
  governedDraft: false,
1161
1147
  descriptionField: null,
@@ -1203,7 +1189,7 @@ var captureSchema = z4.object({
1203
1189
  links: z4.array(z4.object({
1204
1190
  to: z4.string().describe("Target entry ID (e.g. '<PREFIX>-<n>')"),
1205
1191
  type: z4.string().describe("Relation type (e.g. 'governs', 'related_to', 'informs')")
1206
- })).optional().describe("Relations to create after capture. Without links, a capture writes no relations."),
1192
+ })).optional().describe("Links written with the entry in one transaction; each returns written or refused with a reason; a refused link fails nothing."),
1207
1193
  sourceRef: z4.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."),
1208
1194
  sourceExcerpt: z4.string().optional().describe("Verbatim excerpt from the source that backs this entry's claims. Stored as top-level entry field, not in data."),
1209
1195
  // WP-316 S3: Preview gate — dry-run mode. Returns what would happen, no DB writes.
@@ -1489,7 +1475,8 @@ var captureSuccessOutputSchema = z4.object({
1489
1475
  // client-side auto-commit attempt that alone populated them (its gate refusal, the commit's advisory).
1490
1476
  replayed: z4.literal(true).optional(),
1491
1477
  // TASK-898: a re-sent capture answered with the first capture's entry
1492
- replayedLinks: z4.array(z4.object({ to: z4.string(), type: z4.string(), outcome: z4.enum(["written", "existing", "failed"]), error: z4.string().optional() })).optional(),
1478
+ // TASK-897: the kernel's outcome per link, on a create and on a replay.
1479
+ links: z4.array(z4.object({ toEntryId: z4.string(), type: z4.string(), outcome: z4.enum(CAPTURE_LINK_OUTCOMES), reason: z4.string().optional(), line: z4.string() }).passthrough()).optional(),
1493
1480
  fallbackApplied: z4.boolean().optional()
1494
1481
  // round 2 (PRRT_kwDORS4ils6cTNbF): classifier field above still names the original (pre-substitution) prediction
1495
1482
  }).strict();
@@ -1515,7 +1502,7 @@ var captureCompoundSchema = z4.object({
1515
1502
  links: z4.array(z4.object({
1516
1503
  to: z4.string().max(200).describe("Target entry ID (e.g. '<PREFIX>-<n>')"),
1517
1504
  type: z4.string().max(200).describe("Relation type (e.g. 'governs', 'related_to', 'informs')")
1518
- })).max(RELATION_BATCH_MAX).optional().describe("Relations to create after capture. Without links, a capture writes no relations."),
1505
+ })).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."),
1519
1506
  sourceRef: z4.string().max(2e3).optional().describe("URI or path of the source document backing this entry."),
1520
1507
  sourceExcerpt: z4.string().max(SOURCE_EXCERPT_MAX_CHARS).optional().describe("Verbatim excerpt from the source backing this entry's claims."),
1521
1508
  preview: z4.boolean().optional().describe("If true, validates without writing."),
@@ -1640,7 +1627,6 @@ async function handleCapture(server, { collection, name, description, context, c
1640
1627
  context,
1641
1628
  data,
1642
1629
  entryId: "",
1643
- linksCreated: [],
1644
1630
  collectionFields: col.fields ?? []
1645
1631
  });
1646
1632
  for (const [key, val] of Object.entries(inferred)) {
@@ -1695,6 +1681,7 @@ async function handleCapture(server, { collection, name, description, context, c
1695
1681
  let coachingFromCreate;
1696
1682
  let wasAutoCommittedServerSide = false;
1697
1683
  let spineFloor;
1684
+ let linkOutcomes = [];
1698
1685
  try {
1699
1686
  const result2 = await kernelMutation("chain.createEntry", {
1700
1687
  collectionSlug: resolvedCollection,
@@ -1712,16 +1699,11 @@ async function handleCapture(server, { collection, name, description, context, c
1712
1699
  // WP-513: explicit rung-2 owner override (WP-753 O1b: its team derives server-side). TASK-681: `team` places a role or team.
1713
1700
  ...ownerRoleEntryId ? { ownerRoleEntryId } : {},
1714
1701
  ...team ? { teamRef: team } : {},
1715
- // WP-475 E1 (Codex P2 — CLI parity): user links are known UPFRONT, so the
1716
- // post-relations re-evaluation below owns the single LLM run — defer the
1717
- // create-time LLM schedule (deterministic, mirrors `pb capture --link`).
1718
- // A capture without links writes no relations (TASK-599): no defer, it keeps its LLM run.
1719
- // Fail-safe: if the relations batch then creates nothing, the deferred row
1720
- // keeps its delivered heuristic verdict with llmStatus 'skipped' (same
1721
- // accepted shape as the CLI dying between create and re-eval).
1722
- ...links && links.length > 0 ? { deferLLMSchedule: true } : {}
1702
+ // TASK-897 (DEC-2113): the caller's links ride in this one request; the kernel resolves each target, judges the
1703
+ // verb and writes the row before its one coaching verdict, and answers per link (a refused link fails nothing).
1704
+ ...links && links.length > 0 ? { links: links.map((l) => ({ toEntryId: l.to, type: l.type })) } : {}
1723
1705
  });
1724
- if (result2 && "replayed" in result2) return resumeReplayedCapture(result2, name, links, agentId ?? void 0);
1706
+ if (result2 && "replayed" in result2) return resumeReplayedCapture(result2, name);
1725
1707
  if (result2 && isPreviewCreateEntryResult(result2)) {
1726
1708
  const preflight = await resolvePreviewPreflight(result2, links);
1727
1709
  const dupCount = groundingReport.duplicates.length;
@@ -1791,6 +1773,7 @@ No DB writes \u2014 call without \`preview:true\` to capture for real.${result2.
1791
1773
  normalizationMeta = created.normalization;
1792
1774
  formativeHints = created.qualityHints ?? [];
1793
1775
  coachingFromCreate = created.coaching;
1776
+ linkOutcomes = created.links ?? [];
1794
1777
  resolveGapsForEntry(name, created.entryId);
1795
1778
  const createdStatus = created.status;
1796
1779
  if (createdStatus !== void 0 && isTrustAccepted(createdStatus)) wasAutoCommittedServerSide = true;
@@ -1826,78 +1809,8 @@ Use \`context action=get\` to inspect the existing entry, or \`entries action=up
1826
1809
  throw error;
1827
1810
  }
1828
1811
  const tAfterCreate = Date.now();
1829
- const linksCreated = [];
1830
- const userLinkResults = [];
1831
- const pendingRelations = [];
1832
- if (links && links.length > 0 && finalEntryId) {
1833
- const slugByPrefix = new Map((await getCollections()).filter((c) => c.idPrefix).map((c) => [c.idPrefix, c.slug]));
1834
- for (const link of links) {
1835
- pendingRelations.push({
1836
- fromEntryId: finalEntryId,
1837
- toEntryId: link.to,
1838
- type: link.type,
1839
- // WP-474 E4-REWIRE: user-chosen type — mark provenance, NO confidence (human-chosen).
1840
- proposedBy: "user",
1841
- meta: { targetName: link.to, targetCollection: slugByPrefix.get(link.to.split("-")[0]) ?? "unknown" }
1842
- });
1843
- }
1844
- }
1845
- const resolvedCK = canonicalKey;
1846
- const batchRelationsPromise = pendingRelations.length > 0 ? kernelMutation(
1847
- "chain.createEntryRelations",
1848
- {
1849
- // WP-474 E4-REWIRE: carry provenance through to the server seam.
1850
- relations: pendingRelations.slice(0, 25).map((r) => ({
1851
- fromEntryId: r.fromEntryId,
1852
- toEntryId: r.toEntryId,
1853
- type: r.type,
1854
- ...r.proposedBy !== void 0 ? { proposedBy: r.proposedBy } : {}
1855
- })),
1856
- sessionId: agentId ?? void 0
1857
- }
1858
- ).catch((err) => {
1859
- entryWarnings.push(`Linking partially failed: ${err instanceof Error ? err.message : "unknown error"}`);
1860
- return null;
1861
- }) : Promise.resolve(null);
1862
- const cardinalityPromise = resolvedCK ? kernelQuery("chain.checkCardinalityWarning", { canonicalKey: resolvedCK, excludeEntryId: internalId }).catch(() => null) : Promise.resolve(null);
1863
- const coachingPromise = coachingFromCreate !== void 0 ? Promise.resolve(coachingFromCreate) : kernelMutation("quality.evaluateHeuristicAndSchedule", {
1864
- entryId: finalEntryId,
1865
- context: "capture"
1866
- }).catch(() => null);
1867
- const [batchResult, cardinalityCheck, coachingAtCreate] = await Promise.all([
1868
- batchRelationsPromise,
1869
- cardinalityPromise,
1870
- coachingPromise
1871
- ]);
1872
- let verdictResult = coachingAtCreate;
1873
- if ((batchResult?.created ?? 0) > 0) {
1874
- const reEval = await kernelMutation("quality.evaluateHeuristicAndSchedule", {
1875
- entryId: finalEntryId,
1876
- context: "capture"
1877
- }).catch(() => null);
1878
- if (reEval?.verdict) verdictResult = reEval;
1879
- }
1880
- const tAfterRelations = Date.now();
1881
- if (batchResult) {
1882
- for (let i = 0; i < pendingRelations.length; i++) {
1883
- const pending = pendingRelations[i];
1884
- const result2 = batchResult.results[i];
1885
- if (!result2) continue;
1886
- if (result2.ok) {
1887
- userLinkResults.push({ label: `\u2713 ${pending.type} \u2192 ${pending.toEntryId}`, ok: true });
1888
- linksCreated.push({
1889
- targetEntryId: pending.toEntryId,
1890
- targetName: pending.meta.targetName,
1891
- targetCollection: pending.meta.targetCollection,
1892
- relationType: pending.type
1893
- });
1894
- } else {
1895
- userLinkResults.push({ label: `\u2717 ${pending.type} \u2192 ${pending.toEntryId}: ${result2.error ?? "failed"}`, ok: false });
1896
- }
1897
- }
1898
- }
1812
+ const verdictResult = coachingFromCreate;
1899
1813
  const tAfterQuality = Date.now();
1900
- const cardinalityWarning = cardinalityCheck?.warning ?? null;
1901
1814
  let coachingSection = "";
1902
1815
  if (verdictResult?.verdict && verdictResult.verdict.tier !== "passive" && verdictResult.verdict.criteria.length > 0) {
1903
1816
  coachingSection = formatRubricCoaching(verdictResult);
@@ -1969,10 +1882,10 @@ Use \`context action=get\` to inspect the existing entry, or \`entries action=up
1969
1882
  lines.push("");
1970
1883
  lines.push(`**View in Cortex:** ${studioUrl}`);
1971
1884
  }
1972
- if (userLinkResults.length > 0) {
1885
+ if (linkOutcomes.length > 0) {
1973
1886
  lines.push("");
1974
1887
  lines.push("## Requested links");
1975
- for (const r of userLinkResults) lines.push(`- ${r.label}`);
1888
+ for (const l of linkOutcomes) lines.push(`- ${l.line}`);
1976
1889
  }
1977
1890
  if (finalStatus === "committed") {
1978
1891
  lines.push("");
@@ -2005,9 +1918,16 @@ Use \`context action=get\` to inspect the existing entry, or \`entries action=up
2005
1918
  hint_fields: formativeHints.map((h) => h.field)
2006
1919
  });
2007
1920
  }
2008
- const isWorkPackageOrBetCapture = isWorkPackageCapture || resolvedCK === "bet";
2009
- const isBetOrGoal = isWorkPackageOrBetCapture || resolvedCK === "goal";
2010
- const hasStrategyLink = linksCreated.some((l) => l.targetCollection === "strategy");
1921
+ const isWorkPackageOrBetCapture = isWorkPackageCapture || canonicalKey === "bet";
1922
+ const isBetOrGoal = isWorkPackageOrBetCapture || canonicalKey === "goal";
1923
+ let hasStrategyLink = false;
1924
+ if (isBetOrGoal) {
1925
+ const written = linkOutcomes.filter((l) => l.outcome === "written");
1926
+ if (written.length > 0) {
1927
+ const slugByPrefix = new Map((await getCollections()).filter((c) => c.idPrefix).map((c) => [c.idPrefix, c.slug]));
1928
+ hasStrategyLink = written.some((l) => slugByPrefix.get(l.toEntryId.split("-")[0]) === "strategy");
1929
+ }
1930
+ }
2011
1931
  if (isBetOrGoal && !hasStrategyLink) {
2012
1932
  lines.push("");
2013
1933
  lines.push(
@@ -2015,10 +1935,6 @@ Use \`context action=get\` to inspect the existing entry, or \`entries action=up
2015
1935
  );
2016
1936
  await recordSessionActivity({ strategyLinkWarnedForEntryId: internalId });
2017
1937
  }
2018
- if (cardinalityWarning) {
2019
- lines.push("");
2020
- lines.push(`**Cardinality warning:** ${cardinalityWarning}`);
2021
- }
2022
1938
  if (entryWarnings.length > 0) {
2023
1939
  lines.push("");
2024
1940
  lines.push("## Validation Warnings");
@@ -2040,12 +1956,12 @@ Use \`context action=get\` to inspect the existing entry, or \`entries action=up
2040
1956
  lines.push(`2. **Improve quality:** \`entries action=update entryId="${eid}"\` \u2014 fill missing fields`);
2041
1957
  }
2042
1958
  } else {
2043
- if (userLinkResults.length === 0) {
1959
+ if (linkOutcomes.length === 0) {
2044
1960
  lines.push(`1. **Connect it:** find related entries with \`context action=search\`, then \`relations action=create from="${eid}" to="<ID>" type="<verb>"\``);
2045
1961
  }
2046
- lines.push(`${userLinkResults.length === 0 ? "2" : "1"}. **Review the possibility:** Keep this visible draft while it is an alternative. Accept only when commitment is intended and your role is authorized. Types reserved for human acceptance require human ratification. An audit advisory is not a reason to accept it.`);
1962
+ lines.push(`${linkOutcomes.length === 0 ? "2" : "1"}. **Review the possibility:** Keep this visible draft while it is an alternative. Accept only when commitment is intended and your role is authorized. Types reserved for human acceptance require human ratification. An audit advisory is not a reason to accept it.`);
2047
1963
  if (hasQualityGaps) {
2048
- lines.push(`${userLinkResults.length === 0 ? "3" : "2"}. **Improve quality:** \`entries action=update entryId="${eid}"\` \u2014 fill missing fields`);
1964
+ lines.push(`${linkOutcomes.length === 0 ? "3" : "2"}. **Improve quality:** \`entries action=update entryId="${eid}"\` \u2014 fill missing fields`);
2049
1965
  }
2050
1966
  }
2051
1967
  try {
@@ -2088,8 +2004,7 @@ Use \`context action=get\` to inspect the existing entry, or \`entries action=up
2088
2004
  const timing = {
2089
2005
  classifyMs: tAfterClassify - timingStart,
2090
2006
  createMs: tAfterCreate - tAfterClassify,
2091
- autoLinkMs: tAfterRelations - tAfterCreate,
2092
- qualityMs: tAfterQuality - tAfterRelations,
2007
+ qualityMs: tAfterQuality - tAfterCreate,
2093
2008
  totalMs
2094
2009
  };
2095
2010
  const toolResult = {
@@ -2120,6 +2035,8 @@ Use \`context action=get\` to inspect the existing entry, or \`entries action=up
2120
2035
  qualityHints: formativeHints,
2121
2036
  // WP-745 TASK-160 (PR #901 review): the machine-readable projection — was reaching the human text only.
2122
2037
  ...spineFloor && { spineFloor },
2038
+ ...linkOutcomes.length > 0 && { links: linkOutcomes },
2039
+ // TASK-897
2123
2040
  // WP-318 S2: the possible-duplicate report — always included (empty when no ask match)
2124
2041
  groundingReport
2125
2042
  },
@@ -12910,4 +12827,4 @@ export {
12910
12827
  createProductBrainServer,
12911
12828
  initFeatureFlags
12912
12829
  };
12913
- //# sourceMappingURL=chunk-66VWT677.js.map
12830
+ //# sourceMappingURL=chunk-SW67SRBA.js.map