@monoes/monomindcli 2.13.0 → 2.14.1

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.
Files changed (251) hide show
  1. package/.claude/skills/monodesign/scripts/context.mjs +1 -1
  2. package/.claude/skills/monodesign/scripts/detector/detect-antipatterns-browser.js +2 -2
  3. package/.claude/skills/monodesign/scripts/detector/rules/checks.mjs +2 -2
  4. package/.claude/skills/monodesign/scripts/live-browser.js +1 -1
  5. package/bin/cli.js +21 -1
  6. package/bin/mcp-server.js +21 -1
  7. package/dist/src/capabilities/cap-documents.d.ts.map +1 -1
  8. package/dist/src/capabilities/cap-documents.js +23 -0
  9. package/dist/src/capabilities/cap-documents.js.map +1 -1
  10. package/dist/src/commands/doc-filters.d.ts +19 -0
  11. package/dist/src/commands/doc-filters.d.ts.map +1 -0
  12. package/dist/src/commands/doc-filters.js +58 -0
  13. package/dist/src/commands/doc-filters.js.map +1 -0
  14. package/dist/src/commands/doc-library.d.ts +21 -0
  15. package/dist/src/commands/doc-library.d.ts.map +1 -0
  16. package/dist/src/commands/doc-library.js +390 -0
  17. package/dist/src/commands/doc-library.js.map +1 -0
  18. package/dist/src/commands/doc-list.d.ts +9 -0
  19. package/dist/src/commands/doc-list.d.ts.map +1 -0
  20. package/dist/src/commands/doc-list.js +99 -0
  21. package/dist/src/commands/doc-list.js.map +1 -0
  22. package/dist/src/commands/doc.d.ts.map +1 -1
  23. package/dist/src/commands/doc.js +49 -37
  24. package/dist/src/commands/doc.js.map +1 -1
  25. package/dist/src/commands/doctor-mcp-probe.d.ts +48 -0
  26. package/dist/src/commands/doctor-mcp-probe.d.ts.map +1 -0
  27. package/dist/src/commands/doctor-mcp-probe.js +205 -0
  28. package/dist/src/commands/doctor-mcp-probe.js.map +1 -0
  29. package/dist/src/commands/doctor-project-checks.d.ts +12 -1
  30. package/dist/src/commands/doctor-project-checks.d.ts.map +1 -1
  31. package/dist/src/commands/doctor-project-checks.js +55 -4
  32. package/dist/src/commands/doctor-project-checks.js.map +1 -1
  33. package/dist/src/commands/doctor.d.ts.map +1 -1
  34. package/dist/src/commands/doctor.js +10 -1
  35. package/dist/src/commands/doctor.js.map +1 -1
  36. package/dist/src/commands/init.d.ts.map +1 -1
  37. package/dist/src/commands/init.js +17 -0
  38. package/dist/src/commands/init.js.map +1 -1
  39. package/dist/src/commands/mcp.d.ts.map +1 -1
  40. package/dist/src/commands/mcp.js +4 -3
  41. package/dist/src/commands/mcp.js.map +1 -1
  42. package/dist/src/commands/org-observe.d.ts.map +1 -1
  43. package/dist/src/commands/org-observe.js +14 -0
  44. package/dist/src/commands/org-observe.js.map +1 -1
  45. package/dist/src/commands/org.d.ts.map +1 -1
  46. package/dist/src/commands/org.js +12 -2
  47. package/dist/src/commands/org.js.map +1 -1
  48. package/dist/src/init/claudemd-generator.d.ts.map +1 -1
  49. package/dist/src/init/claudemd-generator.js +2 -1
  50. package/dist/src/init/claudemd-generator.js.map +1 -1
  51. package/dist/src/init/mcp-generator.d.ts.map +1 -1
  52. package/dist/src/init/mcp-generator.js +3 -3
  53. package/dist/src/init/mcp-generator.js.map +1 -1
  54. package/dist/src/init/types.d.ts +6 -0
  55. package/dist/src/init/types.d.ts.map +1 -1
  56. package/dist/src/init/types.js.map +1 -1
  57. package/dist/src/init/write-runtime-config.d.ts.map +1 -1
  58. package/dist/src/init/write-runtime-config.js +12 -2
  59. package/dist/src/init/write-runtime-config.js.map +1 -1
  60. package/dist/src/knowledge/capture-envelope.d.ts +80 -0
  61. package/dist/src/knowledge/capture-envelope.d.ts.map +1 -0
  62. package/dist/src/knowledge/capture-envelope.js +165 -0
  63. package/dist/src/knowledge/capture-envelope.js.map +1 -0
  64. package/dist/src/knowledge/capture-text.d.ts +23 -0
  65. package/dist/src/knowledge/capture-text.d.ts.map +1 -0
  66. package/dist/src/knowledge/capture-text.js +49 -0
  67. package/dist/src/knowledge/capture-text.js.map +1 -0
  68. package/dist/src/knowledge/citation.d.ts +121 -0
  69. package/dist/src/knowledge/citation.d.ts.map +1 -0
  70. package/dist/src/knowledge/citation.js +252 -0
  71. package/dist/src/knowledge/citation.js.map +1 -0
  72. package/dist/src/knowledge/document-pipeline.d.ts +60 -0
  73. package/dist/src/knowledge/document-pipeline.d.ts.map +1 -1
  74. package/dist/src/knowledge/document-pipeline.js +188 -4
  75. package/dist/src/knowledge/document-pipeline.js.map +1 -1
  76. package/dist/src/knowledge/html-extract.d.ts +43 -0
  77. package/dist/src/knowledge/html-extract.d.ts.map +1 -0
  78. package/dist/src/knowledge/html-extract.js +286 -0
  79. package/dist/src/knowledge/html-extract.js.map +1 -0
  80. package/dist/src/knowledge/html-tags.d.ts +30 -0
  81. package/dist/src/knowledge/html-tags.d.ts.map +1 -0
  82. package/dist/src/knowledge/html-tags.js +229 -0
  83. package/dist/src/knowledge/html-tags.js.map +1 -0
  84. package/dist/src/knowledge/library.d.ts +106 -0
  85. package/dist/src/knowledge/library.d.ts.map +1 -0
  86. package/dist/src/knowledge/library.js +195 -0
  87. package/dist/src/knowledge/library.js.map +1 -0
  88. package/dist/src/knowledge/mhtml.d.ts +60 -0
  89. package/dist/src/knowledge/mhtml.d.ts.map +1 -0
  90. package/dist/src/knowledge/mhtml.js +154 -0
  91. package/dist/src/knowledge/mhtml.js.map +1 -0
  92. package/dist/src/knowledge/related.d.ts +58 -0
  93. package/dist/src/knowledge/related.d.ts.map +1 -0
  94. package/dist/src/knowledge/related.js +222 -0
  95. package/dist/src/knowledge/related.js.map +1 -0
  96. package/dist/src/knowledge/section-diff.d.ts +48 -0
  97. package/dist/src/knowledge/section-diff.d.ts.map +1 -0
  98. package/dist/src/knowledge/section-diff.js +151 -0
  99. package/dist/src/knowledge/section-diff.js.map +1 -0
  100. package/dist/src/knowledge/watch.d.ts +92 -0
  101. package/dist/src/knowledge/watch.d.ts.map +1 -0
  102. package/dist/src/knowledge/watch.js +221 -0
  103. package/dist/src/knowledge/watch.js.map +1 -0
  104. package/dist/src/mcp-server.d.ts.map +1 -1
  105. package/dist/src/mcp-server.js +44 -87
  106. package/dist/src/mcp-server.js.map +1 -1
  107. package/dist/src/mcp-tools/browser-instrument-tools.d.ts +35 -0
  108. package/dist/src/mcp-tools/browser-instrument-tools.d.ts.map +1 -0
  109. package/dist/src/mcp-tools/browser-instrument-tools.js +359 -0
  110. package/dist/src/mcp-tools/browser-instrument-tools.js.map +1 -0
  111. package/dist/src/mcp-tools/browser-metrics.d.ts +42 -0
  112. package/dist/src/mcp-tools/browser-metrics.d.ts.map +1 -0
  113. package/dist/src/mcp-tools/browser-metrics.js +91 -0
  114. package/dist/src/mcp-tools/browser-metrics.js.map +1 -0
  115. package/dist/src/mcp-tools/browser-profile-tools.d.ts +16 -0
  116. package/dist/src/mcp-tools/browser-profile-tools.d.ts.map +1 -0
  117. package/dist/src/mcp-tools/browser-profile-tools.js +324 -0
  118. package/dist/src/mcp-tools/browser-profile-tools.js.map +1 -0
  119. package/dist/src/mcp-tools/browser-session.d.ts +68 -0
  120. package/dist/src/mcp-tools/browser-session.d.ts.map +1 -0
  121. package/dist/src/mcp-tools/browser-session.js +224 -0
  122. package/dist/src/mcp-tools/browser-session.js.map +1 -0
  123. package/dist/src/mcp-tools/browser-tools.d.ts.map +1 -1
  124. package/dist/src/mcp-tools/browser-tools.js +10 -176
  125. package/dist/src/mcp-tools/browser-tools.js.map +1 -1
  126. package/dist/src/mcp-tools/capture-resource-read.d.ts +115 -0
  127. package/dist/src/mcp-tools/capture-resource-read.d.ts.map +1 -0
  128. package/dist/src/mcp-tools/capture-resource-read.js +296 -0
  129. package/dist/src/mcp-tools/capture-resource-read.js.map +1 -0
  130. package/dist/src/mcp-tools/capture-resource-tools.d.ts +22 -0
  131. package/dist/src/mcp-tools/capture-resource-tools.d.ts.map +1 -0
  132. package/dist/src/mcp-tools/capture-resource-tools.js +182 -0
  133. package/dist/src/mcp-tools/capture-resource-tools.js.map +1 -0
  134. package/dist/src/mcp-tools/capture-resources.d.ts +142 -0
  135. package/dist/src/mcp-tools/capture-resources.d.ts.map +1 -0
  136. package/dist/src/mcp-tools/capture-resources.js +289 -0
  137. package/dist/src/mcp-tools/capture-resources.js.map +1 -0
  138. package/dist/src/mcp-tools/index.d.ts +3 -0
  139. package/dist/src/mcp-tools/index.d.ts.map +1 -1
  140. package/dist/src/mcp-tools/index.js +6 -0
  141. package/dist/src/mcp-tools/index.js.map +1 -1
  142. package/dist/src/mcp-tools/knowledge-tools.d.ts.map +1 -1
  143. package/dist/src/mcp-tools/knowledge-tools.js +9 -1
  144. package/dist/src/mcp-tools/knowledge-tools.js.map +1 -1
  145. package/dist/src/mcp-tools/resource-router.d.ts +86 -0
  146. package/dist/src/mcp-tools/resource-router.d.ts.map +1 -0
  147. package/dist/src/mcp-tools/resource-router.js +181 -0
  148. package/dist/src/mcp-tools/resource-router.js.map +1 -0
  149. package/dist/src/memory/memory-bridge.js +1 -1
  150. package/dist/src/memory/memory-bridge.js.map +1 -1
  151. package/dist/src/orgrt/agent-runner.d.ts +38 -0
  152. package/dist/src/orgrt/agent-runner.d.ts.map +1 -1
  153. package/dist/src/orgrt/agent-runner.js +47 -0
  154. package/dist/src/orgrt/agent-runner.js.map +1 -1
  155. package/dist/src/orgrt/checkpoint.d.ts +12 -1
  156. package/dist/src/orgrt/checkpoint.d.ts.map +1 -1
  157. package/dist/src/orgrt/checkpoint.js +15 -6
  158. package/dist/src/orgrt/checkpoint.js.map +1 -1
  159. package/dist/src/orgrt/completion-gate.d.ts +74 -0
  160. package/dist/src/orgrt/completion-gate.d.ts.map +1 -1
  161. package/dist/src/orgrt/completion-gate.js +45 -0
  162. package/dist/src/orgrt/completion-gate.js.map +1 -1
  163. package/dist/src/orgrt/cost-tier.d.ts +144 -0
  164. package/dist/src/orgrt/cost-tier.d.ts.map +1 -0
  165. package/dist/src/orgrt/cost-tier.js +183 -0
  166. package/dist/src/orgrt/cost-tier.js.map +1 -0
  167. package/dist/src/orgrt/cross-org.d.ts.map +1 -1
  168. package/dist/src/orgrt/cross-org.js +14 -0
  169. package/dist/src/orgrt/cross-org.js.map +1 -1
  170. package/dist/src/orgrt/daemon.d.ts +12 -1
  171. package/dist/src/orgrt/daemon.d.ts.map +1 -1
  172. package/dist/src/orgrt/daemon.js +216 -49
  173. package/dist/src/orgrt/daemon.js.map +1 -1
  174. package/dist/src/orgrt/decisions.d.ts +14 -2
  175. package/dist/src/orgrt/decisions.d.ts.map +1 -1
  176. package/dist/src/orgrt/decisions.js +216 -10
  177. package/dist/src/orgrt/decisions.js.map +1 -1
  178. package/dist/src/orgrt/file-roots.d.ts +7 -0
  179. package/dist/src/orgrt/file-roots.d.ts.map +1 -1
  180. package/dist/src/orgrt/file-roots.js +38 -1
  181. package/dist/src/orgrt/file-roots.js.map +1 -1
  182. package/dist/src/orgrt/forwarder.d.ts.map +1 -1
  183. package/dist/src/orgrt/forwarder.js +16 -2
  184. package/dist/src/orgrt/forwarder.js.map +1 -1
  185. package/dist/src/orgrt/idle-deadline.d.ts +74 -1
  186. package/dist/src/orgrt/idle-deadline.d.ts.map +1 -1
  187. package/dist/src/orgrt/idle-deadline.js +52 -2
  188. package/dist/src/orgrt/idle-deadline.js.map +1 -1
  189. package/dist/src/orgrt/loadouts.d.ts +38 -0
  190. package/dist/src/orgrt/loadouts.d.ts.map +1 -0
  191. package/dist/src/orgrt/loadouts.js +129 -0
  192. package/dist/src/orgrt/loadouts.js.map +1 -0
  193. package/dist/src/orgrt/mailbox.d.ts +17 -1
  194. package/dist/src/orgrt/mailbox.d.ts.map +1 -1
  195. package/dist/src/orgrt/mailbox.js +50 -3
  196. package/dist/src/orgrt/mailbox.js.map +1 -1
  197. package/dist/src/orgrt/policy.d.ts +42 -2
  198. package/dist/src/orgrt/policy.d.ts.map +1 -1
  199. package/dist/src/orgrt/policy.js +53 -8
  200. package/dist/src/orgrt/policy.js.map +1 -1
  201. package/dist/src/orgrt/prompt-vars.d.ts +11 -0
  202. package/dist/src/orgrt/prompt-vars.d.ts.map +1 -0
  203. package/dist/src/orgrt/prompt-vars.js +49 -0
  204. package/dist/src/orgrt/prompt-vars.js.map +1 -0
  205. package/dist/src/orgrt/questions.d.ts +22 -1
  206. package/dist/src/orgrt/questions.d.ts.map +1 -1
  207. package/dist/src/orgrt/questions.js +24 -3
  208. package/dist/src/orgrt/questions.js.map +1 -1
  209. package/dist/src/orgrt/review-packet.d.ts +29 -0
  210. package/dist/src/orgrt/review-packet.d.ts.map +1 -0
  211. package/dist/src/orgrt/review-packet.js +67 -0
  212. package/dist/src/orgrt/review-packet.js.map +1 -0
  213. package/dist/src/orgrt/role-sandbox.d.ts.map +1 -1
  214. package/dist/src/orgrt/role-sandbox.js +16 -3
  215. package/dist/src/orgrt/role-sandbox.js.map +1 -1
  216. package/dist/src/orgrt/session-ledger.d.ts +68 -0
  217. package/dist/src/orgrt/session-ledger.d.ts.map +1 -0
  218. package/dist/src/orgrt/session-ledger.js +128 -0
  219. package/dist/src/orgrt/session-ledger.js.map +1 -0
  220. package/dist/src/orgrt/session.d.ts +45 -5
  221. package/dist/src/orgrt/session.d.ts.map +1 -1
  222. package/dist/src/orgrt/session.js +397 -27
  223. package/dist/src/orgrt/session.js.map +1 -1
  224. package/dist/src/orgrt/task-dag.d.ts +34 -1
  225. package/dist/src/orgrt/task-dag.d.ts.map +1 -1
  226. package/dist/src/orgrt/task-dag.js +58 -7
  227. package/dist/src/orgrt/task-dag.js.map +1 -1
  228. package/dist/src/orgrt/tool-spill.d.ts +94 -0
  229. package/dist/src/orgrt/tool-spill.d.ts.map +1 -0
  230. package/dist/src/orgrt/tool-spill.js +180 -0
  231. package/dist/src/orgrt/tool-spill.js.map +1 -0
  232. package/dist/src/orgrt/types.d.ts +138 -0
  233. package/dist/src/orgrt/types.d.ts.map +1 -1
  234. package/dist/src/orgrt/types.js +148 -0
  235. package/dist/src/orgrt/types.js.map +1 -1
  236. package/dist/src/platform-adapters/renderers/mcp.d.ts +20 -2
  237. package/dist/src/platform-adapters/renderers/mcp.d.ts.map +1 -1
  238. package/dist/src/platform-adapters/renderers/mcp.js +25 -5
  239. package/dist/src/platform-adapters/renderers/mcp.js.map +1 -1
  240. package/dist/src/protocol-capabilities.d.ts +2 -1
  241. package/dist/src/protocol-capabilities.d.ts.map +1 -1
  242. package/dist/src/protocol-capabilities.js +2 -1
  243. package/dist/src/protocol-capabilities.js.map +1 -1
  244. package/dist/src/ui/dashboard.html +450 -164
  245. package/dist/src/ui/org-hil.mjs +276 -0
  246. package/dist/src/ui/org-runtime.mjs +350 -0
  247. package/dist/src/ui/orgs.html +15 -4
  248. package/dist/src/ui/routes-org.mjs +290 -541
  249. package/dist/src/ui/server.mjs +25 -5
  250. package/dist/tsconfig.tsbuildinfo +1 -1
  251. package/package.json +9 -8
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Paragraph-level citation (RCL-10).
3
+ *
4
+ * A search hit already knows WHICH document it came from. A citation has to
5
+ * know WHERE IN IT: the character span of the chunk against the extracted
6
+ * text, an anchor that survives being written down, and a way back to the
7
+ * live page at that passage.
8
+ *
9
+ * Three pieces, deliberately separate:
10
+ *
11
+ * - `span:<start>-<end>` — a tag written next to every chunk at ingest, so a
12
+ * search result carries offsets without re-reading the document.
13
+ * - the ANCHOR `<hash12>#<start>-<end>` — offsets bound to the content hash
14
+ * of the exact version they were measured against. Quoting an offset range
15
+ * without the version it belongs to is how citations rot: the document is
16
+ * re-captured, the text shifts, and the quote silently points at other
17
+ * words. A stale anchor here is DETECTED (`stale: true`), never served as
18
+ * if it were still true.
19
+ * - a W3C text fragment (`#:~:text=`) — the link a human clicks, which lands
20
+ * on the sentence even though the site never had an id there.
21
+ *
22
+ * Everything above `resolveCitation` is pure and synchronous so the pipeline
23
+ * can use it while ingesting. `resolveCitation` reaches back into the
24
+ * document-pipeline through a DYNAMIC import: the pipeline imports this
25
+ * module statically, and a static import back would close the cycle.
26
+ *
27
+ * @module v1/cli/knowledge/citation
28
+ */
29
+ import * as crypto from 'node:crypto';
30
+ import * as fs from 'node:fs';
31
+ import * as path from 'node:path';
32
+ export const SPAN_TAG_PREFIX = 'span:';
33
+ /** The tag written beside a chunk at ingest: `span:<start>-<end>`. */
34
+ export function spanTag(startChar, endChar) {
35
+ return `${SPAN_TAG_PREFIX}${startChar}-${endChar}`;
36
+ }
37
+ /** Offsets from a chunk's tag list, or null when it predates span tags. */
38
+ export function parseSpanTag(tags) {
39
+ for (const tag of tags ?? []) {
40
+ if (typeof tag !== 'string' || !tag.startsWith(SPAN_TAG_PREFIX))
41
+ continue;
42
+ const m = /^span:(\d+)-(\d+)$/.exec(tag);
43
+ if (!m)
44
+ continue;
45
+ const startChar = Number(m[1]);
46
+ const endChar = Number(m[2]);
47
+ if (endChar < startChar)
48
+ continue;
49
+ return { startChar, endChar };
50
+ }
51
+ return null;
52
+ }
53
+ const ANCHOR_HASH_CHARS = 12;
54
+ /** `<hash12>#<start>-<end>` — offsets bound to the version they were measured against. */
55
+ export function citationAnchor(contentHash, startChar, endChar) {
56
+ return `${(contentHash || '').slice(0, ANCHOR_HASH_CHARS)}#${startChar}-${endChar}`;
57
+ }
58
+ export function parseCitationAnchor(anchor) {
59
+ const m = /^([0-9a-f]{4,64})#(\d+)-(\d+)$/.exec((anchor || '').trim());
60
+ if (!m)
61
+ return null;
62
+ const startChar = Number(m[2]);
63
+ const endChar = Number(m[3]);
64
+ if (endChar < startChar)
65
+ return null;
66
+ return { hashPrefix: m[1], startChar, endChar };
67
+ }
68
+ /** True when the anchor was cut against this document version. */
69
+ export function anchorMatchesHash(anchor, contentHash) {
70
+ const parsed = parseCitationAnchor(anchor);
71
+ if (!parsed)
72
+ return false;
73
+ return (contentHash || '').startsWith(parsed.hashPrefix);
74
+ }
75
+ // ── Quotes and links ───────────────────────────────────────────────
76
+ const QUOTE_MAX_CHARS = 320;
77
+ const CONTEXT_PREFIX_RE = /^(§ [^\n]*\n)+/;
78
+ const HEADING_LINE_RE = /^\s{0,3}#{1,6}\s+/;
79
+ /**
80
+ * One line of markdown as the words a reader sees.
81
+ *
82
+ * This matters more than it looks: a text fragment built from `# Pricing ##
83
+ * Starter Nine dollars` matches NOTHING on the live page, because the page
84
+ * never contained a `#`. The link is only worth having if it is made of the
85
+ * prose the site actually renders.
86
+ */
87
+ function stripMarkdown(line) {
88
+ return line
89
+ .replace(HEADING_LINE_RE, '')
90
+ .replace(/^\s{0,3}>\s?/, '')
91
+ .replace(/^\s{0,3}([-*+]|\d+\.)\s+/, '')
92
+ .replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
93
+ .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
94
+ .replace(/`{1,3}([^`]*)`{1,3}/g, '$1')
95
+ .replace(/(\*\*|__)(.*?)\1/g, '$2')
96
+ .replace(/(\*|_)(?=\S)(.*?)(?<=\S)\1/g, '$2')
97
+ .trim();
98
+ }
99
+ /**
100
+ * The prose of a passage: § context prefixes gone, markdown syntax gone,
101
+ * whitespace collapsed.
102
+ *
103
+ * `skipHeadings` drops heading lines as well, which is what a text fragment
104
+ * wants — a heading is usually its own element on the page, so starting a
105
+ * range there matches less reliably than starting at the paragraph. It falls
106
+ * back to including them when the passage is nothing but headings.
107
+ */
108
+ export function passageProse(passage, skipHeadings = false) {
109
+ const lines = String(passage ?? '')
110
+ .replace(CONTEXT_PREFIX_RE, '')
111
+ .split('\n');
112
+ const keep = (only) => lines
113
+ .filter((l) => !only || !HEADING_LINE_RE.test(l))
114
+ .map(stripMarkdown)
115
+ .filter(Boolean)
116
+ .join(' ')
117
+ .replace(/\s+/g, ' ')
118
+ .trim();
119
+ const body = keep(skipHeadings);
120
+ return body || keep(false);
121
+ }
122
+ /** The head of a passage as a quotable sentence or two — the words a reader
123
+ * would see on the page, not the markdown we stored. */
124
+ export function passageQuote(passage, maxChars = QUOTE_MAX_CHARS) {
125
+ const text = passageProse(passage);
126
+ if (text.length <= maxChars)
127
+ return text;
128
+ const window = text.slice(0, maxChars);
129
+ const stop = Math.max(window.lastIndexOf('. '), window.lastIndexOf('? '), window.lastIndexOf('! '));
130
+ return stop > maxChars * 0.4 ? window.slice(0, stop + 1) : `${window.trimEnd()}…`;
131
+ }
132
+ /** `,` and `-` are syntax inside a text fragment, so they cannot be left raw. */
133
+ function encodeFragmentPart(text) {
134
+ return encodeURIComponent(text).replace(/-/g, '%2D').replace(/,/g, '%2C');
135
+ }
136
+ const FRAGMENT_WORDS = 8;
137
+ /**
138
+ * A W3C scroll-to-text fragment for this passage.
139
+ *
140
+ * Long passages use the `textStart,textEnd` form rather than the whole
141
+ * paragraph: the spec matches across the range, and a shorter pair survives
142
+ * the site re-flowing its markup between capture and click.
143
+ */
144
+ export function textFragment(passage) {
145
+ const text = passageProse(passage, true).slice(0, 2000);
146
+ if (!text)
147
+ return '';
148
+ const words = text.split(' ').filter(Boolean);
149
+ if (words.length <= FRAGMENT_WORDS * 2)
150
+ return `:~:text=${encodeFragmentPart(text)}`;
151
+ const start = words.slice(0, FRAGMENT_WORDS).join(' ');
152
+ const end = words.slice(-FRAGMENT_WORDS).join(' ');
153
+ return `:~:text=${encodeFragmentPart(start)},${encodeFragmentPart(end)}`;
154
+ }
155
+ /** The captured page's URL, pointed at the passage. Returns undefined when
156
+ * there is no URL — a citation into a local file has no link to give. */
157
+ export function citationUrl(url, passage) {
158
+ if (!url)
159
+ return undefined;
160
+ const fragment = textFragment(passage);
161
+ if (!fragment)
162
+ return url;
163
+ const hash = url.indexOf('#');
164
+ const base = hash === -1 ? url : url.slice(0, hash);
165
+ return `${base}#${fragment}`;
166
+ }
167
+ function fileEntry(absolutePath) {
168
+ const stat = fs.statSync(absolutePath);
169
+ return {
170
+ path: absolutePath,
171
+ absolutePath,
172
+ extension: path.extname(absolutePath).toLowerCase(),
173
+ size: stat.size,
174
+ modified: stat.mtime,
175
+ created: stat.birthtime,
176
+ };
177
+ }
178
+ /**
179
+ * Resolve a citation back to the passage it came from.
180
+ *
181
+ * `target` is whatever the caller has written down: an indexed file path, a
182
+ * capture's `canonicalUrl`, or the `<scope>:<path>` docId. Throws with a
183
+ * message meant for a human when the document is not indexed, its file is
184
+ * gone, or the chunk does not exist — a citation that cannot be checked must
185
+ * fail loudly, not resolve to an empty string.
186
+ */
187
+ export async function resolveCitation(target, opts = {}) {
188
+ const pipeline = await import('./document-pipeline.js');
189
+ const { extractText } = await import('../capabilities/cap-documents.js');
190
+ const rootDir = opts.rootDir ?? pipeline.getKnowledgeRoot(opts.scope);
191
+ const scope = opts.scope;
192
+ const record = pipeline.findDocumentRecord(rootDir, target, scope);
193
+ if (!record)
194
+ throw new Error(`not indexed: ${target}`);
195
+ if (!fs.existsSync(record.filePath)) {
196
+ throw new Error(`source file is gone: ${record.filePath} (re-ingest the capture to cite it)`);
197
+ }
198
+ const text = await extractText(fileEntry(record.filePath));
199
+ if (!text)
200
+ throw new Error(`no text could be extracted from ${record.filePath}`);
201
+ const hash = crypto.createHash('sha256').update(text).digest('hex');
202
+ let startChar;
203
+ let endChar;
204
+ let chunkIndex;
205
+ const parsedAnchor = opts.anchor ? parseCitationAnchor(opts.anchor) : null;
206
+ if (opts.anchor && !parsedAnchor)
207
+ throw new Error(`malformed citation anchor: ${opts.anchor}`);
208
+ if (parsedAnchor) {
209
+ startChar = Math.min(parsedAnchor.startChar, text.length);
210
+ endChar = Math.min(parsedAnchor.endChar, text.length);
211
+ const spans = await pipeline.chunkSpans(text);
212
+ chunkIndex = spans.findIndex((s) => s.startChar === startChar);
213
+ if (chunkIndex === -1) {
214
+ chunkIndex = spans.findIndex((s) => s.startChar <= startChar && startChar < s.endChar);
215
+ }
216
+ }
217
+ else {
218
+ chunkIndex = opts.chunkIndex ?? 0;
219
+ const spans = await pipeline.chunkSpans(text);
220
+ const span = spans[chunkIndex];
221
+ if (!span) {
222
+ throw new Error(`chunk ${chunkIndex} is out of range: ${path.basename(record.filePath)} has ${spans.length} chunk(s)`);
223
+ }
224
+ startChar = span.startChar;
225
+ endChar = span.endChar;
226
+ }
227
+ const passage = text.slice(startChar, endChar);
228
+ const url = record.canonicalUrl ?? record.provenance?.canonicalUrl ?? record.provenance?.url;
229
+ const stale = parsedAnchor
230
+ ? !anchorMatchesHash(opts.anchor, hash)
231
+ : hash !== record.contentHash;
232
+ return {
233
+ docId: `${record.scope}:${record.filePath}`,
234
+ filePath: record.filePath,
235
+ scope: record.scope,
236
+ chunkIndex: chunkIndex === -1 ? 0 : chunkIndex,
237
+ startChar,
238
+ endChar,
239
+ contentHash: hash,
240
+ anchor: citationAnchor(hash, startChar, endChar),
241
+ passage,
242
+ quote: passageQuote(passage),
243
+ ...(stale ? { stale: true } : {}),
244
+ ...(record.version ? { version: record.version } : {}),
245
+ ...(record.provenance?.title ? { title: record.provenance.title } : {}),
246
+ ...(url ? { url, citeUrl: citationUrl(url, passage) } : {}),
247
+ ...(record.provenance?.capturedAt ? { capturedAt: record.provenance.capturedAt } : {}),
248
+ ...(record.provenance?.source ? { source: record.provenance.source } : {}),
249
+ ...(record.provenance ? { provenance: record.provenance } : {}),
250
+ };
251
+ }
252
+ //# sourceMappingURL=citation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"citation.js","sourceRoot":"","sources":["../../../src/knowledge/citation.ts"],"names":[],"mappings":"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"}
@@ -4,6 +4,8 @@
4
4
  *
5
5
  * @module v1/cli/knowledge/document-pipeline
6
6
  */
7
+ import { type CaptureProvenance } from './capture-envelope.js';
8
+ import { type ChunkSpan } from './citation.js';
7
9
  export interface IngestResult {
8
10
  filePath: string;
9
11
  chunksIndexed: number;
@@ -15,6 +17,16 @@ export interface IngestResult {
15
17
  * none), and re-ingesting repairs it. Absent means the ingest was complete —
16
18
  * a caller must not read `chunksIndexed > 0` alone as success. */
17
19
  partial?: boolean;
20
+ /** RCL-06: the document was already indexed at this exact extracted-text
21
+ * hash, so nothing was written. Reported as "unchanged" rather than as a
22
+ * skip with an error, because nothing went wrong. */
23
+ unchanged?: boolean;
24
+ /** RCL-06: 1 for a first ingest, incremented for each stored revision. */
25
+ version?: number;
26
+ /** RCL-06: contentHash of the version this one replaced, when there was one. */
27
+ supersedes?: string;
28
+ /** RCL-07: provenance read from the capture envelope's `meta.json`. */
29
+ provenance?: CaptureProvenance;
18
30
  }
19
31
  export interface BatchIngestResult {
20
32
  filesProcessed: number;
@@ -35,6 +47,16 @@ export interface KnowledgeExcerpt {
35
47
  * re-ingested (its contentHash is no longer the file's current one). Only
36
48
  * ever set when the caller opted into `includeSuperseded`. */
37
49
  superseded?: boolean;
50
+ /** RCL-07: capture provenance, when the document came from a capture
51
+ * envelope — this is what lets a result cite the page it came from
52
+ * instead of a path under `~/.monomind/inbox`. */
53
+ provenance?: CaptureProvenance;
54
+ /** RCL-10: the chunk's character span against the extracted text. Absent
55
+ * for chunks stored before span tags existed — re-ingest to get them. */
56
+ startChar?: number;
57
+ endChar?: number;
58
+ /** RCL-10: `<hash12>#<start>-<end>`, resolvable with `monomind doc cite`. */
59
+ anchor?: string;
38
60
  }
39
61
  export interface DocumentMeta {
40
62
  filePath: string;
@@ -43,6 +65,16 @@ export interface DocumentMeta {
43
65
  indexedAt: string;
44
66
  scope: string;
45
67
  size: number;
68
+ /** RCL-06: identity URL for a captured page (`canonicalUrl`, else `url`,
69
+ * fragment stripped). Absent for ordinary files on disk. */
70
+ canonicalUrl?: string;
71
+ /** RCL-06: 1 for a first ingest, +1 per stored revision. */
72
+ version?: number;
73
+ /** RCL-06: contentHash of the version this record replaced. Walk it back
74
+ * through `listDocumentVersions` to reach the older ones. */
75
+ supersedes?: string;
76
+ /** RCL-07: the capture envelope's `meta.json`, normalized. */
77
+ provenance?: CaptureProvenance;
46
78
  }
47
79
  export declare function ingestDocument(filePath: string, scope?: string, rootDir?: string, _metadataCache?: DocumentMeta[]): Promise<IngestResult>;
48
80
  export declare function ingestDirectory(dirPath: string, scope?: string, opts?: {
@@ -85,6 +117,34 @@ export declare function searchKnowledge(query: string, opts?: {
85
117
  skipRerank?: boolean;
86
118
  }): Promise<KnowledgeExcerpt[]>;
87
119
  export declare function listDocuments(rootDir?: string, scope?: string): DocumentMeta[];
120
+ /**
121
+ * Every recorded version of one document, oldest first (RCL-06).
122
+ *
123
+ * `target` is either an indexed file path or a capture's `canonicalUrl`. Read
124
+ * from the append-only log rather than the last-wins view, which is how a
125
+ * superseded version stays addressable after its record has been replaced or
126
+ * tombstoned.
127
+ *
128
+ * Best-effort by design: `readMetadata` compacts the log once it passes 1MB
129
+ * and keeps only live records, so history older than a compaction is gone.
130
+ * The `supersedes` pointer on each record is the durable part.
131
+ */
132
+ export declare function listDocumentVersions(rootDir?: string, target?: string, scope?: string): DocumentMeta[];
133
+ /** The store root for a scope: the global brain for `global`, else the project. */
134
+ export declare function getKnowledgeRoot(scope?: string, rootDir?: string): string;
135
+ /** Chunk spans for a text, using the SAME chunker the ingest used — this is
136
+ * what makes a chunk index resolvable back to a character range without
137
+ * storing the text twice. */
138
+ export declare function chunkSpans(text: string): Promise<ChunkSpan[]>;
139
+ /**
140
+ * The indexed record for whatever a caller wrote down: a file path, a
141
+ * capture's `canonicalUrl` (fragment ignored), or the `<scope>:<path>` docId.
142
+ *
143
+ * Live records win. A superseded version is only reached through the
144
+ * append-only log, and only when nothing live matches — otherwise citing a
145
+ * re-captured page would resolve against the version it replaced.
146
+ */
147
+ export declare function findDocumentRecord(rootDir?: string, target?: string, scope?: string): DocumentMeta | undefined;
88
148
  export declare function removeDocument(filePath: string, scope?: string, rootDir?: string): Promise<void>;
89
149
  /**
90
150
  * True for macOS AppleDouble sidecars (`._name`).
@@ -1 +1 @@
1
- {"version":3,"file":"document-pipeline.d.ts","sourceRoot":"","sources":["../../../src/knowledge/document-pipeline.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"document-pipeline.d.ts","sourceRoot":"","sources":["../../../src/knowledge/document-pipeline.ts"],"names":[],"mappings":"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"}
@@ -14,6 +14,8 @@ import { DOC_EXTENSIONS, extractText } from '../capabilities/cap-documents.js';
14
14
  // be duplicated — two copies of "which directory is this project" is exactly the
15
15
  // bug this default exists to fix.
16
16
  import { getProjectRoot } from '../memory/memory-bridge.js';
17
+ import { captureIdentityUrl, ENVELOPE_DOCUMENTS, ENVELOPE_READABLE_FILE, envelopePrimaryDocument, readCaptureProvenance, } from './capture-envelope.js';
18
+ import { citationAnchor, parseSpanTag, spanTag } from './citation.js';
17
19
  const DEFAULT_CHUNK_SIZE = 3200;
18
20
  const DEFAULT_OVERLAP = 400;
19
21
  // Head-of-chunk cap for text served by searchKnowledge — chunks are
@@ -311,6 +313,26 @@ function metadataPath(rootDir) {
311
313
  fs.mkdirSync(dir, { recursive: true });
312
314
  return path.join(dir, METADATA_FILE);
313
315
  }
316
+ /** Every record ever appended, in order, including superseded versions and
317
+ * removal tombstones. Reads the path directly instead of via `metadataPath`,
318
+ * which mkdir's. */
319
+ function readMetadataLog(rootDir) {
320
+ const file = path.join(rootDir, '.monomind', 'knowledge', METADATA_FILE);
321
+ if (!fs.existsSync(file))
322
+ return [];
323
+ const out = [];
324
+ for (const line of fs.readFileSync(file, 'utf-8').split('\n')) {
325
+ if (!line.trim())
326
+ continue;
327
+ try {
328
+ out.push(JSON.parse(line));
329
+ }
330
+ catch {
331
+ /* torn line */
332
+ }
333
+ }
334
+ return out;
335
+ }
314
336
  function readMetadata(rootDir) {
315
337
  const file = metadataPath(rootDir);
316
338
  if (!fs.existsSync(file))
@@ -443,9 +465,41 @@ export async function ingestDocument(filePath, scope = 'shared', rootDir = getPr
443
465
  error: 'file too large (>50MB)',
444
466
  };
445
467
  }
468
+ // RCL-01/RCL-06: one capture envelope is ONE document, whichever of its
469
+ // members the caller points at. `page.html`/`page.mhtml` need no guard —
470
+ // extraction already redirects them to `readable.md`, so they hash
471
+ // identically and fall out as "unchanged" below. `page.pdf` does not: its
472
+ // text differs from the readable pass, so ingesting it alongside would
473
+ // version-flip the same page back and forth on every sweep.
474
+ const envelopePrimary = envelopePrimaryDocument(path.dirname(resolved));
475
+ if (envelopePrimary && envelopePrimary !== resolved) {
476
+ const isMember = ENVELOPE_DOCUMENTS.includes(path.basename(resolved));
477
+ const redirectsToPrimary = path.basename(envelopePrimary) === ENVELOPE_READABLE_FILE &&
478
+ (ext === '.html' || ext === '.htm' || ext === '.xhtml' || ext === '.mhtml' || ext === '.mht');
479
+ if (isMember && !redirectsToPrimary) {
480
+ return {
481
+ filePath: resolved,
482
+ chunksIndexed: 0,
483
+ scope,
484
+ skipped: true,
485
+ error: `capture envelope: ${path.basename(envelopePrimary)} is this capture's document`,
486
+ };
487
+ }
488
+ }
446
489
  rootDir = effectiveRoot(scope, rootDir);
447
490
  const meta = _metadataCache ?? readMetadata(rootDir);
448
- const existing = meta.find((m) => m.filePath === resolved && m.scope === scope);
491
+ // RCL-07: provenance is read before extraction so it is recorded even when a
492
+ // later step degrades. Absent, truncated or wrong-typed `meta.json` yields
493
+ // null and never throws — see capture-envelope.
494
+ const provenance = readCaptureProvenance(resolved);
495
+ const canonicalUrl = captureIdentityUrl(provenance);
496
+ // RCL-06: identity is the PAGE, not the path. A re-capture lands in a new
497
+ // timestamped directory, so matching on filePath alone would file every
498
+ // capture of one article as a separate document.
499
+ const existing = meta.find((m) => m.filePath === resolved && m.scope === scope) ??
500
+ (canonicalUrl
501
+ ? meta.find((m) => m.scope === scope && m.canonicalUrl === canonicalUrl)
502
+ : undefined);
449
503
  let fullContent;
450
504
  try {
451
505
  const entry = toFileEntry(resolved);
@@ -464,9 +518,21 @@ export async function ingestDocument(filePath, scope = 'shared', rootDir = getPr
464
518
  };
465
519
  }
466
520
  const hash = contentHash(fullContent);
521
+ // RCL-06: same page, same extracted text — a no-op, not a duplicate row and
522
+ // not an error. `unchanged` is what a caller reports to the user.
467
523
  if (existing && existing.contentHash === hash) {
468
- return { filePath: resolved, chunksIndexed: existing.chunkCount, scope, skipped: true };
524
+ return {
525
+ filePath: resolved,
526
+ chunksIndexed: existing.chunkCount,
527
+ scope,
528
+ skipped: true,
529
+ unchanged: true,
530
+ ...(existing.version ? { version: existing.version } : {}),
531
+ ...(provenance ? { provenance } : {}),
532
+ };
469
533
  }
534
+ const version = (existing?.version ?? (existing ? 1 : 0)) + 1;
535
+ const supersedes = existing?.contentHash || undefined;
470
536
  // NOTE: the previous version's metadata record is deliberately NOT tombstoned
471
537
  // here. `readMetadata` is last-wins per (filePath, scope), so appending the
472
538
  // new record below already supersedes the old one — the tombstone was a no-op
@@ -488,7 +554,15 @@ export async function ingestDocument(filePath, scope = 'shared', rootDir = getPr
488
554
  value: chunk.text,
489
555
  namespace: namespace(scope),
490
556
  generateEmbeddingFlag: true,
491
- tags: ['document', ext, `src:${resolved}`],
557
+ tags: [
558
+ 'document',
559
+ ext,
560
+ `src:${resolved}`,
561
+ // RCL-10: the chunk's span against the extracted text, so a search
562
+ // hit can cite a passage without re-reading the document.
563
+ spanTag(chunk.startChar, chunk.endChar),
564
+ ...(canonicalUrl ? [`url:${canonicalUrl}`] : []),
565
+ ],
492
566
  upsert: true,
493
567
  dbPath: storeDbPath(scope),
494
568
  });
@@ -522,13 +596,31 @@ export async function ingestDocument(filePath, scope = 'shared', rootDir = getPr
522
596
  indexedAt: new Date().toISOString(),
523
597
  scope,
524
598
  size: stat.size,
599
+ version,
600
+ ...(supersedes ? { supersedes } : {}),
601
+ ...(canonicalUrl ? { canonicalUrl } : {}),
602
+ ...(provenance ? { provenance } : {}),
525
603
  });
604
+ // A re-capture of the same page arrives at a NEW path, so the previous
605
+ // version's record is a different (filePath, scope) key and survives
606
+ // last-wins — leaving its contentHash live and its chunks answering
607
+ // searches forever. Tombstone it so it leaves the live-hash set, which is
608
+ // exactly how a same-path re-ingest already retires its predecessor.
609
+ //
610
+ // AFTER the append, never before: retiring the old version before the
611
+ // replacement is known to have landed is the failure mode the partial-store
612
+ // fix above exists to prevent.
613
+ if (existing && existing.filePath !== resolved) {
614
+ removeMetadataEntry(rootDir, existing.filePath, scope);
615
+ }
526
616
  }
527
617
  return {
528
618
  filePath: resolved,
529
619
  chunksIndexed: indexed,
530
620
  scope,
531
621
  skipped: false,
622
+ ...(complete ? { version, ...(supersedes ? { supersedes } : {}) } : {}),
623
+ ...(provenance ? { provenance } : {}),
532
624
  ...(complete
533
625
  ? {}
534
626
  : indexed > 0
@@ -716,8 +808,12 @@ export async function searchKnowledge(query, opts) {
716
808
  if (!result?.success || !result.results.length)
717
809
  return [];
718
810
  const hashToFile = new Map();
719
- for (const m of meta)
811
+ const hashToProvenance = new Map();
812
+ for (const m of meta) {
720
813
  hashToFile.set(m.contentHash, m.filePath);
814
+ if (m.provenance)
815
+ hashToProvenance.set(m.contentHash, m.provenance);
816
+ }
721
817
  const kept = includeSuperseded
722
818
  ? result.results
723
819
  : result.results.filter((r) => !isSupersededKey(String(r.key ?? ''), live, hasMeta));
@@ -730,6 +826,10 @@ export async function searchKnowledge(query, opts) {
730
826
  // content, and goes empty when a re-ingested file's hash changed.
731
827
  const srcTag = (r.tags ?? []).find((tag) => tag.startsWith('src:'));
732
828
  const superseded = includeSuperseded && isSupersededKey(String(r.key ?? ''), live, hasMeta);
829
+ // RCL-10: offsets ride on the chunk's own `span:` tag, so citing a hit
830
+ // costs nothing here. Chunks stored before span tags simply have none,
831
+ // and `doc cite --chunk` recomputes them from the document.
832
+ const span = parseSpanTag(r.tags);
733
833
  return {
734
834
  id: r.id,
735
835
  filePath: srcTag ? srcTag.slice(4) : (hashToFile.get(hash) ?? ''),
@@ -743,6 +843,17 @@ export async function searchKnowledge(query, opts) {
743
843
  chunkIndex: Number.isNaN(idx) ? 0 : idx,
744
844
  scope: t.label,
745
845
  ...(superseded ? { superseded: true } : {}),
846
+ // RCL-07: only LIVE versions carry provenance here — a superseded
847
+ // chunk's record is no longer in the live metadata. Its `url:` tag
848
+ // still identifies the page it came from.
849
+ ...(hashToProvenance.has(hash) ? { provenance: hashToProvenance.get(hash) } : {}),
850
+ ...(span
851
+ ? {
852
+ startChar: span.startChar,
853
+ endChar: span.endChar,
854
+ anchor: citationAnchor(hash, span.startChar, span.endChar),
855
+ }
856
+ : {}),
746
857
  };
747
858
  });
748
859
  }));
@@ -756,6 +867,79 @@ export function listDocuments(rootDir = getProjectRoot(), scope) {
756
867
  const all = readMetadata(rootDir);
757
868
  return scope ? all.filter((m) => m.scope === scope) : all;
758
869
  }
870
+ /**
871
+ * Every recorded version of one document, oldest first (RCL-06).
872
+ *
873
+ * `target` is either an indexed file path or a capture's `canonicalUrl`. Read
874
+ * from the append-only log rather than the last-wins view, which is how a
875
+ * superseded version stays addressable after its record has been replaced or
876
+ * tombstoned.
877
+ *
878
+ * Best-effort by design: `readMetadata` compacts the log once it passes 1MB
879
+ * and keeps only live records, so history older than a compaction is gone.
880
+ * The `supersedes` pointer on each record is the durable part.
881
+ */
882
+ export function listDocumentVersions(rootDir = getProjectRoot(), target, scope) {
883
+ const resolved = target ? path.resolve(target) : undefined;
884
+ return readMetadataLog(rootDir)
885
+ .filter((m) => m.chunkCount >= 0)
886
+ .filter((m) => !scope || m.scope === scope)
887
+ .filter((m) => !target || m.filePath === resolved || m.filePath === target || m.canonicalUrl === target)
888
+ .sort((a, b) => (a.version ?? 0) - (b.version ?? 0) || a.indexedAt.localeCompare(b.indexedAt));
889
+ }
890
+ // ── Citation / lookup helpers (RCL-10) ─────────────────────────────
891
+ /** The store root for a scope: the global brain for `global`, else the project. */
892
+ export function getKnowledgeRoot(scope = 'shared', rootDir = getProjectRoot()) {
893
+ return effectiveRoot(scope, rootDir);
894
+ }
895
+ /** Chunk spans for a text, using the SAME chunker the ingest used — this is
896
+ * what makes a chunk index resolvable back to a character range without
897
+ * storing the text twice. */
898
+ export async function chunkSpans(text) {
899
+ const chunks = await chunkDocument('cite', text);
900
+ return chunks.map((c) => ({
901
+ chunkIndex: c.chunkIndex,
902
+ startChar: c.startChar,
903
+ endChar: c.endChar,
904
+ }));
905
+ }
906
+ /**
907
+ * The indexed record for whatever a caller wrote down: a file path, a
908
+ * capture's `canonicalUrl` (fragment ignored), or the `<scope>:<path>` docId.
909
+ *
910
+ * Live records win. A superseded version is only reached through the
911
+ * append-only log, and only when nothing live matches — otherwise citing a
912
+ * re-captured page would resolve against the version it replaced.
913
+ */
914
+ export function findDocumentRecord(rootDir = getProjectRoot(), target = '', scope) {
915
+ const raw = target.trim();
916
+ if (!raw)
917
+ return undefined;
918
+ // `<scope>:<path>` — but not `https://…`, whose colon is a URL scheme.
919
+ const scoped = /^([a-z][a-z0-9_-]*):(?!\/\/)(.+)$/i.exec(raw);
920
+ const candidates = scoped ? [raw, scoped[2]] : [raw];
921
+ const hash = raw.indexOf('#');
922
+ if (hash > 0)
923
+ candidates.push(raw.slice(0, hash));
924
+ const resolvedPaths = new Set(candidates.map((c) => path.resolve(c)));
925
+ const wanted = new Set(candidates);
926
+ const scopeFilter = scope ?? (scoped ? scoped[1] : undefined);
927
+ const matches = (m) => {
928
+ if (scopeFilter && m.scope !== scopeFilter && !wanted.has(m.filePath))
929
+ return false;
930
+ if (wanted.has(m.filePath) || resolvedPaths.has(path.resolve(m.filePath)))
931
+ return true;
932
+ return !!m.canonicalUrl && wanted.has(m.canonicalUrl);
933
+ };
934
+ const live = readMetadata(rootDir).filter(matches);
935
+ if (live.length) {
936
+ return live.sort((a, b) => (b.version ?? 0) - (a.version ?? 0))[0];
937
+ }
938
+ const historical = readMetadataLog(rootDir)
939
+ .filter((m) => m.chunkCount >= 0)
940
+ .filter(matches);
941
+ return historical.sort((a, b) => (b.version ?? 0) - (a.version ?? 0))[0];
942
+ }
759
943
  export async function removeDocument(filePath, scope = 'shared', rootDir = getProjectRoot()) {
760
944
  removeMetadataEntry(rootDir, path.resolve(filePath), scope);
761
945
  // SQLite cleanup: bridge doesn't expose delete-by-key, so metadata removal is sufficient.