@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.
- package/.claude/skills/monodesign/scripts/context.mjs +1 -1
- package/.claude/skills/monodesign/scripts/detector/detect-antipatterns-browser.js +2 -2
- package/.claude/skills/monodesign/scripts/detector/rules/checks.mjs +2 -2
- package/.claude/skills/monodesign/scripts/live-browser.js +1 -1
- package/bin/cli.js +21 -1
- package/bin/mcp-server.js +21 -1
- package/dist/src/capabilities/cap-documents.d.ts.map +1 -1
- package/dist/src/capabilities/cap-documents.js +23 -0
- package/dist/src/capabilities/cap-documents.js.map +1 -1
- package/dist/src/commands/doc-filters.d.ts +19 -0
- package/dist/src/commands/doc-filters.d.ts.map +1 -0
- package/dist/src/commands/doc-filters.js +58 -0
- package/dist/src/commands/doc-filters.js.map +1 -0
- package/dist/src/commands/doc-library.d.ts +21 -0
- package/dist/src/commands/doc-library.d.ts.map +1 -0
- package/dist/src/commands/doc-library.js +390 -0
- package/dist/src/commands/doc-library.js.map +1 -0
- package/dist/src/commands/doc-list.d.ts +9 -0
- package/dist/src/commands/doc-list.d.ts.map +1 -0
- package/dist/src/commands/doc-list.js +99 -0
- package/dist/src/commands/doc-list.js.map +1 -0
- package/dist/src/commands/doc.d.ts.map +1 -1
- package/dist/src/commands/doc.js +49 -37
- package/dist/src/commands/doc.js.map +1 -1
- package/dist/src/commands/doctor-mcp-probe.d.ts +48 -0
- package/dist/src/commands/doctor-mcp-probe.d.ts.map +1 -0
- package/dist/src/commands/doctor-mcp-probe.js +205 -0
- package/dist/src/commands/doctor-mcp-probe.js.map +1 -0
- package/dist/src/commands/doctor-project-checks.d.ts +12 -1
- package/dist/src/commands/doctor-project-checks.d.ts.map +1 -1
- package/dist/src/commands/doctor-project-checks.js +55 -4
- package/dist/src/commands/doctor-project-checks.js.map +1 -1
- package/dist/src/commands/doctor.d.ts.map +1 -1
- package/dist/src/commands/doctor.js +10 -1
- package/dist/src/commands/doctor.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +17 -0
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/mcp.d.ts.map +1 -1
- package/dist/src/commands/mcp.js +4 -3
- package/dist/src/commands/mcp.js.map +1 -1
- package/dist/src/commands/org-observe.d.ts.map +1 -1
- package/dist/src/commands/org-observe.js +14 -0
- package/dist/src/commands/org-observe.js.map +1 -1
- package/dist/src/commands/org.d.ts.map +1 -1
- package/dist/src/commands/org.js +12 -2
- package/dist/src/commands/org.js.map +1 -1
- package/dist/src/init/claudemd-generator.d.ts.map +1 -1
- package/dist/src/init/claudemd-generator.js +2 -1
- package/dist/src/init/claudemd-generator.js.map +1 -1
- package/dist/src/init/mcp-generator.d.ts.map +1 -1
- package/dist/src/init/mcp-generator.js +3 -3
- package/dist/src/init/mcp-generator.js.map +1 -1
- package/dist/src/init/types.d.ts +6 -0
- package/dist/src/init/types.d.ts.map +1 -1
- package/dist/src/init/types.js.map +1 -1
- package/dist/src/init/write-runtime-config.d.ts.map +1 -1
- package/dist/src/init/write-runtime-config.js +12 -2
- package/dist/src/init/write-runtime-config.js.map +1 -1
- package/dist/src/knowledge/capture-envelope.d.ts +80 -0
- package/dist/src/knowledge/capture-envelope.d.ts.map +1 -0
- package/dist/src/knowledge/capture-envelope.js +165 -0
- package/dist/src/knowledge/capture-envelope.js.map +1 -0
- package/dist/src/knowledge/capture-text.d.ts +23 -0
- package/dist/src/knowledge/capture-text.d.ts.map +1 -0
- package/dist/src/knowledge/capture-text.js +49 -0
- package/dist/src/knowledge/capture-text.js.map +1 -0
- package/dist/src/knowledge/citation.d.ts +121 -0
- package/dist/src/knowledge/citation.d.ts.map +1 -0
- package/dist/src/knowledge/citation.js +252 -0
- package/dist/src/knowledge/citation.js.map +1 -0
- package/dist/src/knowledge/document-pipeline.d.ts +60 -0
- package/dist/src/knowledge/document-pipeline.d.ts.map +1 -1
- package/dist/src/knowledge/document-pipeline.js +188 -4
- package/dist/src/knowledge/document-pipeline.js.map +1 -1
- package/dist/src/knowledge/html-extract.d.ts +43 -0
- package/dist/src/knowledge/html-extract.d.ts.map +1 -0
- package/dist/src/knowledge/html-extract.js +286 -0
- package/dist/src/knowledge/html-extract.js.map +1 -0
- package/dist/src/knowledge/html-tags.d.ts +30 -0
- package/dist/src/knowledge/html-tags.d.ts.map +1 -0
- package/dist/src/knowledge/html-tags.js +229 -0
- package/dist/src/knowledge/html-tags.js.map +1 -0
- package/dist/src/knowledge/library.d.ts +106 -0
- package/dist/src/knowledge/library.d.ts.map +1 -0
- package/dist/src/knowledge/library.js +195 -0
- package/dist/src/knowledge/library.js.map +1 -0
- package/dist/src/knowledge/mhtml.d.ts +60 -0
- package/dist/src/knowledge/mhtml.d.ts.map +1 -0
- package/dist/src/knowledge/mhtml.js +154 -0
- package/dist/src/knowledge/mhtml.js.map +1 -0
- package/dist/src/knowledge/related.d.ts +58 -0
- package/dist/src/knowledge/related.d.ts.map +1 -0
- package/dist/src/knowledge/related.js +222 -0
- package/dist/src/knowledge/related.js.map +1 -0
- package/dist/src/knowledge/section-diff.d.ts +48 -0
- package/dist/src/knowledge/section-diff.d.ts.map +1 -0
- package/dist/src/knowledge/section-diff.js +151 -0
- package/dist/src/knowledge/section-diff.js.map +1 -0
- package/dist/src/knowledge/watch.d.ts +92 -0
- package/dist/src/knowledge/watch.d.ts.map +1 -0
- package/dist/src/knowledge/watch.js +221 -0
- package/dist/src/knowledge/watch.js.map +1 -0
- package/dist/src/mcp-server.d.ts.map +1 -1
- package/dist/src/mcp-server.js +44 -87
- package/dist/src/mcp-server.js.map +1 -1
- package/dist/src/mcp-tools/browser-instrument-tools.d.ts +35 -0
- package/dist/src/mcp-tools/browser-instrument-tools.d.ts.map +1 -0
- package/dist/src/mcp-tools/browser-instrument-tools.js +359 -0
- package/dist/src/mcp-tools/browser-instrument-tools.js.map +1 -0
- package/dist/src/mcp-tools/browser-metrics.d.ts +42 -0
- package/dist/src/mcp-tools/browser-metrics.d.ts.map +1 -0
- package/dist/src/mcp-tools/browser-metrics.js +91 -0
- package/dist/src/mcp-tools/browser-metrics.js.map +1 -0
- package/dist/src/mcp-tools/browser-profile-tools.d.ts +16 -0
- package/dist/src/mcp-tools/browser-profile-tools.d.ts.map +1 -0
- package/dist/src/mcp-tools/browser-profile-tools.js +324 -0
- package/dist/src/mcp-tools/browser-profile-tools.js.map +1 -0
- package/dist/src/mcp-tools/browser-session.d.ts +68 -0
- package/dist/src/mcp-tools/browser-session.d.ts.map +1 -0
- package/dist/src/mcp-tools/browser-session.js +224 -0
- package/dist/src/mcp-tools/browser-session.js.map +1 -0
- package/dist/src/mcp-tools/browser-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/browser-tools.js +10 -176
- package/dist/src/mcp-tools/browser-tools.js.map +1 -1
- package/dist/src/mcp-tools/capture-resource-read.d.ts +115 -0
- package/dist/src/mcp-tools/capture-resource-read.d.ts.map +1 -0
- package/dist/src/mcp-tools/capture-resource-read.js +296 -0
- package/dist/src/mcp-tools/capture-resource-read.js.map +1 -0
- package/dist/src/mcp-tools/capture-resource-tools.d.ts +22 -0
- package/dist/src/mcp-tools/capture-resource-tools.d.ts.map +1 -0
- package/dist/src/mcp-tools/capture-resource-tools.js +182 -0
- package/dist/src/mcp-tools/capture-resource-tools.js.map +1 -0
- package/dist/src/mcp-tools/capture-resources.d.ts +142 -0
- package/dist/src/mcp-tools/capture-resources.d.ts.map +1 -0
- package/dist/src/mcp-tools/capture-resources.js +289 -0
- package/dist/src/mcp-tools/capture-resources.js.map +1 -0
- package/dist/src/mcp-tools/index.d.ts +3 -0
- package/dist/src/mcp-tools/index.d.ts.map +1 -1
- package/dist/src/mcp-tools/index.js +6 -0
- package/dist/src/mcp-tools/index.js.map +1 -1
- package/dist/src/mcp-tools/knowledge-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/knowledge-tools.js +9 -1
- package/dist/src/mcp-tools/knowledge-tools.js.map +1 -1
- package/dist/src/mcp-tools/resource-router.d.ts +86 -0
- package/dist/src/mcp-tools/resource-router.d.ts.map +1 -0
- package/dist/src/mcp-tools/resource-router.js +181 -0
- package/dist/src/mcp-tools/resource-router.js.map +1 -0
- package/dist/src/memory/memory-bridge.js +1 -1
- package/dist/src/memory/memory-bridge.js.map +1 -1
- package/dist/src/orgrt/agent-runner.d.ts +38 -0
- package/dist/src/orgrt/agent-runner.d.ts.map +1 -1
- package/dist/src/orgrt/agent-runner.js +47 -0
- package/dist/src/orgrt/agent-runner.js.map +1 -1
- package/dist/src/orgrt/checkpoint.d.ts +12 -1
- package/dist/src/orgrt/checkpoint.d.ts.map +1 -1
- package/dist/src/orgrt/checkpoint.js +15 -6
- package/dist/src/orgrt/checkpoint.js.map +1 -1
- package/dist/src/orgrt/completion-gate.d.ts +74 -0
- package/dist/src/orgrt/completion-gate.d.ts.map +1 -1
- package/dist/src/orgrt/completion-gate.js +45 -0
- package/dist/src/orgrt/completion-gate.js.map +1 -1
- package/dist/src/orgrt/cost-tier.d.ts +144 -0
- package/dist/src/orgrt/cost-tier.d.ts.map +1 -0
- package/dist/src/orgrt/cost-tier.js +183 -0
- package/dist/src/orgrt/cost-tier.js.map +1 -0
- package/dist/src/orgrt/cross-org.d.ts.map +1 -1
- package/dist/src/orgrt/cross-org.js +14 -0
- package/dist/src/orgrt/cross-org.js.map +1 -1
- package/dist/src/orgrt/daemon.d.ts +12 -1
- package/dist/src/orgrt/daemon.d.ts.map +1 -1
- package/dist/src/orgrt/daemon.js +216 -49
- package/dist/src/orgrt/daemon.js.map +1 -1
- package/dist/src/orgrt/decisions.d.ts +14 -2
- package/dist/src/orgrt/decisions.d.ts.map +1 -1
- package/dist/src/orgrt/decisions.js +216 -10
- package/dist/src/orgrt/decisions.js.map +1 -1
- package/dist/src/orgrt/file-roots.d.ts +7 -0
- package/dist/src/orgrt/file-roots.d.ts.map +1 -1
- package/dist/src/orgrt/file-roots.js +38 -1
- package/dist/src/orgrt/file-roots.js.map +1 -1
- package/dist/src/orgrt/forwarder.d.ts.map +1 -1
- package/dist/src/orgrt/forwarder.js +16 -2
- package/dist/src/orgrt/forwarder.js.map +1 -1
- package/dist/src/orgrt/idle-deadline.d.ts +74 -1
- package/dist/src/orgrt/idle-deadline.d.ts.map +1 -1
- package/dist/src/orgrt/idle-deadline.js +52 -2
- package/dist/src/orgrt/idle-deadline.js.map +1 -1
- package/dist/src/orgrt/loadouts.d.ts +38 -0
- package/dist/src/orgrt/loadouts.d.ts.map +1 -0
- package/dist/src/orgrt/loadouts.js +129 -0
- package/dist/src/orgrt/loadouts.js.map +1 -0
- package/dist/src/orgrt/mailbox.d.ts +17 -1
- package/dist/src/orgrt/mailbox.d.ts.map +1 -1
- package/dist/src/orgrt/mailbox.js +50 -3
- package/dist/src/orgrt/mailbox.js.map +1 -1
- package/dist/src/orgrt/policy.d.ts +42 -2
- package/dist/src/orgrt/policy.d.ts.map +1 -1
- package/dist/src/orgrt/policy.js +53 -8
- package/dist/src/orgrt/policy.js.map +1 -1
- package/dist/src/orgrt/prompt-vars.d.ts +11 -0
- package/dist/src/orgrt/prompt-vars.d.ts.map +1 -0
- package/dist/src/orgrt/prompt-vars.js +49 -0
- package/dist/src/orgrt/prompt-vars.js.map +1 -0
- package/dist/src/orgrt/questions.d.ts +22 -1
- package/dist/src/orgrt/questions.d.ts.map +1 -1
- package/dist/src/orgrt/questions.js +24 -3
- package/dist/src/orgrt/questions.js.map +1 -1
- package/dist/src/orgrt/review-packet.d.ts +29 -0
- package/dist/src/orgrt/review-packet.d.ts.map +1 -0
- package/dist/src/orgrt/review-packet.js +67 -0
- package/dist/src/orgrt/review-packet.js.map +1 -0
- package/dist/src/orgrt/role-sandbox.d.ts.map +1 -1
- package/dist/src/orgrt/role-sandbox.js +16 -3
- package/dist/src/orgrt/role-sandbox.js.map +1 -1
- package/dist/src/orgrt/session-ledger.d.ts +68 -0
- package/dist/src/orgrt/session-ledger.d.ts.map +1 -0
- package/dist/src/orgrt/session-ledger.js +128 -0
- package/dist/src/orgrt/session-ledger.js.map +1 -0
- package/dist/src/orgrt/session.d.ts +45 -5
- package/dist/src/orgrt/session.d.ts.map +1 -1
- package/dist/src/orgrt/session.js +397 -27
- package/dist/src/orgrt/session.js.map +1 -1
- package/dist/src/orgrt/task-dag.d.ts +34 -1
- package/dist/src/orgrt/task-dag.d.ts.map +1 -1
- package/dist/src/orgrt/task-dag.js +58 -7
- package/dist/src/orgrt/task-dag.js.map +1 -1
- package/dist/src/orgrt/tool-spill.d.ts +94 -0
- package/dist/src/orgrt/tool-spill.d.ts.map +1 -0
- package/dist/src/orgrt/tool-spill.js +180 -0
- package/dist/src/orgrt/tool-spill.js.map +1 -0
- package/dist/src/orgrt/types.d.ts +138 -0
- package/dist/src/orgrt/types.d.ts.map +1 -1
- package/dist/src/orgrt/types.js +148 -0
- package/dist/src/orgrt/types.js.map +1 -1
- package/dist/src/platform-adapters/renderers/mcp.d.ts +20 -2
- package/dist/src/platform-adapters/renderers/mcp.d.ts.map +1 -1
- package/dist/src/platform-adapters/renderers/mcp.js +25 -5
- package/dist/src/platform-adapters/renderers/mcp.js.map +1 -1
- package/dist/src/protocol-capabilities.d.ts +2 -1
- package/dist/src/protocol-capabilities.d.ts.map +1 -1
- package/dist/src/protocol-capabilities.js +2 -1
- package/dist/src/protocol-capabilities.js.map +1 -1
- package/dist/src/ui/dashboard.html +450 -164
- package/dist/src/ui/org-hil.mjs +276 -0
- package/dist/src/ui/org-runtime.mjs +350 -0
- package/dist/src/ui/orgs.html +15 -4
- package/dist/src/ui/routes-org.mjs +290 -541
- package/dist/src/ui/server.mjs +25 -5
- package/dist/tsconfig.tsbuildinfo +1 -1
- 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":"AAAA;;;;;GAKG;
|
|
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
|
-
|
|
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 {
|
|
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: [
|
|
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
|
-
|
|
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.
|