eminiar 0.6.2 → 0.8.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.
package/dist/cli.js CHANGED
@@ -26524,9 +26524,9 @@ var require_parse2 = __commonJS({
26524
26524
  const idx = prev.value.lastIndexOf("[");
26525
26525
  const pre = prev.value.slice(0, idx);
26526
26526
  const rest3 = prev.value.slice(idx + 2);
26527
- const posix = POSIX_REGEX_SOURCE[rest3];
26528
- if (posix) {
26529
- prev.value = pre + posix;
26527
+ const posix2 = POSIX_REGEX_SOURCE[rest3];
26528
+ if (posix2) {
26529
+ prev.value = pre + posix2;
26530
26530
  state.backtrack = true;
26531
26531
  advance();
26532
26532
  if (!bos.output && tokens.indexOf(prev) === 1) {
@@ -27048,7 +27048,7 @@ var require_picomatch = __commonJS({
27048
27048
  throw new TypeError("Expected pattern to be a non-empty string");
27049
27049
  }
27050
27050
  const opts = options || {};
27051
- const posix = utils.isWindows(options);
27051
+ const posix2 = utils.isWindows(options);
27052
27052
  const regex = isState ? picomatch.compileRe(glob2, options) : picomatch.makeRe(glob2, options, false, true);
27053
27053
  const state = regex.state;
27054
27054
  delete regex.state;
@@ -27058,8 +27058,8 @@ var require_picomatch = __commonJS({
27058
27058
  isIgnored = picomatch(opts.ignore, ignoreOpts, returnState);
27059
27059
  }
27060
27060
  const matcher = (input2, returnObject = false) => {
27061
- const { isMatch, match, output: output2 } = picomatch.test(input2, regex, options, { glob: glob2, posix });
27062
- const result = { glob: glob2, state, regex, posix, input: input2, output: output2, match, isMatch };
27061
+ const { isMatch, match, output: output2 } = picomatch.test(input2, regex, options, { glob: glob2, posix: posix2 });
27062
+ const result = { glob: glob2, state, regex, posix: posix2, input: input2, output: output2, match, isMatch };
27063
27063
  if (typeof opts.onResult === "function") {
27064
27064
  opts.onResult(result);
27065
27065
  }
@@ -27084,7 +27084,7 @@ var require_picomatch = __commonJS({
27084
27084
  }
27085
27085
  return matcher;
27086
27086
  };
27087
- picomatch.test = (input2, regex, options, { glob: glob2, posix } = {}) => {
27087
+ picomatch.test = (input2, regex, options, { glob: glob2, posix: posix2 } = {}) => {
27088
27088
  if (typeof input2 !== "string") {
27089
27089
  throw new TypeError("Expected input to be a string");
27090
27090
  }
@@ -27092,7 +27092,7 @@ var require_picomatch = __commonJS({
27092
27092
  return { isMatch: false, output: "" };
27093
27093
  }
27094
27094
  const opts = options || {};
27095
- const format = opts.format || (posix ? utils.toPosixSlashes : null);
27095
+ const format = opts.format || (posix2 ? utils.toPosixSlashes : null);
27096
27096
  let match = input2 === glob2;
27097
27097
  let output2 = match && format ? format(input2) : input2;
27098
27098
  if (match === false) {
@@ -27101,14 +27101,14 @@ var require_picomatch = __commonJS({
27101
27101
  }
27102
27102
  if (match === false || opts.capture === true) {
27103
27103
  if (opts.matchBase === true || opts.basename === true) {
27104
- match = picomatch.matchBase(input2, regex, options, posix);
27104
+ match = picomatch.matchBase(input2, regex, options, posix2);
27105
27105
  } else {
27106
27106
  match = regex.exec(output2);
27107
27107
  }
27108
27108
  }
27109
27109
  return { isMatch: Boolean(match), match, output: output2 };
27110
27110
  };
27111
- picomatch.matchBase = (input2, glob2, options, posix = utils.isWindows(options)) => {
27111
+ picomatch.matchBase = (input2, glob2, options, posix2 = utils.isWindows(options)) => {
27112
27112
  const regex = glob2 instanceof RegExp ? glob2 : picomatch.makeRe(glob2, options);
27113
27113
  return regex.test(path3.basename(input2));
27114
27114
  };
@@ -27299,9 +27299,9 @@ var require_micromatch = __commonJS({
27299
27299
  return [].concat(patterns).every((p) => picomatch(p, options)(str2));
27300
27300
  };
27301
27301
  micromatch.capture = (glob2, input2, options) => {
27302
- let posix = utils.isWindows(options);
27302
+ let posix2 = utils.isWindows(options);
27303
27303
  let regex = picomatch.makeRe(String(glob2), { ...options, capture: true });
27304
- let match = regex.exec(posix ? utils.toPosixSlashes(input2) : input2);
27304
+ let match = regex.exec(posix2 ? utils.toPosixSlashes(input2) : input2);
27305
27305
  if (match) {
27306
27306
  return match.slice(1).map((v) => v === void 0 ? "" : v);
27307
27307
  }
@@ -27481,15 +27481,15 @@ var require_pattern = __commonJS({
27481
27481
  exports.removeDuplicateSlashes = removeDuplicateSlashes;
27482
27482
  function partitionAbsoluteAndRelative(patterns) {
27483
27483
  const absolute = [];
27484
- const relative10 = [];
27484
+ const relative11 = [];
27485
27485
  for (const pattern2 of patterns) {
27486
27486
  if (isAbsolute(pattern2)) {
27487
27487
  absolute.push(pattern2);
27488
27488
  } else {
27489
- relative10.push(pattern2);
27489
+ relative11.push(pattern2);
27490
27490
  }
27491
27491
  }
27492
- return [absolute, relative10];
27492
+ return [absolute, relative11];
27493
27493
  }
27494
27494
  exports.partitionAbsoluteAndRelative = partitionAbsoluteAndRelative;
27495
27495
  function isAbsolute(pattern2) {
@@ -27920,11 +27920,11 @@ var require_out = __commonJS({
27920
27920
  async.read(path3, getSettings(optionsOrSettingsOrCallback), callback);
27921
27921
  }
27922
27922
  exports.stat = stat;
27923
- function statSync11(path3, optionsOrSettings) {
27923
+ function statSync12(path3, optionsOrSettings) {
27924
27924
  const settings = getSettings(optionsOrSettings);
27925
27925
  return sync.read(path3, settings);
27926
27926
  }
27927
- exports.statSync = statSync11;
27927
+ exports.statSync = statSync12;
27928
27928
  function getSettings(settingsOrOptions = {}) {
27929
27929
  if (settingsOrOptions instanceof settings_1.default) {
27930
27930
  return settingsOrOptions;
@@ -29804,19 +29804,19 @@ var require_out4 = __commonJS({
29804
29804
  return utils.path.convertPathToPattern(source);
29805
29805
  }
29806
29806
  FastGlob2.convertPathToPattern = convertPathToPattern;
29807
- let posix;
29808
- (function(posix2) {
29807
+ let posix2;
29808
+ (function(posix3) {
29809
29809
  function escapePath2(source) {
29810
29810
  assertPatternsInput(source);
29811
29811
  return utils.path.escapePosixPath(source);
29812
29812
  }
29813
- posix2.escapePath = escapePath2;
29813
+ posix3.escapePath = escapePath2;
29814
29814
  function convertPathToPattern2(source) {
29815
29815
  assertPatternsInput(source);
29816
29816
  return utils.path.convertPosixPathToPattern(source);
29817
29817
  }
29818
- posix2.convertPathToPattern = convertPathToPattern2;
29819
- })(posix = FastGlob2.posix || (FastGlob2.posix = {}));
29818
+ posix3.convertPathToPattern = convertPathToPattern2;
29819
+ })(posix2 = FastGlob2.posix || (FastGlob2.posix = {}));
29820
29820
  let win32;
29821
29821
  (function(win322) {
29822
29822
  function escapePath2(source) {
@@ -30776,13 +30776,14 @@ function fileOf(id) {
30776
30776
  if (kind === "sym") return rest2.split("#")[0];
30777
30777
  return void 0;
30778
30778
  }
30779
- var fileId, memoryId, symId, pkgId, personId, conceptId;
30779
+ var fileId, memoryId, docId, symId, pkgId, personId, conceptId;
30780
30780
  var init_ids = __esm({
30781
30781
  "src/graph/ids.ts"() {
30782
30782
  "use strict";
30783
30783
  init_esm_shims();
30784
30784
  fileId = (rel) => `file:${rel}`;
30785
30785
  memoryId = (name) => `memory:${name}`;
30786
+ docId = (name) => `doc:${name}`;
30786
30787
  symId = (rel, name) => `sym:${rel}#${name}`;
30787
30788
  pkgId = (name) => `pkg:${name}`;
30788
30789
  personId = (name) => `person:${name}`;
@@ -31152,9 +31153,10 @@ function impact(db, ref, opts = {}) {
31152
31153
  frontier = next;
31153
31154
  if (!frontier.length) break;
31154
31155
  }
31155
- for (const e of db.graphEdgesTo(start, ["mentions"])) {
31156
+ for (const e of db.graphEdgesTo(start, ["mentions", "documents"])) {
31156
31157
  const via = e.dst === root.id ? root.name : e.dst.replace(/^sym:[^#]*#/, "");
31157
- add(e.src, 0.5 * e.confidence, 1, `memory mentions ${via}`);
31158
+ const who = e.kind === "documents" ? "doc documents" : "memory mentions";
31159
+ add(e.src, 0.5 * e.confidence, 1, `${who} ${via}`);
31158
31160
  }
31159
31161
  if (opts.includeGit !== false) {
31160
31162
  const file3 = toFile(root.id);
@@ -35514,10 +35516,13 @@ __export(store_exports, {
35514
35516
  isValidName: () => isValidName,
35515
35517
  list: () => list,
35516
35518
  memoriesDir: () => memoriesDir,
35519
+ memoriesRel: () => memoriesRel,
35520
+ memoryLinks: () => memoryLinks,
35517
35521
  normalizeRelated: () => normalizeRelated,
35518
35522
  read: () => read,
35519
35523
  reconcile: () => reconcile,
35520
35524
  relOf: () => relOf,
35525
+ relatedTarget: () => relatedTarget,
35521
35526
  remove: () => remove,
35522
35527
  rename: () => rename,
35523
35528
  setMeta: () => setMeta,
@@ -35593,6 +35598,17 @@ function normalizeRelated(ids) {
35593
35598
  }
35594
35599
  return [...out];
35595
35600
  }
35601
+ function relatedTarget(id) {
35602
+ return id.startsWith("mem:") ? memoryId(id.slice(4)) : id;
35603
+ }
35604
+ function memoryLinks(body, self) {
35605
+ const out = /* @__PURE__ */ new Set();
35606
+ for (const m of body.matchAll(MEM_LINK_RE)) {
35607
+ const name = m[1].replace(/[.-]+$/, "");
35608
+ if (name && name !== self && isValidName(name)) out.add(name);
35609
+ }
35610
+ return [...out];
35611
+ }
35596
35612
  function writeMemoryFile(ctx, name, meta4, body) {
35597
35613
  const abs = absOf(ctx, name);
35598
35614
  mkdirSync7(dirname8(abs), { recursive: true });
@@ -35600,36 +35616,30 @@ function writeMemoryFile(ctx, name, meta4, body) {
35600
35616
  ${body.trim()}
35601
35617
  `));
35602
35618
  }
35603
- function syncGraph(db, name, rel, kind, tags, related, supersedes) {
35619
+ function syncGraph(db, name, rel, g) {
35620
+ const self = memoryId(name);
35604
35621
  db.graphDeleteForPath("curated", rel);
35605
35622
  db.graphUpsertNodes([
35606
35623
  {
35607
- id: memoryId(name),
35624
+ id: self,
35608
35625
  kind: "memory",
35609
35626
  name,
35610
35627
  path: rel,
35611
35628
  source: "curated",
35612
- meta: { kind, tags }
35629
+ meta: { kind: g.kind, tags: g.tags, ...g.status ? { status: g.status } : {} }
35613
35630
  }
35614
35631
  ]);
35615
- const edges = related.map((dst) => ({
35616
- src: memoryId(name),
35617
- dst,
35618
- kind: "mentions",
35619
- source: "curated",
35620
- confidence: 1,
35621
- weight: 1
35622
- }));
35623
- if (supersedes) {
35624
- edges.push({
35625
- src: memoryId(name),
35626
- dst: memoryId(supersedes),
35627
- kind: "supersedes",
35628
- source: "curated",
35629
- confidence: 1,
35630
- weight: 1
35631
- });
35632
- }
35632
+ const edges = [];
35633
+ const seen = /* @__PURE__ */ new Set();
35634
+ const push = (dst, kind) => {
35635
+ const key = `${kind}|${dst}`;
35636
+ if (dst === self || seen.has(key)) return;
35637
+ seen.add(key);
35638
+ edges.push({ src: self, dst, kind, source: "curated", confidence: 1, weight: 1 });
35639
+ };
35640
+ for (const id of g.related) push(relatedTarget(id), "mentions");
35641
+ for (const other of memoryLinks(g.body, name)) push(memoryId(other), "links");
35642
+ if (g.supersedes) push(memoryId(g.supersedes.replace(/^mem(?:ory)?:/, "")), "supersedes");
35633
35643
  if (edges.length) db.graphUpsertEdges(edges);
35634
35644
  }
35635
35645
  function forgetInDb(db, name, rel) {
@@ -35716,7 +35726,7 @@ tags: ${tags.join(", ")}` : ""}`,
35716
35726
  status,
35717
35727
  supersedes
35718
35728
  });
35719
- syncGraph(db, name, rel, kind, tags, related, supersedes);
35729
+ syncGraph(db, name, rel, { kind, tags, related, status, supersedes, body });
35720
35730
  return { changed: true, row: db.memoryGetByName(name) };
35721
35731
  }
35722
35732
  function setMeta(ctx, db, name, patch) {
@@ -35919,7 +35929,7 @@ function gitignoreHint(ctx) {
35919
35929
  const ignored = readFileSync16(file3, "utf8").split(/\r?\n/).map((l) => l.trim().replace(/^\//, "").replace(/\/$/, "")).some((l) => l === top || l === dir || l === `${top}/**` || l === `${dir}/**`);
35920
35930
  return ignored ? `note: .gitignore ignores ${dir}, so memories stay local to this machine (remove that line to share them with the team)` : void 0;
35921
35931
  }
35922
- var import_fast_glob3, MEMORY_KINDS, SEGMENT, NAME_RE, hinted;
35932
+ var import_fast_glob3, MEMORY_KINDS, SEGMENT, NAME_RE, MEM_LINK_RE, hinted;
35923
35933
  var init_store = __esm({
35924
35934
  "src/memory/store.ts"() {
35925
35935
  "use strict";
@@ -35941,6 +35951,7 @@ var init_store = __esm({
35941
35951
  ];
35942
35952
  SEGMENT = "[a-z0-9][a-z0-9._-]*";
35943
35953
  NAME_RE = new RegExp(`^(?:${SEGMENT}/)?${SEGMENT}$`);
35954
+ MEM_LINK_RE = /(?<![A-Za-z0-9_./-])mem:([a-z0-9][a-z0-9._-]*(?:\/[a-z0-9][a-z0-9._-]*)?)/g;
35944
35955
  hinted = /* @__PURE__ */ new Set();
35945
35956
  }
35946
35957
  });
@@ -36626,7 +36637,7 @@ function architecture(ctx, db, aspects = ASPECTS) {
36626
36637
  for (const l of languages2.values()) langCounts[l] = (langCounts[l] ?? 0) + 1;
36627
36638
  const counts = db.graphCounts();
36628
36639
  const nodeCounts = db.graphNodeCounts();
36629
- const symbols = Object.entries(nodeCounts).filter(([k]) => !["file", "package", "person", "concept", "memory"].includes(k)).reduce((a, [, n]) => a + n, 0);
36640
+ const symbols = Object.entries(nodeCounts).filter(([k]) => !["file", "package", "person", "concept", "memory", "doc"].includes(k)).reduce((a, [, n]) => a + n, 0);
36630
36641
  const out = {
36631
36642
  project: {
36632
36643
  name: typeof pkg.name === "string" ? pkg.name : void 0,
@@ -39030,6 +39041,341 @@ var init_discover = __esm({
39030
39041
  }
39031
39042
  });
39032
39043
 
39044
+ // src/docs/category.ts
39045
+ function categoryInfo(id) {
39046
+ return BY_ID.get(id) ?? BY_ID.get("local");
39047
+ }
39048
+ function localCategory(s) {
39049
+ const title = s.title ?? "";
39050
+ for (const [re, cat] of TITLE_RULES) if (re.test(title)) return cat;
39051
+ for (const [re, cat] of NAME_RULES) if (re.test(s.name)) return cat;
39052
+ const path3 = s.path ?? "";
39053
+ if (/(^|\/)(adrs?|decisions)$/i.test(path3)) return "adr";
39054
+ const globs = s.glob ?? [];
39055
+ const hits = /* @__PURE__ */ new Map();
39056
+ for (const g of globs)
39057
+ for (const [re, cat] of GLOB_RULES)
39058
+ if (re.test(g)) {
39059
+ hits.set(cat, (hits.get(cat) ?? 0) + 1);
39060
+ break;
39061
+ }
39062
+ if (hits.size) return [...hits.entries()].sort((a, b) => b[1] - a[1])[0][0];
39063
+ if (/(^|\/)(docs?|documentation)$/i.test(path3)) return "guides";
39064
+ if (/README|CONTRIBUTING|CHANGELOG/i.test(path3)) return "readme";
39065
+ return "local";
39066
+ }
39067
+ function docCategory(s, entry) {
39068
+ if (s.kind === "path") return localCategory(s);
39069
+ if (entry) {
39070
+ const tier = entry.tier ?? "library";
39071
+ return tier === "service" ? "service" : tier === "infra" ? "infra" : "library";
39072
+ }
39073
+ const provider = s.provider ?? "";
39074
+ const i = provider.indexOf(":");
39075
+ const kind = i === -1 ? provider : provider.slice(0, i);
39076
+ switch (kind) {
39077
+ case "api":
39078
+ return "api";
39079
+ case "env":
39080
+ return "service";
39081
+ case "image":
39082
+ case "files":
39083
+ return "infra";
39084
+ case "npm":
39085
+ case "pypi":
39086
+ case "go":
39087
+ return "library";
39088
+ default:
39089
+ return "web";
39090
+ }
39091
+ }
39092
+ function categoryRank(id) {
39093
+ const i = DOC_CATEGORIES.findIndex((c) => c.id === id);
39094
+ return i === -1 ? DOC_CATEGORIES.length : i;
39095
+ }
39096
+ var DOC_CATEGORIES, BY_ID, GROUP_LABELS, TITLE_RULES, NAME_RULES, GLOB_RULES;
39097
+ var init_category = __esm({
39098
+ "src/docs/category.ts"() {
39099
+ "use strict";
39100
+ init_esm_shims();
39101
+ DOC_CATEGORIES = [
39102
+ {
39103
+ id: "readme",
39104
+ group: "project",
39105
+ label: "Readme & guides",
39106
+ blurb: "README, CONTRIBUTING, CHANGELOG and other top-level guides"
39107
+ },
39108
+ {
39109
+ id: "guides",
39110
+ group: "project",
39111
+ label: "Docs folders",
39112
+ blurb: "docs/ directories: handbooks, how-tos, references"
39113
+ },
39114
+ {
39115
+ id: "adr",
39116
+ group: "project",
39117
+ label: "Decision records",
39118
+ blurb: "architecture decision records"
39119
+ },
39120
+ {
39121
+ id: "agent-instructions",
39122
+ group: "project",
39123
+ label: "Agent instructions",
39124
+ blurb: "Copilot, Claude and Cursor instructions, agents and skills"
39125
+ },
39126
+ {
39127
+ id: "api-specs",
39128
+ group: "project",
39129
+ label: "API specs",
39130
+ blurb: "OpenAPI, Postman, GraphQL and protobuf definitions"
39131
+ },
39132
+ {
39133
+ id: "env-reference",
39134
+ group: "project",
39135
+ label: "Environment reference",
39136
+ blurb: ".env examples: every variable the project reads"
39137
+ },
39138
+ {
39139
+ id: "deploy-config",
39140
+ group: "project",
39141
+ label: "Deploy config",
39142
+ blurb: "Terraform, Kubernetes, Helm, Compose and CI workflows"
39143
+ },
39144
+ { id: "local", group: "project", label: "Other local files", blurb: "hand-registered paths" },
39145
+ {
39146
+ id: "service",
39147
+ group: "external",
39148
+ label: "Services",
39149
+ blurb: "third-party products and APIs the project integrates"
39150
+ },
39151
+ {
39152
+ id: "api",
39153
+ group: "external",
39154
+ label: "External APIs",
39155
+ blurb: "hosts the code calls, found by probing their docs"
39156
+ },
39157
+ {
39158
+ id: "infra",
39159
+ group: "external",
39160
+ label: "Infrastructure",
39161
+ blurb: "platforms the project runs on"
39162
+ },
39163
+ { id: "library", group: "external", label: "Libraries", blurb: "frameworks and packages" },
39164
+ { id: "web", group: "external", label: "Web pages", blurb: "hand-registered URLs" }
39165
+ ];
39166
+ BY_ID = new Map(DOC_CATEGORIES.map((c) => [c.id, c]));
39167
+ GROUP_LABELS = {
39168
+ project: "Project documentation",
39169
+ external: "External documentation"
39170
+ };
39171
+ TITLE_RULES = [
39172
+ [/^project readme/i, "readme"],
39173
+ [/^(docs|doc|documentation)\/ directory$/i, "guides"],
39174
+ [/decision records/i, "adr"],
39175
+ [/copilot|claude|cursor/i, "agent-instructions"],
39176
+ [/openapi|postman|graphql|protobuf/i, "api-specs"],
39177
+ [/environment variable/i, "env-reference"],
39178
+ [/terraform|kubernetes|helm|compose|workflow/i, "deploy-config"]
39179
+ ];
39180
+ NAME_RULES = [
39181
+ [/(^|-)readme$/, "readme"],
39182
+ [/(^|-)docs$/, "guides"],
39183
+ [/(^|-)adrs?$/, "adr"],
39184
+ [/(^|-)agent-instructions$/, "agent-instructions"],
39185
+ [/(^|-)api-specs$/, "api-specs"],
39186
+ [/(^|-)env-example$/, "env-reference"],
39187
+ [/(^|-)deploy-config$/, "deploy-config"]
39188
+ ];
39189
+ GLOB_RULES = [
39190
+ [/openapi|swagger|postman_collection|asyncapi|\.graphql$|\.proto$/i, "api-specs"],
39191
+ [/^\.env\./, "env-reference"],
39192
+ [
39193
+ /\.tf$|tfvars|^k8s\/|^helm\/|^charts\/|^manifests\/|compose.*\.ya?ml|\.github\/workflows/i,
39194
+ "deploy-config"
39195
+ ],
39196
+ [
39197
+ /copilot-instructions|\.github\/(instructions|agents|skills)|\.claude\/(agents|skills)|\.cursor\/rules|^(AGENTS|CLAUDE)\.md$/i,
39198
+ "agent-instructions"
39199
+ ],
39200
+ [/^(README|CONTRIBUTING|CHANGELOG|SECURITY)(\.md)?$/i, "readme"],
39201
+ [/^\*\*\/\*\.(md|mdx|rst|txt)$/i, "guides"]
39202
+ ];
39203
+ }
39204
+ });
39205
+
39206
+ // src/docs/graph.ts
39207
+ import { existsSync as existsSync21, statSync as statSync9 } from "fs";
39208
+ import { dirname as dirname10, posix, relative as relative8, sep as sep2 } from "path";
39209
+ function manifestRel(ctx) {
39210
+ return relative8(ctx.root, manifestPath(ctx)).split(sep2).join("/");
39211
+ }
39212
+ function packageCandidates(provider) {
39213
+ if (!provider) return [];
39214
+ const i = provider.indexOf(":");
39215
+ if (i === -1) return [];
39216
+ const kind = provider.slice(0, i);
39217
+ const name = provider.slice(i + 1).trim();
39218
+ if (!name) return [];
39219
+ if (kind === "npm" || kind === "go") return [pkgId(name)];
39220
+ if (kind === "pypi") {
39221
+ const lower = name.toLowerCase();
39222
+ return [.../* @__PURE__ */ new Set([pkgId(lower.replace(/-/g, "_")), pkgId(lower), pkgId(name)])];
39223
+ }
39224
+ return [];
39225
+ }
39226
+ function pathsIn(text3, pageRel, files) {
39227
+ const out = /* @__PURE__ */ new Set();
39228
+ for (const m of text3.matchAll(PATH_RE)) {
39229
+ const raw = m[1];
39230
+ const tries = [raw.replace(/^(\.\/)+/, "")];
39231
+ if (pageRel && raw.startsWith("."))
39232
+ tries.unshift(posix.normalize(posix.join(dirname10(pageRel), raw)));
39233
+ for (const t of tries) {
39234
+ if (files.has(t)) {
39235
+ out.add(t);
39236
+ break;
39237
+ }
39238
+ }
39239
+ }
39240
+ return [...out];
39241
+ }
39242
+ function pageRelOf(ctx, uri) {
39243
+ if (!uri?.startsWith("file://")) return void 0;
39244
+ const rel = relative8(ctx.root, uri.slice("file://".length)).split(sep2).join("/");
39245
+ return rel.startsWith("..") ? void 0 : rel;
39246
+ }
39247
+ function localEdges(ctx, db, src, files) {
39248
+ const id = docId(src.name);
39249
+ const hits = /* @__PURE__ */ new Map();
39250
+ for (const page of db.documentsBySource(docsTag(src.name), MAX_PAGES)) {
39251
+ const rel = pageRelOf(ctx, page.uri);
39252
+ if (rel && files.has(rel)) hits.set(rel, { via: "covers", count: 1 });
39253
+ const text3 = db.chunksOfDocument(page.id).map((c) => c.body).join("\n");
39254
+ for (const f of pathsIn(text3, rel, files)) {
39255
+ const h = hits.get(f);
39256
+ if (h) h.count++;
39257
+ else hits.set(f, { via: "mentions", count: 1 });
39258
+ }
39259
+ if (hits.size >= MAX_EDGES) break;
39260
+ }
39261
+ return [...hits].sort((a, b) => b[1].count - a[1].count).slice(0, MAX_EDGES).map(([rel, h]) => ({
39262
+ src: id,
39263
+ dst: `file:${rel}`,
39264
+ kind: DOCUMENTS_EDGE,
39265
+ source: "curated",
39266
+ confidence: h.via === "covers" ? 1 : 0.8,
39267
+ weight: Math.min(1, 0.5 + h.count / 10),
39268
+ meta: { via: h.via, count: h.count }
39269
+ }));
39270
+ }
39271
+ function packageEdges(src, packages) {
39272
+ const id = docId(src.name);
39273
+ const cands = packageCandidates(src.provider);
39274
+ if (!cands.length) return [];
39275
+ let targets = cands.filter((c) => packages.has(c));
39276
+ if (src.provider?.startsWith("go:"))
39277
+ targets = [...targets, ...[...packages].filter((p) => p.startsWith(`${cands[0]}/`))];
39278
+ if (!targets.length) targets = [cands[0]];
39279
+ return [...new Set(targets)].map((dst) => ({
39280
+ src: id,
39281
+ dst,
39282
+ kind: DOCUMENTS_EDGE,
39283
+ source: "curated",
39284
+ confidence: 1,
39285
+ weight: 1,
39286
+ meta: { via: "provider" }
39287
+ }));
39288
+ }
39289
+ function localDocPages(ctx, db) {
39290
+ const out = /* @__PURE__ */ new Map();
39291
+ for (const s of readManifest(ctx).sources) {
39292
+ if (s.kind !== "path") continue;
39293
+ for (const page of db.documentsBySource(docsTag(s.name), MAX_PAGES)) {
39294
+ const rel = pageRelOf(ctx, page.uri);
39295
+ if (!rel) continue;
39296
+ const names = out.get(rel) ?? [];
39297
+ if (!names.includes(s.name)) names.push(s.name);
39298
+ out.set(rel, names);
39299
+ }
39300
+ }
39301
+ return out;
39302
+ }
39303
+ function stampOf(ctx, db) {
39304
+ const file3 = manifestPath(ctx);
39305
+ const st = existsSync21(file3) ? statSync9(file3) : void 0;
39306
+ const counts = db.graphNodeCounts();
39307
+ const syncs = readManifest(ctx).sources.map((s) => syncState(db, s.name)?.at ?? 0).join(",");
39308
+ return `${st ? `${Math.floor(st.mtimeMs)}:${st.size}` : "none"}|${counts.file ?? 0}|${counts.package ?? 0}|${syncs}`;
39309
+ }
39310
+ function syncDocsGraph(ctx, db) {
39311
+ const rel = manifestRel(ctx);
39312
+ const m = readManifest(ctx);
39313
+ const catalog = mergedCatalog(ctx.config.docs.catalog);
39314
+ const files = new Set(
39315
+ db.graphFileNodes().filter((n) => n.source === "tree-sitter" && n.path).map((n) => n.path)
39316
+ );
39317
+ const packages = new Set(db.graphNodesByKind(["package"]).map((n) => n.id));
39318
+ const nodes = [];
39319
+ const edges = [];
39320
+ for (const s of m.sources) {
39321
+ const category = docCategory(
39322
+ s,
39323
+ s.provider ? catalogForProvider(s.provider, catalog) : void 0
39324
+ );
39325
+ nodes.push({
39326
+ id: docId(s.name),
39327
+ kind: "doc",
39328
+ name: s.name,
39329
+ path: rel,
39330
+ source: "curated",
39331
+ meta: {
39332
+ category,
39333
+ group: categoryInfo(category).group,
39334
+ sourceKind: s.kind,
39335
+ ...s.provider ? { provider: s.provider } : {},
39336
+ ...s.title ? { title: s.title } : {},
39337
+ ...s.tags?.length ? { tags: s.tags } : {},
39338
+ ...s.kind === "path" ? { target: s.path } : { url: s.urls?.[0] },
39339
+ documents: db.countDocuments(docsTag(s.name))
39340
+ }
39341
+ });
39342
+ edges.push(...packageEdges(s, packages));
39343
+ if (s.kind === "path") edges.push(...localEdges(ctx, db, s, files));
39344
+ }
39345
+ db.graphDeleteForPath("curated", rel, { keepFile: true });
39346
+ if (nodes.length) db.graphUpsertNodes(nodes);
39347
+ if (edges.length) db.graphUpsertEdges(edges);
39348
+ db.metaSet(STAMP_KEY, stampOf(ctx, db));
39349
+ return { sources: nodes.length, edges: edges.length };
39350
+ }
39351
+ function ensureDocsGraph(ctx, db) {
39352
+ if (db.metaGet(STAMP_KEY) === stampOf(ctx, db)) return false;
39353
+ syncDocsGraph(ctx, db);
39354
+ return true;
39355
+ }
39356
+ function syncDocsGraphQuietly(ctx, db) {
39357
+ try {
39358
+ syncDocsGraph(ctx, db);
39359
+ } catch {
39360
+ }
39361
+ }
39362
+ var DOCUMENTS_EDGE, MAX_PAGES, MAX_EDGES, PATH_RE, STAMP_KEY;
39363
+ var init_graph = __esm({
39364
+ "src/docs/graph.ts"() {
39365
+ "use strict";
39366
+ init_esm_shims();
39367
+ init_ids();
39368
+ init_catalog();
39369
+ init_category();
39370
+ init_manifest();
39371
+ DOCUMENTS_EDGE = "documents";
39372
+ MAX_PAGES = 500;
39373
+ MAX_EDGES = 400;
39374
+ PATH_RE = /(?:^|[\s(`'"[<])((?:\.{1,2}\/)*(?:[\w@.-]+\/)+[\w@.-]+\.[A-Za-z0-9]{1,8})(?=$|[\s)`'"\]>:#,;!?]|\.(?:\s|$))/gm;
39375
+ STAMP_KEY = "docs.graph.stamp";
39376
+ }
39377
+ });
39378
+
39033
39379
  // src/docs/resolve.ts
39034
39380
  async function getJson(url2, opts) {
39035
39381
  const f = opts.fetchImpl ?? fetch;
@@ -39914,6 +40260,7 @@ async function syncSources(ctx, db, names, opts = {}) {
39914
40260
  });
39915
40261
  }
39916
40262
  for (const src of wanted) out.push(await syncSource(ctx, db, src, opts));
40263
+ syncDocsGraphQuietly(ctx, db);
39917
40264
  return out;
39918
40265
  }
39919
40266
  function formatSync(results) {
@@ -39943,6 +40290,7 @@ var init_sync = __esm({
39943
40290
  init_queue();
39944
40291
  init_fetcher();
39945
40292
  init_indexer();
40293
+ init_graph();
39946
40294
  init_manifest();
39947
40295
  init_specs();
39948
40296
  SKIP_EXT2 = /\.(png|jpe?g|gif|svg|webp|ico|css|js|mjs|map|zip|gz|tgz|tar|pdf|woff2?|ttf|otf|eot|mp4|mp3|webm|xml|rss|atom|wasm)$/i;
@@ -40086,12 +40434,14 @@ async function addDocSources(ctx, db, inputs, opts = {}) {
40086
40434
  const names = [...out.added, ...out.updated];
40087
40435
  if (opts.sync !== false && names.length)
40088
40436
  out.sync = await syncSources(ctx, db, names, { force: opts.force });
40437
+ else if (names.length) syncDocsGraphQuietly(ctx, db);
40089
40438
  return out;
40090
40439
  }
40091
40440
  function removeDocSource(ctx, db, name) {
40092
40441
  const removed = removeSource(ctx, name);
40093
40442
  const documents = db.deleteDocumentsBySource(docsTag(name));
40094
40443
  clearSyncState(db, name);
40444
+ if (removed) syncDocsGraphQuietly(ctx, db);
40095
40445
  return { removed, documents };
40096
40446
  }
40097
40447
  function formatAdd(o) {
@@ -40125,6 +40475,7 @@ var init_add = __esm({
40125
40475
  init_esm_shims();
40126
40476
  init_catalog();
40127
40477
  init_discover();
40478
+ init_graph();
40128
40479
  init_manifest();
40129
40480
  init_resolve2();
40130
40481
  init_sync();
@@ -40141,168 +40492,6 @@ var init_add = __esm({
40141
40492
  }
40142
40493
  });
40143
40494
 
40144
- // src/docs/category.ts
40145
- function categoryInfo(id) {
40146
- return BY_ID.get(id) ?? BY_ID.get("local");
40147
- }
40148
- function localCategory(s) {
40149
- const title = s.title ?? "";
40150
- for (const [re, cat] of TITLE_RULES) if (re.test(title)) return cat;
40151
- for (const [re, cat] of NAME_RULES) if (re.test(s.name)) return cat;
40152
- const path3 = s.path ?? "";
40153
- if (/(^|\/)(adrs?|decisions)$/i.test(path3)) return "adr";
40154
- const globs = s.glob ?? [];
40155
- const hits = /* @__PURE__ */ new Map();
40156
- for (const g of globs)
40157
- for (const [re, cat] of GLOB_RULES)
40158
- if (re.test(g)) {
40159
- hits.set(cat, (hits.get(cat) ?? 0) + 1);
40160
- break;
40161
- }
40162
- if (hits.size) return [...hits.entries()].sort((a, b) => b[1] - a[1])[0][0];
40163
- if (/(^|\/)(docs?|documentation)$/i.test(path3)) return "guides";
40164
- if (/README|CONTRIBUTING|CHANGELOG/i.test(path3)) return "readme";
40165
- return "local";
40166
- }
40167
- function docCategory(s, entry) {
40168
- if (s.kind === "path") return localCategory(s);
40169
- if (entry) {
40170
- const tier = entry.tier ?? "library";
40171
- return tier === "service" ? "service" : tier === "infra" ? "infra" : "library";
40172
- }
40173
- const provider = s.provider ?? "";
40174
- const i = provider.indexOf(":");
40175
- const kind = i === -1 ? provider : provider.slice(0, i);
40176
- switch (kind) {
40177
- case "api":
40178
- return "api";
40179
- case "env":
40180
- return "service";
40181
- case "image":
40182
- case "files":
40183
- return "infra";
40184
- case "npm":
40185
- case "pypi":
40186
- case "go":
40187
- return "library";
40188
- default:
40189
- return "web";
40190
- }
40191
- }
40192
- function categoryRank(id) {
40193
- const i = DOC_CATEGORIES.findIndex((c) => c.id === id);
40194
- return i === -1 ? DOC_CATEGORIES.length : i;
40195
- }
40196
- var DOC_CATEGORIES, BY_ID, GROUP_LABELS, TITLE_RULES, NAME_RULES, GLOB_RULES;
40197
- var init_category = __esm({
40198
- "src/docs/category.ts"() {
40199
- "use strict";
40200
- init_esm_shims();
40201
- DOC_CATEGORIES = [
40202
- {
40203
- id: "readme",
40204
- group: "project",
40205
- label: "Readme & guides",
40206
- blurb: "README, CONTRIBUTING, CHANGELOG and other top-level guides"
40207
- },
40208
- {
40209
- id: "guides",
40210
- group: "project",
40211
- label: "Docs folders",
40212
- blurb: "docs/ directories: handbooks, how-tos, references"
40213
- },
40214
- {
40215
- id: "adr",
40216
- group: "project",
40217
- label: "Decision records",
40218
- blurb: "architecture decision records"
40219
- },
40220
- {
40221
- id: "agent-instructions",
40222
- group: "project",
40223
- label: "Agent instructions",
40224
- blurb: "Copilot, Claude and Cursor instructions, agents and skills"
40225
- },
40226
- {
40227
- id: "api-specs",
40228
- group: "project",
40229
- label: "API specs",
40230
- blurb: "OpenAPI, Postman, GraphQL and protobuf definitions"
40231
- },
40232
- {
40233
- id: "env-reference",
40234
- group: "project",
40235
- label: "Environment reference",
40236
- blurb: ".env examples: every variable the project reads"
40237
- },
40238
- {
40239
- id: "deploy-config",
40240
- group: "project",
40241
- label: "Deploy config",
40242
- blurb: "Terraform, Kubernetes, Helm, Compose and CI workflows"
40243
- },
40244
- { id: "local", group: "project", label: "Other local files", blurb: "hand-registered paths" },
40245
- {
40246
- id: "service",
40247
- group: "external",
40248
- label: "Services",
40249
- blurb: "third-party products and APIs the project integrates"
40250
- },
40251
- {
40252
- id: "api",
40253
- group: "external",
40254
- label: "External APIs",
40255
- blurb: "hosts the code calls, found by probing their docs"
40256
- },
40257
- {
40258
- id: "infra",
40259
- group: "external",
40260
- label: "Infrastructure",
40261
- blurb: "platforms the project runs on"
40262
- },
40263
- { id: "library", group: "external", label: "Libraries", blurb: "frameworks and packages" },
40264
- { id: "web", group: "external", label: "Web pages", blurb: "hand-registered URLs" }
40265
- ];
40266
- BY_ID = new Map(DOC_CATEGORIES.map((c) => [c.id, c]));
40267
- GROUP_LABELS = {
40268
- project: "Project documentation",
40269
- external: "External documentation"
40270
- };
40271
- TITLE_RULES = [
40272
- [/^project readme/i, "readme"],
40273
- [/^(docs|doc|documentation)\/ directory$/i, "guides"],
40274
- [/decision records/i, "adr"],
40275
- [/copilot|claude|cursor/i, "agent-instructions"],
40276
- [/openapi|postman|graphql|protobuf/i, "api-specs"],
40277
- [/environment variable/i, "env-reference"],
40278
- [/terraform|kubernetes|helm|compose|workflow/i, "deploy-config"]
40279
- ];
40280
- NAME_RULES = [
40281
- [/(^|-)readme$/, "readme"],
40282
- [/(^|-)docs$/, "guides"],
40283
- [/(^|-)adrs?$/, "adr"],
40284
- [/(^|-)agent-instructions$/, "agent-instructions"],
40285
- [/(^|-)api-specs$/, "api-specs"],
40286
- [/(^|-)env-example$/, "env-reference"],
40287
- [/(^|-)deploy-config$/, "deploy-config"]
40288
- ];
40289
- GLOB_RULES = [
40290
- [/openapi|swagger|postman_collection|asyncapi|\.graphql$|\.proto$/i, "api-specs"],
40291
- [/^\.env\./, "env-reference"],
40292
- [
40293
- /\.tf$|tfvars|^k8s\/|^helm\/|^charts\/|^manifests\/|compose.*\.ya?ml|\.github\/workflows/i,
40294
- "deploy-config"
40295
- ],
40296
- [
40297
- /copilot-instructions|\.github\/(instructions|agents|skills)|\.claude\/(agents|skills)|\.cursor\/rules|^(AGENTS|CLAUDE)\.md$/i,
40298
- "agent-instructions"
40299
- ],
40300
- [/^(README|CONTRIBUTING|CHANGELOG|SECURITY)(\.md)?$/i, "readme"],
40301
- [/^\*\*\/\*\.(md|mdx|rst|txt)$/i, "guides"]
40302
- ];
40303
- }
40304
- });
40305
-
40306
40495
  // src/tools/em_docs.ts
40307
40496
  var em_docs_exports = {};
40308
40497
  __export(em_docs_exports, {
@@ -41003,8 +41192,8 @@ var init_em_search = __esm({
41003
41192
  });
41004
41193
 
41005
41194
  // src/ui/api.ts
41006
- import { existsSync as existsSync21, readdirSync as readdirSync8 } from "fs";
41007
- import { join as join26, relative as relative8 } from "path";
41195
+ import { existsSync as existsSync22, readdirSync as readdirSync8 } from "fs";
41196
+ import { join as join26, relative as relative9 } from "path";
41008
41197
  function listProjects() {
41009
41198
  const dir = paths().projects;
41010
41199
  const out = [];
@@ -41037,7 +41226,7 @@ function statsPayload(ctx) {
41037
41226
  const sessionId = getCurrentSession(ctx);
41038
41227
  const g = globalDb();
41039
41228
  const onboardingDir = join26(memoriesDir(ctx), "onboarding");
41040
- const onboarding = existsSync21(onboardingDir) ? readdirSync8(onboardingDir).filter((f) => f.endsWith(".md")).map((f) => f.slice(0, -3)) : [];
41229
+ const onboarding = existsSync22(onboardingDir) ? readdirSync8(onboardingDir).filter((f) => f.endsWith(".md")).map((f) => f.slice(0, -3)) : [];
41041
41230
  return {
41042
41231
  project: ctx.root,
41043
41232
  projectId: ctx.id,
@@ -41229,7 +41418,7 @@ function docsPayload(ctx) {
41229
41418
  const known = new Set(m.sources.map((s) => docsTag(s.name)));
41230
41419
  const orphans2 = [...stats.values()].filter((s) => !known.has(s.source)).map((s) => ({ source: s.source, documents: s.documents, chunks: s.chunks }));
41231
41420
  return {
41232
- manifest: relative8(ctx.root, manifestPath(ctx)),
41421
+ manifest: relative9(ctx.root, manifestPath(ctx)),
41233
41422
  sources,
41234
41423
  orphans: orphans2,
41235
41424
  totals: {
@@ -41575,7 +41764,7 @@ var init_namepath = __esm({
41575
41764
  });
41576
41765
 
41577
41766
  // src/code/symbols.ts
41578
- import { existsSync as existsSync22, readFileSync as readFileSync20 } from "fs";
41767
+ import { existsSync as existsSync23, readFileSync as readFileSync20 } from "fs";
41579
41768
  import { join as join27 } from "path";
41580
41769
  function isExported2(n) {
41581
41770
  if (!n.meta) return false;
@@ -41612,8 +41801,8 @@ var init_symbols2 = __esm({
41612
41801
  });
41613
41802
 
41614
41803
  // src/ui/graph-api.ts
41615
- import { readFileSync as readFileSync21, realpathSync as realpathSync2, statSync as statSync9 } from "fs";
41616
- import { relative as relative9, resolve as resolve7 } from "path";
41804
+ import { readFileSync as readFileSync21, realpathSync as realpathSync2, statSync as statSync10 } from "fs";
41805
+ import { relative as relative10, resolve as resolve7 } from "path";
41617
41806
  function metaOf2(row) {
41618
41807
  if (!row?.meta) return {};
41619
41808
  try {
@@ -41675,16 +41864,16 @@ function treePayload(ctx) {
41675
41864
  function filePayload(ctx, rel) {
41676
41865
  const clean = rel.replace(/^\.\//, "");
41677
41866
  const abs = resolve7(ctx.root, clean);
41678
- if (relative9(ctx.root, abs).startsWith("..")) throw new Error("path outside the project");
41867
+ if (relative10(ctx.root, abs).startsWith("..")) throw new Error("path outside the project");
41679
41868
  let real = abs;
41680
41869
  try {
41681
41870
  real = realpathSync2(abs);
41682
41871
  } catch {
41683
41872
  return { error: "not found", path: clean };
41684
41873
  }
41685
- if (relative9(realpathSync2(ctx.root), real).startsWith(".."))
41874
+ if (relative10(realpathSync2(ctx.root), real).startsWith(".."))
41686
41875
  throw new Error("path outside the project");
41687
- const st = statSync9(real);
41876
+ const st = statSync10(real);
41688
41877
  if (!st.isFile()) return { error: "not a file", path: clean };
41689
41878
  const db = getStorage(ctx);
41690
41879
  const row = db.fileIndexGet(clean);
@@ -43269,6 +43458,693 @@ var init_history_api = __esm({
43269
43458
  }
43270
43459
  });
43271
43460
 
43461
+ // src/history/lineage.ts
43462
+ function mergedBranchName(subject) {
43463
+ const pr = PR_MERGE.exec(subject);
43464
+ if (pr) return { name: pr[2], pr: Number(pr[1]) };
43465
+ const br = BRANCH_MERGE.exec(subject);
43466
+ if (br) return { name: br[1].replace(/^origin\//, "") };
43467
+ return void 0;
43468
+ }
43469
+ function firstParents(m, sha, stop, max) {
43470
+ const out = [];
43471
+ let cur = sha ? m.bySha.get(sha) : void 0;
43472
+ while (cur && !stop.has(cur.sha) && out.length < max) {
43473
+ out.push(cur);
43474
+ cur = cur.parents[0] ? m.bySha.get(cur.parents[0]) : void 0;
43475
+ }
43476
+ return out;
43477
+ }
43478
+ function branchLineage(m, opts = {}) {
43479
+ const max = opts.maxPerBranch ?? 500;
43480
+ const def = m.branches.find((b) => b.isDefault && !b.remote) ?? m.branches.find((b) => b.isDefault);
43481
+ const defaultName = def ? def.remote ? def.name.replace(/^[^/]+\//, "") : def.name : m.base ?? "main";
43482
+ const mainline = firstParents(m, def?.sha ?? m.head, /* @__PURE__ */ new Set(), 1e5);
43483
+ const onMain = new Set(mainline.map((c) => c.sha));
43484
+ const byName = /* @__PURE__ */ new Map();
43485
+ const entry = (name) => {
43486
+ let b = byName.get(name);
43487
+ if (!b) {
43488
+ b = {
43489
+ name,
43490
+ commits: [],
43491
+ tipAt: 0,
43492
+ isDefault: name === defaultName,
43493
+ live: false,
43494
+ merged: false,
43495
+ prs: [],
43496
+ merges: []
43497
+ };
43498
+ byName.set(name, b);
43499
+ }
43500
+ return b;
43501
+ };
43502
+ const own2 = /* @__PURE__ */ new Map();
43503
+ const add = (name, cs) => {
43504
+ const set2 = own2.get(name) ?? /* @__PURE__ */ new Set();
43505
+ for (const c of cs) if (!c.merge) set2.add(c.sha);
43506
+ own2.set(name, set2);
43507
+ const b = entry(name);
43508
+ for (const c of cs) b.tipAt = Math.max(b.tipAt, c.at);
43509
+ };
43510
+ add(defaultName, mainline);
43511
+ entry(defaultName).live = !!def;
43512
+ for (const c of mainline) {
43513
+ if (!c.merge || c.parents.length < 2) continue;
43514
+ const found = mergedBranchName(c.subject);
43515
+ const name = found?.name ?? (c.pr ? `PR #${c.pr}` : `merge ${c.short}`);
43516
+ if (name === defaultName) continue;
43517
+ add(name, firstParents(m, c.parents[1], onMain, max));
43518
+ const b = entry(name);
43519
+ b.merged = true;
43520
+ b.merges.push(c.sha);
43521
+ const pr = found?.pr ?? c.pr;
43522
+ if (pr && !b.prs.includes(pr)) b.prs.push(pr);
43523
+ }
43524
+ const locals = new Set(m.branches.filter((b) => !b.remote).map((b) => b.name));
43525
+ for (const ref of [...m.branches].sort((a, b) => Number(a.remote) - Number(b.remote))) {
43526
+ if (ref.isDefault) continue;
43527
+ const name = ref.remote ? ref.name.replace(/^[^/]+\//, "") : ref.name;
43528
+ if (ref.remote && (locals.has(name) || name === "HEAD" || !name)) continue;
43529
+ if (name === defaultName) continue;
43530
+ add(name, firstParents(m, ref.sha, onMain, max));
43531
+ const b = entry(name);
43532
+ b.live = true;
43533
+ b.merged ||= ref.merged;
43534
+ if (!b.tipAt) b.tipAt = ref.at;
43535
+ }
43536
+ for (const [name, set2] of own2) {
43537
+ entry(name).commits = [...set2].map((s) => m.bySha.get(s)).sort((a, b) => b.at - a.at).map((c) => c.sha);
43538
+ }
43539
+ const branches = [...byName.values()];
43540
+ const ownerOf = /* @__PURE__ */ new Map();
43541
+ for (const b of branches)
43542
+ for (const s of b.commits) ownerOf.set(s, [...ownerOf.get(s) ?? [], b.name]);
43543
+ return { defaultName, branches, ownerOf };
43544
+ }
43545
+ var PR_MERGE, BRANCH_MERGE;
43546
+ var init_lineage = __esm({
43547
+ "src/history/lineage.ts"() {
43548
+ "use strict";
43549
+ init_esm_shims();
43550
+ PR_MERGE = /^Merge pull request #(\d+) from [^/\s]+\/(\S+)/;
43551
+ BRANCH_MERGE = /^Merge (?:remote-tracking )?branch '([^']+)'/;
43552
+ }
43553
+ });
43554
+
43555
+ // src/ui/history-graph-api.ts
43556
+ import { existsSync as existsSync24 } from "fs";
43557
+ import { join as join28 } from "path";
43558
+ function parseList(raw) {
43559
+ if (!raw) return [];
43560
+ try {
43561
+ const v = JSON.parse(raw);
43562
+ return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
43563
+ } catch {
43564
+ return [];
43565
+ }
43566
+ }
43567
+ function metaOf3(raw) {
43568
+ if (!raw) return {};
43569
+ try {
43570
+ return JSON.parse(raw);
43571
+ } catch {
43572
+ return {};
43573
+ }
43574
+ }
43575
+ function historyGraphKnowledgePayload(ctx, opts = {}) {
43576
+ const db = getStorage(ctx);
43577
+ const m = getHistory(ctx, db);
43578
+ if (!m) return NO_GIT2;
43579
+ reconcile(ctx, db);
43580
+ try {
43581
+ ensureDocsGraph(ctx, db);
43582
+ } catch {
43583
+ }
43584
+ return buildHistoryGraph(ctx, db, m, opts);
43585
+ }
43586
+ function buildHistoryGraph(ctx, db, m, opts = {}) {
43587
+ const nBranches = Math.max(1, Math.min(50, opts.branches ?? 8));
43588
+ const perBranch = Math.max(1, Math.min(200, opts.commitsPerBranch ?? 25));
43589
+ const perCommit = Math.max(1, Math.min(200, opts.filesPerCommit ?? 12));
43590
+ const limit = Math.max(20, Math.min(2e3, opts.limit ?? 400));
43591
+ const focus = opts.focus?.trim() || null;
43592
+ const { defaultName, branches, ownerOf } = branchLineage(m);
43593
+ const branchBy = new Map(branches.map((b) => [b.name, b]));
43594
+ const memDir = `${memoriesRel(ctx)}/`;
43595
+ const memoryOfPath = (p) => p.startsWith(memDir) && p.endsWith(".md") ? p.slice(memDir.length, -3) : void 0;
43596
+ const memRows = new Map(
43597
+ db.memoryList({ limit: 1e5 }).filter((r) => r.name).map((r) => [r.name, r])
43598
+ );
43599
+ const memFiles = /* @__PURE__ */ new Map();
43600
+ for (const [name, row] of memRows) {
43601
+ const paths2 = /* @__PURE__ */ new Set();
43602
+ for (const id of parseList(row.node_ids)) {
43603
+ const f = fileOf(id);
43604
+ if (f) paths2.add(f);
43605
+ }
43606
+ if (paths2.size) memFiles.set(name, [...paths2]);
43607
+ }
43608
+ const nonMerge = (cs) => cs.filter((c) => !c.merge);
43609
+ const touching = (paths2, cap) => {
43610
+ const out = /* @__PURE__ */ new Map();
43611
+ for (const p of paths2) for (const c of nonMerge(commitsForFile(m, p))) out.set(c.sha, c);
43612
+ return [...out.values()].sort((a, b) => b.at - a.at).slice(0, cap);
43613
+ };
43614
+ const commitSet = /* @__PURE__ */ new Map();
43615
+ const branchSet = /* @__PURE__ */ new Set();
43616
+ let onlyFiles = null;
43617
+ const addCommits = (cs) => {
43618
+ for (const c of cs) commitSet.set(c.sha, c);
43619
+ };
43620
+ let focusId = null;
43621
+ let followOwners = true;
43622
+ const kind = focus ? parseId(focus).kind : null;
43623
+ if (!focus) {
43624
+ followOwners = false;
43625
+ const feature = branches.filter((b) => !b.isDefault && b.commits.length).sort((a, b) => b.tipAt - a.tipAt).slice(0, nBranches);
43626
+ for (const b of feature) {
43627
+ branchSet.add(b.name);
43628
+ addCommits(b.commits.slice(0, perBranch).map((s) => m.bySha.get(s)));
43629
+ }
43630
+ const def = branchBy.get(defaultName);
43631
+ if (def) {
43632
+ branchSet.add(def.name);
43633
+ const oldest = Math.min(...feature.map((b) => b.tipAt), Date.now());
43634
+ addCommits(
43635
+ def.commits.map((s) => m.bySha.get(s)).filter((c, i) => i < perBranch && (feature.length < nBranches || c.at >= oldest))
43636
+ );
43637
+ }
43638
+ } else if (kind === "branch") {
43639
+ const name = focus.slice("branch:".length);
43640
+ const b = branchBy.get(name) ?? branches.find((x) => x.name.endsWith(`/${name}`));
43641
+ if (b) {
43642
+ focusId = branchId(b.name);
43643
+ branchSet.add(b.name);
43644
+ if (b.merged) branchSet.add(defaultName);
43645
+ addCommits(b.commits.slice(0, 150).map((s) => m.bySha.get(s)));
43646
+ }
43647
+ followOwners = false;
43648
+ } else if (kind === "commit") {
43649
+ const c = resolveCommit(m, focus.slice("commit:".length));
43650
+ if (c) {
43651
+ focusId = commitId(c.sha);
43652
+ addCommits([c]);
43653
+ addCommits([...c.origins, ...c.blamedBy].flatMap((s) => m.bySha.get(s) ?? []));
43654
+ }
43655
+ } else if (kind === "memory") {
43656
+ const name = focus.slice("memory:".length);
43657
+ focusId = memoryId(name);
43658
+ const paths2 = memFiles.get(name) ?? [];
43659
+ onlyFiles = new Set(paths2);
43660
+ const own2 = `${memDir}${name}.md`;
43661
+ addCommits([...touching(paths2, 60), ...touching([own2], 20)]);
43662
+ } else if (kind === "doc") {
43663
+ focusId = focus;
43664
+ const paths2 = db.graphEdgesFrom([focus], ["documents"]).map((e) => fileOf(e.dst)).filter((p) => !!p);
43665
+ onlyFiles = new Set(paths2);
43666
+ addCommits(touching(paths2, 60));
43667
+ } else {
43668
+ const path3 = fileOf(focus) ?? focus.replace(/^file:/, "");
43669
+ focusId = fileId(path3);
43670
+ onlyFiles = /* @__PURE__ */ new Set([path3]);
43671
+ addCommits(touching([path3], 60));
43672
+ }
43673
+ if (followOwners)
43674
+ for (const sha of commitSet.keys())
43675
+ for (const name of ownerOf.get(sha) ?? []) branchSet.add(name);
43676
+ const edges = /* @__PURE__ */ new Map();
43677
+ const addEdge = (src, dst, kind2, weight = 1) => {
43678
+ if (src === dst) return;
43679
+ const key = `${src}|${dst}|${kind2}`;
43680
+ if (!edges.has(key)) edges.set(key, { src, dst, kind: kind2, weight });
43681
+ };
43682
+ for (const name of branchSet) {
43683
+ const b = branchBy.get(name);
43684
+ if (!b) continue;
43685
+ for (const sha of b.commits)
43686
+ if (commitSet.has(sha)) addEdge(branchId(name), commitId(sha), "contains");
43687
+ if (b.merged && !b.isDefault && branchSet.has(defaultName))
43688
+ addEdge(branchId(name), branchId(defaultName), "merged_into");
43689
+ }
43690
+ const touchCount = /* @__PURE__ */ new Map();
43691
+ const commitFiles = /* @__PURE__ */ new Map();
43692
+ const recorded = /* @__PURE__ */ new Map();
43693
+ for (const c of commitSet.values()) {
43694
+ const mems = [];
43695
+ const paths2 = [];
43696
+ for (const f of [...c.files].sort((a, b) => b.added + b.deleted - (a.added + a.deleted))) {
43697
+ const mem = memoryOfPath(f.path);
43698
+ if (mem) {
43699
+ mems.push(mem);
43700
+ continue;
43701
+ }
43702
+ if (LOCKFILE.test(f.path)) continue;
43703
+ if (onlyFiles && !onlyFiles.has(f.path)) continue;
43704
+ paths2.push(f.path);
43705
+ }
43706
+ const kept2 = paths2.slice(0, perCommit);
43707
+ commitFiles.set(c.sha, kept2);
43708
+ for (const p of kept2) touchCount.set(p, (touchCount.get(p) ?? 0) + 1);
43709
+ if (mems.length) recorded.set(c.sha, mems);
43710
+ }
43711
+ for (const p of onlyFiles ?? []) if (!touchCount.has(p)) touchCount.set(p, 0);
43712
+ const fileBudget = Math.max(10, limit - branchSet.size - commitSet.size - 40);
43713
+ const files = new Set(
43714
+ [...touchCount.entries()].sort(
43715
+ (a, b) => Number(onlyFiles?.has(b[0]) ?? false) - Number(onlyFiles?.has(a[0]) ?? false) || b[1] - a[1] || (m.files.get(b[0])?.risk ?? 0) - (m.files.get(a[0])?.risk ?? 0) || a[0].localeCompare(b[0])
43716
+ ).slice(0, fileBudget).map(([p]) => p)
43717
+ );
43718
+ for (const [sha, paths2] of commitFiles)
43719
+ for (const p of paths2) if (files.has(p)) addEdge(commitId(sha), fileId(p), "touches");
43720
+ const memories = /* @__PURE__ */ new Set();
43721
+ for (const [sha, mems] of recorded)
43722
+ for (const name of mems) {
43723
+ memories.add(name);
43724
+ addEdge(commitId(sha), memoryId(name), "records");
43725
+ }
43726
+ for (const [name, paths2] of memFiles)
43727
+ for (const p of paths2)
43728
+ if (files.has(p)) {
43729
+ memories.add(name);
43730
+ addEdge(memoryId(name), fileId(p), "mentions");
43731
+ }
43732
+ if (kind === "memory" && focus) memories.add(focus.slice("memory:".length));
43733
+ const docs = /* @__PURE__ */ new Set();
43734
+ if (files.size)
43735
+ for (const e of db.graphEdgesTo(
43736
+ [...files].map((p) => fileId(p)),
43737
+ ["documents"]
43738
+ )) {
43739
+ docs.add(e.src);
43740
+ addEdge(e.src, e.dst, "documents");
43741
+ }
43742
+ if (kind === "doc" && focus) docs.add(focus);
43743
+ for (const l of m.links)
43744
+ if (commitSet.has(l.fix) && commitSet.has(l.origin))
43745
+ addEdge(commitId(l.fix), commitId(l.origin), "fixes", Math.min(3, 1 + l.lines / 20));
43746
+ const nodes = [];
43747
+ for (const name of branchSet) {
43748
+ const b = branchBy.get(name);
43749
+ if (!b) continue;
43750
+ const id = branchId(name);
43751
+ nodes.push({
43752
+ id,
43753
+ label: name,
43754
+ kind: "branch",
43755
+ group: "branch",
43756
+ path: null,
43757
+ line: null,
43758
+ root: id === focusId,
43759
+ at: b.tipAt,
43760
+ commits: b.commits.length,
43761
+ merged: b.merged,
43762
+ live: b.live,
43763
+ isDefault: b.isDefault,
43764
+ prs: b.prs.length ? b.prs : void 0
43765
+ });
43766
+ }
43767
+ for (const c of commitSet.values()) {
43768
+ const id = commitId(c.sha);
43769
+ nodes.push({
43770
+ id,
43771
+ label: c.subject,
43772
+ kind: "commit",
43773
+ group: "commit",
43774
+ path: null,
43775
+ line: null,
43776
+ root: id === focusId,
43777
+ sub: c.kind,
43778
+ bucket: c.bucket,
43779
+ at: c.at,
43780
+ author: c.author,
43781
+ short: c.short,
43782
+ risk: c.risk
43783
+ });
43784
+ }
43785
+ for (const p of files) {
43786
+ const id = fileId(p);
43787
+ const h = m.files.get(p);
43788
+ nodes.push({
43789
+ id,
43790
+ label: p,
43791
+ kind: "file",
43792
+ group: "file",
43793
+ path: p,
43794
+ line: null,
43795
+ root: id === focusId,
43796
+ missing: existsSync24(join28(ctx.root, p)) ? void 0 : true,
43797
+ risk: h?.risk,
43798
+ fixes: h?.fixes
43799
+ });
43800
+ }
43801
+ for (const name of memories) {
43802
+ const id = memoryId(name);
43803
+ const row = memRows.get(name);
43804
+ nodes.push({
43805
+ id,
43806
+ label: name,
43807
+ kind: "memory",
43808
+ group: "memory",
43809
+ path: row?.path ?? null,
43810
+ line: null,
43811
+ root: id === focusId,
43812
+ missing: row ? void 0 : true,
43813
+ sub: row?.kind,
43814
+ status: row?.status ?? (row?.kind === "decision" ? "accepted" : void 0),
43815
+ detail: name.includes("/") ? `${name.split("/")[0]}/` : void 0
43816
+ });
43817
+ }
43818
+ const docRows = new Map(db.graphGetNodes([...docs]).map((n) => [n.id, n]));
43819
+ for (const id of docs) {
43820
+ const row = docRows.get(id);
43821
+ const meta4 = metaOf3(row?.meta);
43822
+ const cat = typeof meta4.category === "string" ? meta4.category : void 0;
43823
+ nodes.push({
43824
+ id,
43825
+ label: row?.name ?? id.slice("doc:".length),
43826
+ kind: "doc",
43827
+ group: "doc",
43828
+ path: row?.path ?? null,
43829
+ line: null,
43830
+ root: id === focusId,
43831
+ missing: row ? void 0 : true,
43832
+ sub: cat,
43833
+ detail: cat ? categoryInfo(cat).label : void 0
43834
+ });
43835
+ }
43836
+ if (focusId && !nodes.some((n) => n.id === focusId)) {
43837
+ const p = parseId(focusId);
43838
+ nodes.push({
43839
+ id: focusId,
43840
+ label: p.rest,
43841
+ kind: p.kind,
43842
+ group: ["branch", "commit", "memory", "doc"].includes(p.kind) ? p.kind : "file",
43843
+ path: p.kind === "file" ? p.rest : null,
43844
+ line: null,
43845
+ root: true,
43846
+ missing: true
43847
+ });
43848
+ }
43849
+ for (const n of nodes) if (n.root) n.seed = true;
43850
+ const deg = /* @__PURE__ */ new Map();
43851
+ for (const e of edges.values()) {
43852
+ deg.set(e.src, (deg.get(e.src) ?? 0) + 1);
43853
+ deg.set(e.dst, (deg.get(e.dst) ?? 0) + 1);
43854
+ }
43855
+ const RANK = {
43856
+ branch: 1,
43857
+ commit: 2,
43858
+ memory: 3,
43859
+ doc: 3,
43860
+ file: 4
43861
+ };
43862
+ nodes.sort(
43863
+ (a, b) => (a.root ? 0 : RANK[a.group]) - (b.root ? 0 : RANK[b.group]) || (b.at ?? 0) - (a.at ?? 0) || (deg.get(b.id) ?? 0) - (deg.get(a.id) ?? 0) || a.id.localeCompare(b.id)
43864
+ );
43865
+ const total = nodes.length;
43866
+ const kept = nodes.slice(0, limit);
43867
+ const keep = new Set(kept.map((n) => n.id));
43868
+ const keptEdges = [...edges.values()].filter((e) => keep.has(e.src) && keep.has(e.dst));
43869
+ return {
43870
+ git: true,
43871
+ focus: focusId ?? focus,
43872
+ defaultBranch: defaultName,
43873
+ nodes: kept,
43874
+ edges: keptEdges,
43875
+ truncated: total > kept.length,
43876
+ total,
43877
+ /** Every branch with own commits, newest first, for the focus picker. */
43878
+ branches: branches.filter((b) => b.commits.length).sort((a, b) => Number(b.isDefault) - Number(a.isDefault) || b.tipAt - a.tipAt).map((b) => ({
43879
+ name: b.name,
43880
+ commits: b.commits.length,
43881
+ tipAt: b.tipAt,
43882
+ merged: b.merged,
43883
+ live: b.live,
43884
+ isDefault: b.isDefault
43885
+ }))
43886
+ };
43887
+ }
43888
+ var LOCKFILE, branchId, commitId, NO_GIT2;
43889
+ var init_history_graph_api = __esm({
43890
+ "src/ui/history-graph-api.ts"() {
43891
+ "use strict";
43892
+ init_esm_shims();
43893
+ init_category();
43894
+ init_graph();
43895
+ init_ids();
43896
+ init_lineage();
43897
+ init_model();
43898
+ init_store();
43899
+ init_storage();
43900
+ LOCKFILE = /(^|\/)(package-lock\.json|npm-shrinkwrap\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?|Cargo\.lock|go\.sum|poetry\.lock|uv\.lock|composer\.lock|Gemfile\.lock)$/;
43901
+ branchId = (name) => `branch:${name}`;
43902
+ commitId = (sha) => `commit:${sha}`;
43903
+ NO_GIT2 = { git: false, error: "not a git repository (or no commits yet)" };
43904
+ }
43905
+ });
43906
+
43907
+ // src/ui/knowledge-api.ts
43908
+ function metaOf4(row) {
43909
+ if (!row?.meta) return {};
43910
+ try {
43911
+ return JSON.parse(row.meta);
43912
+ } catch {
43913
+ return {};
43914
+ }
43915
+ }
43916
+ function kindOfMissing(id) {
43917
+ const k = parseId(id).kind;
43918
+ return k === "pkg" ? "package" : k === "sym" ? "symbol" : k === "doc" ? "doc" : k;
43919
+ }
43920
+ function scopeOf2(raw) {
43921
+ return raw === "memory" || raw === "docs" ? raw : "all";
43922
+ }
43923
+ function knowledgePayload(ctx, opts = {}) {
43924
+ const db = getStorage(ctx);
43925
+ reconcile(ctx, db);
43926
+ try {
43927
+ ensureDocsGraph(ctx, db);
43928
+ } catch {
43929
+ }
43930
+ return buildKnowledge(ctx, db, opts);
43931
+ }
43932
+ function buildKnowledge(ctx, db, opts = {}) {
43933
+ const scope = opts.scope ?? "all";
43934
+ const limit = Math.max(10, Math.min(2e3, opts.limit ?? 300));
43935
+ const focus = opts.focus?.trim() || null;
43936
+ const memRows = new Map(
43937
+ db.memoryList({ limit: 1e5 }).filter((r) => r.name).map((r) => [r.name, r])
43938
+ );
43939
+ const memNodes = db.graphNodesByKind(["memory"]);
43940
+ const docNodes = db.graphNodesByKind(["doc"]);
43941
+ const rows = /* @__PURE__ */ new Map();
43942
+ for (const n of [...memNodes, ...docNodes]) rows.set(n.id, n);
43943
+ const hubsOf = (id) => {
43944
+ const n = rows.get(id);
43945
+ if (!n) return [];
43946
+ const m = metaOf4(n);
43947
+ const out = [];
43948
+ if (n.kind === "memory") {
43949
+ const row = memRows.get(n.name);
43950
+ const tags = row?.tags ? JSON.parse(row.tags) : m.tags ?? [];
43951
+ for (const t of tags) out.push({ id: tagId(t), kind: "tag", label: `#${t}` });
43952
+ if (n.name.includes("/")) {
43953
+ const topic = n.name.split("/")[0];
43954
+ out.push({ id: topicId(topic), kind: "topic", label: `${topic}/` });
43955
+ }
43956
+ } else if (n.kind === "doc") {
43957
+ for (const t of m.tags ?? [])
43958
+ out.push({ id: tagId(t), kind: "tag", label: `#${t}` });
43959
+ const cat = typeof m.category === "string" ? m.category : "local";
43960
+ out.push({ id: topicId(`docs/${cat}`), kind: "topic", label: categoryInfo(cat).label });
43961
+ }
43962
+ return out;
43963
+ };
43964
+ const scoped = [
43965
+ ...scope !== "docs" ? memNodes : [],
43966
+ ...scope !== "memory" ? docNodes : []
43967
+ ].map((n) => n.id);
43968
+ let seeds;
43969
+ if (!focus) seeds = scoped;
43970
+ else if (focus.startsWith("tag:") || focus.startsWith("topic:"))
43971
+ seeds = [...memNodes, ...docNodes].map((n) => n.id).filter((id) => hubsOf(id).some((h) => h.id === focus));
43972
+ else seeds = [focus];
43973
+ const edges = /* @__PURE__ */ new Map();
43974
+ const addEdge = (e) => {
43975
+ if (e.src === e.dst) return;
43976
+ const key = `${e.src}|${e.dst}|${e.kind}`;
43977
+ if (!edges.has(key)) edges.set(key, e);
43978
+ };
43979
+ const stored = (e) => addEdge({ src: e.src, dst: e.dst, kind: e.kind, weight: e.weight });
43980
+ const hop = (ids2) => {
43981
+ if (!ids2.length) return [];
43982
+ const found = /* @__PURE__ */ new Set();
43983
+ for (const e of db.graphEdgesFrom(ids2, ALL_KINDS)) {
43984
+ stored(e);
43985
+ found.add(e.dst);
43986
+ }
43987
+ for (const e of db.graphEdgesTo(ids2, ALL_KINDS)) {
43988
+ stored(e);
43989
+ found.add(e.src);
43990
+ }
43991
+ return [...found];
43992
+ };
43993
+ const first = hop(seeds);
43994
+ if (focus && !focus.startsWith("tag:") && !focus.startsWith("topic:")) {
43995
+ const fresh = first.filter((id) => !seeds.includes(id));
43996
+ hop(fresh);
43997
+ }
43998
+ const pages = localDocPages(ctx, db);
43999
+ const knowledgeIds = () => new Set([...edges.values()].flatMap((e) => [e.src, e.dst]).concat(seeds));
44000
+ if (pages.size) {
44001
+ for (const id of knowledgeIds()) {
44002
+ if (!id.startsWith("memory:")) continue;
44003
+ const row = memRows.get(id.slice("memory:".length));
44004
+ const related = row?.node_ids ? JSON.parse(row.node_ids) : [];
44005
+ for (const r of related) {
44006
+ const f = fileOf(r);
44007
+ for (const name of f && pages.get(f) || [])
44008
+ addEdge({ src: id, dst: docId(name), kind: "about", weight: 1, derived: true });
44009
+ }
44010
+ }
44011
+ const docsInView = [...knowledgeIds()].filter((id) => id.startsWith("doc:"));
44012
+ if (docsInView.length) {
44013
+ for (const [name, row] of memRows) {
44014
+ const related = row.node_ids ? JSON.parse(row.node_ids) : [];
44015
+ for (const r of related) {
44016
+ const f = fileOf(r);
44017
+ for (const doc of f && pages.get(f) || [])
44018
+ if (docsInView.includes(docId(doc)))
44019
+ addEdge({
44020
+ src: memoryId(name),
44021
+ dst: docId(doc),
44022
+ kind: "about",
44023
+ weight: 1,
44024
+ derived: true
44025
+ });
44026
+ }
44027
+ }
44028
+ }
44029
+ }
44030
+ const hubNodes = /* @__PURE__ */ new Map();
44031
+ for (const id of knowledgeIds()) {
44032
+ for (const h of hubsOf(id)) {
44033
+ if (!hubNodes.has(h.id))
44034
+ hubNodes.set(h.id, {
44035
+ id: h.id,
44036
+ label: h.label,
44037
+ kind: h.kind,
44038
+ path: null,
44039
+ line: null,
44040
+ group: "hub"
44041
+ });
44042
+ addEdge({
44043
+ src: id,
44044
+ dst: h.id,
44045
+ kind: h.kind === "tag" ? "tagged" : "in_topic",
44046
+ weight: 0.5,
44047
+ derived: true
44048
+ });
44049
+ }
44050
+ }
44051
+ if (focus && (focus.startsWith("tag:") || focus.startsWith("topic:")) && !hubNodes.has(focus))
44052
+ hubNodes.set(focus, {
44053
+ id: focus,
44054
+ label: focus.startsWith("tag:") ? `#${focus.slice(4)}` : `${focus.slice(6)}/`,
44055
+ kind: focus.startsWith("tag:") ? "tag" : "topic",
44056
+ path: null,
44057
+ line: null,
44058
+ group: "hub"
44059
+ });
44060
+ const ids = knowledgeIds();
44061
+ for (const h of hubNodes.keys()) ids.add(h);
44062
+ const graphRows = new Map(
44063
+ db.graphGetNodes([...ids].filter((id) => !hubNodes.has(id))).map((n) => [n.id, n])
44064
+ );
44065
+ const seedSet = new Set(seeds);
44066
+ if (focus) seedSet.add(focus);
44067
+ const nodes = [];
44068
+ for (const id of ids) {
44069
+ const hub = hubNodes.get(id);
44070
+ if (hub) {
44071
+ nodes.push({ ...hub, seed: seedSet.has(id) || void 0, root: id === focus });
44072
+ continue;
44073
+ }
44074
+ const row = graphRows.get(id);
44075
+ const base = vis(row, id, id === focus);
44076
+ const m = metaOf4(row);
44077
+ const node2 = {
44078
+ ...base,
44079
+ kind: row ? row.kind : kindOfMissing(id),
44080
+ group: "code",
44081
+ seed: seedSet.has(id) || void 0,
44082
+ missing: row ? void 0 : true
44083
+ };
44084
+ if (node2.kind === "memory") {
44085
+ const name = row?.name ?? id.slice("memory:".length);
44086
+ const mr = memRows.get(name);
44087
+ node2.group = "memory";
44088
+ node2.label = name;
44089
+ node2.sub = mr?.kind ?? m.kind;
44090
+ node2.status = mr?.status ?? m.status ?? (node2.sub === "decision" ? "accepted" : void 0);
44091
+ node2.tags = mr?.tags ? JSON.parse(mr.tags) : m.tags;
44092
+ node2.detail = name.includes("/") ? `${name.split("/")[0]}/` : void 0;
44093
+ } else if (node2.kind === "doc") {
44094
+ node2.group = "doc";
44095
+ node2.label = row?.name ?? id.slice("doc:".length);
44096
+ node2.sub = typeof m.category === "string" ? m.category : void 0;
44097
+ node2.detail = node2.sub ? categoryInfo(node2.sub).label : void 0;
44098
+ node2.provider = typeof m.provider === "string" ? m.provider : void 0;
44099
+ node2.tags = m.tags;
44100
+ }
44101
+ nodes.push(node2);
44102
+ }
44103
+ const deg = /* @__PURE__ */ new Map();
44104
+ for (const e of edges.values()) {
44105
+ deg.set(e.src, (deg.get(e.src) ?? 0) + 1);
44106
+ deg.set(e.dst, (deg.get(e.dst) ?? 0) + 1);
44107
+ }
44108
+ const updated = (n) => n.kind === "memory" ? memRows.get(n.label)?.updated_at ?? 0 : 0;
44109
+ const rank = (n) => n.root ? 0 : n.seed ? 1 : n.group === "memory" || n.group === "doc" ? 2 : n.group === "hub" ? 3 : 4;
44110
+ nodes.sort(
44111
+ (a, b) => rank(a) - rank(b) || updated(b) - updated(a) || (deg.get(b.id) ?? 0) - (deg.get(a.id) ?? 0) || a.id.localeCompare(b.id)
44112
+ );
44113
+ const total = nodes.length;
44114
+ const kept = nodes.slice(0, limit);
44115
+ const keep = new Set(kept.map((n) => n.id));
44116
+ const keptEdges = [...edges.values()].filter((e) => keep.has(e.src) && keep.has(e.dst));
44117
+ const counts = { nodes: {}, edges: {} };
44118
+ for (const n of kept) counts.nodes[n.kind] = (counts.nodes[n.kind] ?? 0) + 1;
44119
+ for (const e of keptEdges) counts.edges[e.kind] = (counts.edges[e.kind] ?? 0) + 1;
44120
+ return {
44121
+ scope,
44122
+ focus,
44123
+ nodes: kept,
44124
+ edges: keptEdges,
44125
+ truncated: total > kept.length,
44126
+ total,
44127
+ counts
44128
+ };
44129
+ }
44130
+ var KNOWLEDGE_EDGES, ALL_KINDS, tagId, topicId;
44131
+ var init_knowledge_api = __esm({
44132
+ "src/ui/knowledge-api.ts"() {
44133
+ "use strict";
44134
+ init_esm_shims();
44135
+ init_category();
44136
+ init_graph();
44137
+ init_ids();
44138
+ init_store();
44139
+ init_storage();
44140
+ init_graph_api();
44141
+ KNOWLEDGE_EDGES = ["mentions", "links", "supersedes", "documents"];
44142
+ ALL_KINDS = [...KNOWLEDGE_EDGES];
44143
+ tagId = (t) => `tag:${t}`;
44144
+ topicId = (t) => `topic:${t}`;
44145
+ }
44146
+ });
44147
+
43272
44148
  // src/ui/server.ts
43273
44149
  var server_exports = {};
43274
44150
  __export(server_exports, {
@@ -43276,9 +44152,9 @@ __export(server_exports, {
43276
44152
  uiDir: () => uiDir
43277
44153
  });
43278
44154
  import { spawn as spawn2 } from "child_process";
43279
- import { existsSync as existsSync23, readFileSync as readFileSync22, statSync as statSync10 } from "fs";
44155
+ import { existsSync as existsSync25, readFileSync as readFileSync22, statSync as statSync11 } from "fs";
43280
44156
  import { createServer } from "http";
43281
- import { extname as extname3, join as join28, normalize as normalize6 } from "path";
44157
+ import { extname as extname3, join as join29, normalize as normalize6 } from "path";
43282
44158
  import { URL as URL2 } from "url";
43283
44159
  function json2(res, status, body) {
43284
44160
  res.writeHead(status, {
@@ -43300,7 +44176,7 @@ function strList(v) {
43300
44176
  return void 0;
43301
44177
  }
43302
44178
  function uiDir() {
43303
- return process.env.EMINIAR_UI_DIR || join28(pluginRoot(), "dist", "ui");
44179
+ return process.env.EMINIAR_UI_DIR || join29(pluginRoot(), "dist", "ui");
43304
44180
  }
43305
44181
  function originAllowed(req, port) {
43306
44182
  const host = req.headers.host ?? "";
@@ -43324,14 +44200,14 @@ function originAllowed(req, port) {
43324
44200
  function serveStatic(res, p) {
43325
44201
  const dir = uiDir();
43326
44202
  const rel = p === "/" ? "index.html" : p.replace(/^\/+/, "");
43327
- const file3 = normalize6(join28(dir, rel));
44203
+ const file3 = normalize6(join29(dir, rel));
43328
44204
  if (!file3.startsWith(normalize6(dir))) return false;
43329
- if (!existsSync23(file3) || !statSync10(file3).isFile()) return false;
44205
+ if (!existsSync25(file3) || !statSync11(file3).isFile()) return false;
43330
44206
  const type = MIME[extname3(file3).toLowerCase()] ?? "application/octet-stream";
43331
44207
  if (rel === "index.html") {
43332
44208
  const stamp = (name) => {
43333
44209
  try {
43334
- return `/${name}?v=${Math.floor(statSync10(join28(dir, name)).mtimeMs)}`;
44210
+ return `/${name}?v=${Math.floor(statSync11(join29(dir, name)).mtimeMs)}`;
43335
44211
  } catch {
43336
44212
  return `/${name}`;
43337
44213
  }
@@ -43495,6 +44371,16 @@ async function serve(opts) {
43495
44371
  if (!provider) return json2(res, 400, { error: "provider required" });
43496
44372
  return json2(res, 200, await docsResolvePayload(ctx(), provider));
43497
44373
  }
44374
+ if (p === "/api/knowledge")
44375
+ return json2(
44376
+ res,
44377
+ 200,
44378
+ knowledgePayload(ctx(), {
44379
+ scope: scopeOf2(q.get("scope")),
44380
+ focus: str2("focus"),
44381
+ limit: num2("limit", 300)
44382
+ })
44383
+ );
43498
44384
  if (p === "/api/graph") {
43499
44385
  const mode = q.get("mode") ?? "impact";
43500
44386
  const node2 = q.get("node") ?? "";
@@ -43651,6 +44537,18 @@ async function serve(opts) {
43651
44537
  }
43652
44538
  if (p === "/api/history/graph")
43653
44539
  return json2(res, 200, historyGraphPayload(ctx(), num2("limit", 150)));
44540
+ if (p === "/api/history/knowledge")
44541
+ return json2(
44542
+ res,
44543
+ 200,
44544
+ historyGraphKnowledgePayload(ctx(), {
44545
+ focus: str2("focus"),
44546
+ branches: num2("branches", 8),
44547
+ commitsPerBranch: num2("commits", 25),
44548
+ filesPerCommit: num2("files", 12),
44549
+ limit: num2("limit", 400)
44550
+ })
44551
+ );
43654
44552
  if (p === "/api/history/risk")
43655
44553
  return json2(res, 200, historyRiskPayload(ctx(), strList(q.get("paths"))));
43656
44554
  if (p === "/api/history/symbol") {
@@ -43705,6 +44603,8 @@ var init_server = __esm({
43705
44603
  init_api2();
43706
44604
  init_graph_api();
43707
44605
  init_history_api();
44606
+ init_history_graph_api();
44607
+ init_knowledge_api();
43708
44608
  MIME = {
43709
44609
  ".html": "text/html; charset=utf-8",
43710
44610
  ".js": "text/javascript; charset=utf-8",
@@ -43727,7 +44627,7 @@ var statusline_exports = {};
43727
44627
  __export(statusline_exports, {
43728
44628
  renderStatusline: () => renderStatusline
43729
44629
  });
43730
- import { existsSync as existsSync24, readFileSync as readFileSync23, writeFileSync as writeFileSync9 } from "fs";
44630
+ import { existsSync as existsSync26, readFileSync as readFileSync23, writeFileSync as writeFileSync9 } from "fs";
43731
44631
  function renderStatusline(raw) {
43732
44632
  let input2 = {};
43733
44633
  try {
@@ -43736,7 +44636,7 @@ function renderStatusline(raw) {
43736
44636
  }
43737
44637
  const cache3 = paths().statuslineCache;
43738
44638
  try {
43739
- if (existsSync24(cache3)) {
44639
+ if (existsSync26(cache3)) {
43740
44640
  const c = JSON.parse(readFileSync23(cache3, "utf8"));
43741
44641
  if (Date.now() - c.at < 5e3 && c.key === (input2.session_id ?? "")) return c.line;
43742
44642
  }