@jefuriiij/synthra 0.14.1 → 0.15.0

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.
@@ -2769,7 +2769,8 @@ async function rememberEntry(paths, input) {
2769
2769
  content: input.text,
2770
2770
  tags: input.tags ?? [],
2771
2771
  files: input.files ?? [],
2772
- date: (/* @__PURE__ */ new Date()).toISOString()
2772
+ date: (/* @__PURE__ */ new Date()).toISOString(),
2773
+ ...input.anchors && input.anchors.length > 0 ? { anchors: input.anchors } : {}
2773
2774
  };
2774
2775
  await appendEntry(active.paths.contextStore, entry);
2775
2776
  const entries = await readEntries(active.paths.contextStore);
@@ -3071,7 +3072,7 @@ var TOOLS = [
3071
3072
  files: {
3072
3073
  type: "array",
3073
3074
  items: { type: "string" },
3074
- description: "Optional project-relative file paths this entry relates to."
3075
+ description: "Optional project-relative file paths this entry relates to. Linked files also anchor the entry: recall flags it 'possibly stale' if they change, and graph_read of those files surfaces it automatically."
3075
3076
  }
3076
3077
  },
3077
3078
  required: ["text", "kind"]
@@ -3613,9 +3614,28 @@ async function graphContinue(args, ctx) {
3613
3614
  Files: ${retrieval.files.map((f) => f.path).join(", ") || "(none)"}
3614
3615
  Reason: ${retrieval.reason}
3615
3616
  `;
3616
- return textContent(`${header}
3617
+ const remembered = matchRememberedFacts(query, retrieval.files, await safeRecallAll(ctx), ctx);
3618
+ return textContent(`${header}${remembered}
3617
3619
  ${packed.text}`);
3618
3620
  }
3621
+ function matchRememberedFacts(query, retrievedFiles, entries, ctx) {
3622
+ if (entries.length === 0) return "";
3623
+ const qTokens = new Set(tokenizeQuery(query));
3624
+ const retrievedPaths = new Set(retrievedFiles.map((f) => f.path));
3625
+ const scored = entries.map((e) => {
3626
+ let score2 = 0;
3627
+ for (const t of tokenizeQuery(`${e.content} ${e.tags.join(" ")}`)) {
3628
+ if (qTokens.has(t)) score2 += 1;
3629
+ }
3630
+ if (e.files.some((f) => retrievedPaths.has(f)) || e.anchors?.some((a) => retrievedPaths.has(a.path))) {
3631
+ score2 += 2;
3632
+ }
3633
+ return { e, score: score2 };
3634
+ }).filter((x) => x.score > 0).sort((a, b) => b.score - a.score).slice(0, PACK_FACTS_MAX);
3635
+ if (scored.length === 0) return "";
3636
+ return `${scored.map((x) => `Remembered: ${factLine(x.e, ctx.graph).slice(2)}`).join("\n")}
3637
+ `;
3638
+ }
3619
3639
  function resolveFileTarget(graph, filePath) {
3620
3640
  const files = graph.nodes.filter((n) => n.kind === "file");
3621
3641
  const exact = files.find((n) => n.path === filePath);
@@ -3708,6 +3728,43 @@ function buildTestsFooter(symbol, graph) {
3708
3728
  if (isLikelyEntry(symbol.file)) return "";
3709
3729
  return "Tests: none linked to this file.";
3710
3730
  }
3731
+ var FACTS_MAX = 3;
3732
+ var FACTS_CONTENT_MAX = 160;
3733
+ var PACK_FACTS_MAX = 2;
3734
+ function staleAnchorPaths(entry, graph) {
3735
+ if (!entry.anchors || entry.anchors.length === 0) return [];
3736
+ const hashByPath = /* @__PURE__ */ new Map();
3737
+ for (const n of graph.nodes) if (n.kind === "file") hashByPath.set(n.path, n.file_hash);
3738
+ return entry.anchors.filter((a) => hashByPath.get(a.path) !== a.hash).map((a) => a.path);
3739
+ }
3740
+ function factLine(entry, graph) {
3741
+ const content = entry.content.length > FACTS_CONTENT_MAX ? `${entry.content.slice(0, FACTS_CONTENT_MAX - 1)}\u2026` : entry.content;
3742
+ const date = entry.date ? ` (${entry.date.slice(0, 10)})` : "";
3743
+ const stale = staleAnchorPaths(entry, graph);
3744
+ const staleNote = stale.length ? ` \u26A0 possibly stale \u2014 ${stale[0]} changed since stored` : "";
3745
+ return `- [${entry.type}] ${content}${date}${staleNote}`;
3746
+ }
3747
+ function entryLinksFile(entry, filePath) {
3748
+ if (entry.anchors?.some((a) => a.path === filePath)) return true;
3749
+ return entry.files.some((f) => f === filePath || filePath.endsWith(`/${f}`));
3750
+ }
3751
+ function buildFactsFooter(filePath, entries, graph) {
3752
+ const linked = entries.filter((e) => entryLinksFile(e, filePath));
3753
+ if (linked.length === 0) return "";
3754
+ const newestFirst = linked.slice().reverse();
3755
+ const shown = newestFirst.slice(0, FACTS_MAX);
3756
+ const omitted = newestFirst.length - shown.length;
3757
+ const lines = ["\u{1F4CC} Remembered for this file:", ...shown.map((e) => factLine(e, graph))];
3758
+ if (omitted > 0) lines.push(`\u2026+${omitted} more \u2014 mcp__synthra__context_recall()`);
3759
+ return lines.join("\n");
3760
+ }
3761
+ async function safeRecallAll(ctx) {
3762
+ try {
3763
+ return (await recallEntries(ctx.paths, {})).entries;
3764
+ } catch {
3765
+ return [];
3766
+ }
3767
+ }
3711
3768
  async function graphRead(args, ctx) {
3712
3769
  const target = typeof args?.target === "string" ? args.target : "";
3713
3770
  if (!target) return errorContent("graph_read: 'target' (string) is required");
@@ -3726,10 +3783,15 @@ async function graphRead(args, ctx) {
3726
3783
  }
3727
3784
  const fileNode = resolved.node;
3728
3785
  await logAccess(ctx, { ts: nowIso(), path: fileNode.path, source: "read" });
3786
+ const facts = buildFactsFooter(fileNode.path, await safeRecallAll(ctx), ctx.graph);
3787
+ const factsBlock = facts ? `
3788
+
3789
+ ---
3790
+ ${facts}` : "";
3729
3791
  if (!symbolName) {
3730
3792
  return textContent(`# ${fileNode.path}
3731
3793
 
3732
- ${fileNode.content}`);
3794
+ ${fileNode.content}${factsBlock}`);
3733
3795
  }
3734
3796
  const cleanSym = symbolName.trim();
3735
3797
  const symbol = ctx.graph.nodes.find(
@@ -3759,7 +3821,7 @@ ${tests}` : "";
3759
3821
  return textContent(
3760
3822
  `# ${fileNode.path}::${symbol.name} (L${symbol.start_line}-${symbol.end_line})
3761
3823
 
3762
- ${body}${depsBlock}${testsBlock}${editHint}`
3824
+ ${body}${depsBlock}${testsBlock}${factsBlock}${editHint}`
3763
3825
  );
3764
3826
  }
3765
3827
  var editedFiles = /* @__PURE__ */ new Set();
@@ -3794,16 +3856,26 @@ async function contextRemember(args, ctx) {
3794
3856
  }
3795
3857
  const tags = Array.isArray(args?.tags) ? args.tags.filter((t) => typeof t === "string") : [];
3796
3858
  const files = Array.isArray(args?.files) ? args.files.filter((f) => typeof f === "string") : [];
3859
+ const anchors = [];
3860
+ for (const f of files) {
3861
+ const resolved = resolveFileTarget(ctx.graph, f);
3862
+ if ("node" in resolved) {
3863
+ anchors.push({ path: resolved.node.path, hash: resolved.node.file_hash });
3864
+ }
3865
+ }
3797
3866
  const result = await rememberEntry(ctx.paths, {
3798
3867
  text,
3799
3868
  kind: kindRaw,
3800
3869
  tags,
3801
- files
3870
+ files,
3871
+ anchors
3802
3872
  });
3873
+ const anchorNote = anchors.length ? `
3874
+ Anchored to ${anchors.length} file(s) \u2014 recall will flag this entry if they change.` : "";
3803
3875
  return textContent(
3804
3876
  `Remembered ${result.entry.type} on branch '${result.branch}'.
3805
3877
  Stored: ${result.storePath}
3806
- CONTEXT.md refreshed: ${result.contextMdPath}`
3878
+ CONTEXT.md refreshed: ${result.contextMdPath}${anchorNote}`
3807
3879
  );
3808
3880
  }
3809
3881
  var DEFAULT_RECENT_WINDOW_MS = 60 * 60 * 1e3;
@@ -3838,7 +3910,9 @@ async function contextRecall(args, ctx) {
3838
3910
  const lines = [`# Context entries \u2014 branch: ${result.branch}`, ""];
3839
3911
  for (const e of result.entries) {
3840
3912
  const tags = e.tags.length ? ` [${e.tags.join(", ")}]` : "";
3841
- lines.push(`- **${e.type}**${tags} (${e.date}): ${e.content}`);
3913
+ const stale = staleAnchorPaths(e, ctx.graph);
3914
+ const staleNote = stale.length ? ` \u26A0 possibly stale \u2014 ${stale.join(", ")} changed since stored` : "";
3915
+ lines.push(`- **${e.type}**${tags} (${e.date}): ${e.content}${staleNote}`);
3842
3916
  if (e.files.length) lines.push(` files: ${e.files.join(", ")}`);
3843
3917
  }
3844
3918
  return textContent(lines.join("\n"));