@hraness/kb 0.17.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/DISCLOSURE +43 -0
- package/LICENSE +21 -0
- package/README.md +545 -0
- package/dist/agent-context.js +35 -0
- package/dist/agent-guide-audit.js +18 -0
- package/dist/attachments.js +19 -0
- package/dist/authoring.js +33 -0
- package/dist/benchmark.js +19 -0
- package/dist/capture.js +49 -0
- package/dist/cli.js +3646 -0
- package/dist/clip/acquire.js +57 -0
- package/dist/clip/args.js +21 -0
- package/dist/clip/bounded-byte-buffer.js +9 -0
- package/dist/clip/browser-profiles.js +17 -0
- package/dist/clip/bundle-reader.js +11 -0
- package/dist/clip/cli.js +27 -0
- package/dist/clip/cookies.js +21 -0
- package/dist/clip/defuddle-worker.js +84 -0
- package/dist/clip/doctor.js +32 -0
- package/dist/clip/jobs.js +784 -0
- package/dist/clip/network-proxy.js +9 -0
- package/dist/clip/network.js +28 -0
- package/dist/clip/persist.js +30 -0
- package/dist/clip/refresh.js +11 -0
- package/dist/clip/terminal.js +11 -0
- package/dist/evaluation-builder.js +12238 -0
- package/dist/evaluation-kb.js +26 -0
- package/dist/evaluation.js +35 -0
- package/dist/git.js +39 -0
- package/dist/graph.js +39 -0
- package/dist/index-01jj6rbv.js +1103 -0
- package/dist/index-06c9ctr6.js +738 -0
- package/dist/index-0kavxzqj.js +2480 -0
- package/dist/index-1gwbassd.js +743 -0
- package/dist/index-1n418kb9.js +827 -0
- package/dist/index-1vrd1rmn.js +56 -0
- package/dist/index-1xxnjn0d.js +171 -0
- package/dist/index-3rm7cz6h.js +373 -0
- package/dist/index-3v2z4f0q.js +328 -0
- package/dist/index-48pz4jpc.js +392 -0
- package/dist/index-4j3tt0c3.js +234 -0
- package/dist/index-5n05se68.js +307 -0
- package/dist/index-5vwpzb5a.js +764 -0
- package/dist/index-6g2pv9d2.js +370 -0
- package/dist/index-7fzc32gf.js +712 -0
- package/dist/index-84x0vjjp.js +432 -0
- package/dist/index-adx6khj5.js +488 -0
- package/dist/index-b88v3vtm.js +805 -0
- package/dist/index-cv6fh7z5.js +462 -0
- package/dist/index-cxfrakt7.js +1067 -0
- package/dist/index-d13v9ckt.js +150 -0
- package/dist/index-dycjbhw2.js +1943 -0
- package/dist/index-dyqwejk5.js +531 -0
- package/dist/index-e5fbsywq.js +658 -0
- package/dist/index-f984hw45.js +927 -0
- package/dist/index-gh719d91.js +63 -0
- package/dist/index-hgve9rh2.js +2046 -0
- package/dist/index-hya40gb2.js +474 -0
- package/dist/index-j4zgmzjr.js +152 -0
- package/dist/index-jsmvyyvf.js +1166 -0
- package/dist/index-kh0pwvdd.js +1974 -0
- package/dist/index-mqx4nd6v.js +119 -0
- package/dist/index-mxxxytys.js +1254 -0
- package/dist/index-n5dd7r0v.js +711 -0
- package/dist/index-npg9z1a4.js +431 -0
- package/dist/index-pj501bh1.js +140 -0
- package/dist/index-qry4vhxk.js +1041 -0
- package/dist/index-s2gw5aw9.js +199 -0
- package/dist/index-vvdj6kca.js +70 -0
- package/dist/index-w2zc0vwa.js +571 -0
- package/dist/index-x3fthpsc.js +585 -0
- package/dist/index-zr53sf63.js +44 -0
- package/dist/index-zxdy5pby.js +2728 -0
- package/dist/index-zzhgcwyt.js +966 -0
- package/dist/index.js +453 -0
- package/dist/navigation.js +13 -0
- package/dist/pdf.js +85 -0
- package/dist/percolate.js +24 -0
- package/dist/portfolio.js +77 -0
- package/dist/query.js +29 -0
- package/dist/repository-memory.js +60 -0
- package/dist/sdk.js +36 -0
- package/dist/search-rules.js +34 -0
- package/dist/search.js +22 -0
- package/dist/semantic.js +39 -0
- package/dist/source-inbox.js +17 -0
- package/dist/untrusted-content.js +32 -0
- package/dist/url-intelligence.js +74 -0
- package/dist/workflow.js +23 -0
- package/dist/workflows/decision-context.js +20 -0
- package/dist/workflows/explain-change.js +8 -0
- package/dist/workflows/index.js +28 -0
- package/dist/workflows/plan-radar.js +10 -0
- package/package.json +409 -0
- package/skills/kb/AGENTS.md +19 -0
- package/skills/kb/SKILL.md +82 -0
- package/skills/kb/agents/openai.yaml +4 -0
- package/skills/kb/references/pdf-review.md +51 -0
- package/skills/kb/references/percolate.md +144 -0
- package/skills/kb/references/plan-structure.md +105 -0
- package/skills/kb/references/plan.md +115 -0
- package/skills/kb/references/query.md +231 -0
- package/skills/kb/references/refresh.md +133 -0
- package/skills/kb/references/save-pdf.md +125 -0
- package/skills/kb/references/save-url.md +178 -0
- package/skills/kb/references/url-authentication.md +75 -0
- package/skills/kb/references/url-platforms.md +25 -0
- package/src/agent-context.ts +1241 -0
- package/src/agent-guide-audit.ts +750 -0
- package/src/attachments.ts +723 -0
- package/src/authoring.ts +1725 -0
- package/src/benchmark.ts +362 -0
- package/src/capture.ts +68 -0
- package/src/cli.ts +3660 -0
- package/src/clip/acquire.ts +1273 -0
- package/src/clip/archive-today.ts +402 -0
- package/src/clip/args.ts +423 -0
- package/src/clip/assets.ts +262 -0
- package/src/clip/bounded-byte-buffer.ts +75 -0
- package/src/clip/browser-profiles.ts +372 -0
- package/src/clip/bundle-reader.ts +608 -0
- package/src/clip/capture.ts +1054 -0
- package/src/clip/cli.ts +181 -0
- package/src/clip/cookies.ts +512 -0
- package/src/clip/doctor.ts +1110 -0
- package/src/clip/extract.ts +868 -0
- package/src/clip/jobs.ts +959 -0
- package/src/clip/lib.ts +861 -0
- package/src/clip/media.ts +1474 -0
- package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
- package/src/clip/metadata-search-tool/Cargo.toml +18 -0
- package/src/clip/metadata-search-tool/runner.ts +685 -0
- package/src/clip/metadata-search-tool/src/main.rs +389 -0
- package/src/clip/metadata-search.ts +687 -0
- package/src/clip/network-proxy.ts +627 -0
- package/src/clip/network.ts +950 -0
- package/src/clip/package-root.ts +54 -0
- package/src/clip/persist.ts +1472 -0
- package/src/clip/platforms.ts +1824 -0
- package/src/clip/refresh.ts +197 -0
- package/src/clip/structured.ts +432 -0
- package/src/clip/terminal.ts +197 -0
- package/src/clip/url-intelligence.ts +581 -0
- package/src/clip/url-metadata-backfill.ts +616 -0
- package/src/clip/url-metadata-cli.ts +251 -0
- package/src/clip/url-metadata.ts +1397 -0
- package/src/evaluation-analysis-v2.ts +1440 -0
- package/src/evaluation-builder.ts +2539 -0
- package/src/evaluation-corpus-authoring.ts +2052 -0
- package/src/evaluation-evidence.ts +1147 -0
- package/src/evaluation-execution-v2.ts +426 -0
- package/src/evaluation-implementation.ts +131 -0
- package/src/evaluation-kb-closure.ts +1430 -0
- package/src/evaluation-kb-runner-v2.ts +735 -0
- package/src/evaluation-kb-v2.ts +2567 -0
- package/src/evaluation-kb.ts +1018 -0
- package/src/evaluation-measurement-v2.ts +1462 -0
- package/src/evaluation-packing-v2.ts +452 -0
- package/src/evaluation-redaction.ts +28 -0
- package/src/evaluation-v2.ts +3857 -0
- package/src/evaluation.ts +1220 -0
- package/src/git.ts +1276 -0
- package/src/graph.ts +1487 -0
- package/src/index.ts +20 -0
- package/src/init.ts +123 -0
- package/src/navigation.ts +277 -0
- package/src/note-lock.ts +529 -0
- package/src/pdf/args.ts +202 -0
- package/src/pdf/capture.ts +298 -0
- package/src/pdf/cli.ts +159 -0
- package/src/pdf/extract.ts +303 -0
- package/src/pdf/layout.ts +618 -0
- package/src/pdf/markdown.ts +154 -0
- package/src/pdf/model.ts +234 -0
- package/src/pdf/ocr.ts +190 -0
- package/src/pdf/persist.ts +314 -0
- package/src/pdf/source.ts +135 -0
- package/src/pdf/tools.ts +146 -0
- package/src/pdf.ts +92 -0
- package/src/percolate.ts +877 -0
- package/src/portfolio-audit.ts +574 -0
- package/src/portfolio-identity.ts +162 -0
- package/src/portfolio-registry.ts +473 -0
- package/src/portfolio.ts +765 -0
- package/src/query.ts +646 -0
- package/src/repository-memory.ts +1150 -0
- package/src/sdk.ts +1452 -0
- package/src/search-rules.ts +712 -0
- package/src/search.ts +710 -0
- package/src/semantic-runtime.ts +1139 -0
- package/src/semantic.ts +2480 -0
- package/src/source-inbox.ts +232 -0
- package/src/untrusted-content.ts +401 -0
- package/src/url-intelligence.ts +104 -0
- package/src/vault.ts +636 -0
- package/src/workflow.ts +653 -0
- package/src/workflows/decision-context.ts +81 -0
- package/src/workflows/explain-change.ts +73 -0
- package/src/workflows/index.ts +3 -0
- package/src/workflows/plan-radar.ts +101 -0
|
@@ -0,0 +1,1147 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const EVALUATION_EVIDENCE_SCHEMA_VERSION = 1;
|
|
4
|
+
export const EVALUATION_EVIDENCE_PARSER_VERSION = "evaluation-evidence-v1";
|
|
5
|
+
export const MAX_EVALUATION_EVIDENCE_DOCUMENTS = 10_000;
|
|
6
|
+
export const MAX_EVALUATION_EVIDENCE_DOCUMENT_BYTES = 8 * 1_024 * 1_024;
|
|
7
|
+
export const MAX_EVALUATION_EVIDENCE_TOTAL_BYTES = 64 * 1_024 * 1_024;
|
|
8
|
+
export const MAX_EVALUATION_EVIDENCE_LINES_PER_DOCUMENT = 200_000;
|
|
9
|
+
export const MAX_EVALUATION_EVIDENCE_LINE_BYTES = 1 * 1_024 * 1_024;
|
|
10
|
+
export const MAX_EVALUATION_EVIDENCE_UNIT_BYTES = 1 * 1_024 * 1_024;
|
|
11
|
+
export const MAX_EVALUATION_EVIDENCE_UNITS_PER_DOCUMENT = 100_000;
|
|
12
|
+
export const MAX_EVALUATION_EVIDENCE_TOTAL_UNITS = 100_000;
|
|
13
|
+
export const MAX_EVALUATION_EVIDENCE_LIST_ITEMS_PER_UNIT = 128;
|
|
14
|
+
export const MAX_EVALUATION_EVIDENCE_TABLE_LINES_PER_UNIT = 258;
|
|
15
|
+
export const MAX_EVALUATION_EVIDENCE_NEIGHBORS = 256;
|
|
16
|
+
export const MAX_EVALUATION_EVIDENCE_NEIGHBORHOOD_BYTES = 1 * 1_024 * 1_024;
|
|
17
|
+
|
|
18
|
+
const WINDOWS_ABSOLUTE_PATH = /^[a-z]:[\\/]/iu;
|
|
19
|
+
const PARSER_VERSION = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
|
|
20
|
+
const UNIT_ID = /^eeu:[a-z0-9][a-z0-9._-]{0,63}:[0-9a-f]{64}$/u;
|
|
21
|
+
const PDF_PAGE_MARKER = /^\s*<!--\s*pdf-page:\s*([1-9][0-9]{0,8})\s*-->\s*$/u;
|
|
22
|
+
|
|
23
|
+
export type EvaluationEvidenceTrustClass = string;
|
|
24
|
+
|
|
25
|
+
export type EvaluationEvidenceDocument = {
|
|
26
|
+
readonly documentId: string;
|
|
27
|
+
readonly sourcePath: string;
|
|
28
|
+
readonly markdown: string;
|
|
29
|
+
readonly trustClass: EvaluationEvidenceTrustClass;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type EvaluationEvidenceDocumentSnapshot = EvaluationEvidenceDocument & {
|
|
33
|
+
readonly byteLength: number;
|
|
34
|
+
readonly sourceSha256: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type EvaluationEvidenceUnitKind =
|
|
38
|
+
| "frontmatter-field"
|
|
39
|
+
| "heading"
|
|
40
|
+
| "paragraph"
|
|
41
|
+
| "list"
|
|
42
|
+
| "table"
|
|
43
|
+
| "code-block"
|
|
44
|
+
| "pdf-page-span";
|
|
45
|
+
|
|
46
|
+
export type EvaluationEvidenceByteRange = {
|
|
47
|
+
/** Inclusive UTF-8 byte offset. */
|
|
48
|
+
readonly start: number;
|
|
49
|
+
/** Exclusive UTF-8 byte offset. */
|
|
50
|
+
readonly end: number;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type EvaluationEvidenceLineRange = {
|
|
54
|
+
/** Inclusive, 1-based source line. */
|
|
55
|
+
readonly start: number;
|
|
56
|
+
/** Inclusive, 1-based source line. */
|
|
57
|
+
readonly end: number;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export type EvaluationEvidenceUnit = {
|
|
61
|
+
readonly id: string;
|
|
62
|
+
readonly parserVersion: string;
|
|
63
|
+
readonly kind: EvaluationEvidenceUnitKind;
|
|
64
|
+
readonly documentId: string;
|
|
65
|
+
readonly sourcePath: string;
|
|
66
|
+
readonly byteRange: EvaluationEvidenceByteRange;
|
|
67
|
+
readonly lineRange: EvaluationEvidenceLineRange;
|
|
68
|
+
readonly headingAncestry: readonly string[];
|
|
69
|
+
readonly pdfPage?: number;
|
|
70
|
+
readonly frontmatterField?: string;
|
|
71
|
+
/** SHA-256 of exactly the UTF-8 bytes covered by byteRange. */
|
|
72
|
+
readonly sha256: string;
|
|
73
|
+
readonly trustClass: EvaluationEvidenceTrustClass;
|
|
74
|
+
/** The exact source slice, including its authored newline bytes. */
|
|
75
|
+
readonly text: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type EvaluationEvidenceRegistry = {
|
|
79
|
+
readonly schemaVersion: 1;
|
|
80
|
+
readonly parserVersion: string;
|
|
81
|
+
readonly documents: readonly EvaluationEvidenceDocumentSnapshot[];
|
|
82
|
+
/** Canonical order: document identity, byte range, then unit kind and ID. */
|
|
83
|
+
readonly units: readonly EvaluationEvidenceUnit[];
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export type EvaluationEvidenceLocator =
|
|
87
|
+
| { readonly documentId: string; readonly unitId: string }
|
|
88
|
+
| { readonly documentId: string; readonly byteOffset: number }
|
|
89
|
+
| { readonly documentId: string; readonly line: number }
|
|
90
|
+
| { readonly documentId: string; readonly pdfPage: number };
|
|
91
|
+
|
|
92
|
+
export type EvaluationEvidenceNeighborRelation =
|
|
93
|
+
| "parent-heading"
|
|
94
|
+
| "adjacent-block"
|
|
95
|
+
| "same-page";
|
|
96
|
+
|
|
97
|
+
export type EvaluationEvidenceNeighbor = {
|
|
98
|
+
readonly relation: EvaluationEvidenceNeighborRelation;
|
|
99
|
+
readonly direction?: "before" | "after";
|
|
100
|
+
readonly unit: EvaluationEvidenceUnit;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export type EvaluationEvidenceNeighborhood = {
|
|
104
|
+
readonly primary: EvaluationEvidenceUnit;
|
|
105
|
+
readonly neighbors: readonly EvaluationEvidenceNeighbor[];
|
|
106
|
+
/** Exact UTF-8 bytes in primary and all returned neighbors. */
|
|
107
|
+
readonly bytesUsed: number;
|
|
108
|
+
readonly maxBytes: number;
|
|
109
|
+
readonly candidateCount: number;
|
|
110
|
+
readonly truncated: boolean;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
type SourceLine = {
|
|
114
|
+
readonly number: number;
|
|
115
|
+
readonly content: string;
|
|
116
|
+
readonly startCharacter: number;
|
|
117
|
+
readonly endCharacter: number;
|
|
118
|
+
readonly startByte: number;
|
|
119
|
+
readonly endByte: number;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
type HeadingEntry = {
|
|
123
|
+
readonly level: number;
|
|
124
|
+
readonly text: string;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
type UnitContext = {
|
|
128
|
+
readonly headingAncestry: readonly string[];
|
|
129
|
+
readonly pdfPage?: number;
|
|
130
|
+
readonly frontmatterField?: string;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
type MutablePageSpan = {
|
|
134
|
+
readonly page: number;
|
|
135
|
+
readonly startLine: number;
|
|
136
|
+
readonly headingAncestry: readonly string[];
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
type NeighborCandidate = {
|
|
140
|
+
readonly relation: EvaluationEvidenceNeighborRelation;
|
|
141
|
+
readonly direction?: "before" | "after";
|
|
142
|
+
readonly unit: EvaluationEvidenceUnit;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const KIND_ORDER: Readonly<Record<EvaluationEvidenceUnitKind, number>> = Object.freeze({
|
|
146
|
+
"frontmatter-field": 0,
|
|
147
|
+
heading: 1,
|
|
148
|
+
paragraph: 2,
|
|
149
|
+
list: 3,
|
|
150
|
+
table: 4,
|
|
151
|
+
"code-block": 5,
|
|
152
|
+
"pdf-page-span": 6,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
function compareText(left: string, right: string): number {
|
|
156
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function hasUnpairedSurrogate(value: string): boolean {
|
|
160
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
161
|
+
const code = value.charCodeAt(index);
|
|
162
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
163
|
+
const next = value.charCodeAt(index + 1);
|
|
164
|
+
if (next < 0xdc00 || next > 0xdfff) return true;
|
|
165
|
+
index += 1;
|
|
166
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function boundedSingleLine(value: unknown, label: string, maximumBytes: number): string {
|
|
174
|
+
if (
|
|
175
|
+
typeof value !== "string"
|
|
176
|
+
|| value === ""
|
|
177
|
+
|| value.trim() !== value
|
|
178
|
+
|| /[\0\r\n]/u.test(value)
|
|
179
|
+
|| hasUnpairedSurrogate(value)
|
|
180
|
+
|| value.normalize("NFC") !== value
|
|
181
|
+
|| Buffer.byteLength(value, "utf8") > maximumBytes
|
|
182
|
+
) {
|
|
183
|
+
throw new TypeError(
|
|
184
|
+
`${label} must be a non-empty NFC single-line string of at most ${maximumBytes} UTF-8 bytes.`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
return value;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function parserVersion(value: unknown): string {
|
|
191
|
+
const parsed = boundedSingleLine(value, "parserVersion", 64);
|
|
192
|
+
if (!PARSER_VERSION.test(parsed)) {
|
|
193
|
+
throw new TypeError("parserVersion must be a lowercase version token.");
|
|
194
|
+
}
|
|
195
|
+
return parsed;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function confinedSourcePath(value: unknown): string {
|
|
199
|
+
if (typeof value !== "string" || /[\0\r\n]/u.test(value) || hasUnpairedSurrogate(value)) {
|
|
200
|
+
throw new TypeError("sourcePath must be a confined repository-relative path.");
|
|
201
|
+
}
|
|
202
|
+
const path = value.replaceAll("\\", "/").replace(/^\.\//u, "");
|
|
203
|
+
if (
|
|
204
|
+
path === ""
|
|
205
|
+
|| path.normalize("NFC") !== path
|
|
206
|
+
|| path.startsWith("/")
|
|
207
|
+
|| WINDOWS_ABSOLUTE_PATH.test(path)
|
|
208
|
+
|| Buffer.byteLength(path, "utf8") > 4_096
|
|
209
|
+
|| path.split("/").some((part) => part === "" || part === "." || part === "..")
|
|
210
|
+
) {
|
|
211
|
+
throw new TypeError("sourcePath must be a confined repository-relative path.");
|
|
212
|
+
}
|
|
213
|
+
return path;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function sha256(value: string | Uint8Array): string {
|
|
217
|
+
return createHash("sha256").update(value).digest("hex");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function unitId(input: {
|
|
221
|
+
readonly parserVersion: string;
|
|
222
|
+
readonly documentId: string;
|
|
223
|
+
readonly kind: EvaluationEvidenceUnitKind;
|
|
224
|
+
readonly byteStart: number;
|
|
225
|
+
readonly byteEnd: number;
|
|
226
|
+
readonly sliceSha256: string;
|
|
227
|
+
}): string {
|
|
228
|
+
const digest = createHash("sha256");
|
|
229
|
+
digest.update("evaluation-evidence-unit\0", "utf8");
|
|
230
|
+
for (const field of [
|
|
231
|
+
input.parserVersion,
|
|
232
|
+
input.documentId,
|
|
233
|
+
input.kind,
|
|
234
|
+
String(input.byteStart),
|
|
235
|
+
String(input.byteEnd),
|
|
236
|
+
input.sliceSha256,
|
|
237
|
+
]) {
|
|
238
|
+
const bytes = Buffer.from(field, "utf8");
|
|
239
|
+
digest.update(String(bytes.byteLength), "utf8");
|
|
240
|
+
digest.update(":", "utf8");
|
|
241
|
+
digest.update(bytes);
|
|
242
|
+
digest.update("\0", "utf8");
|
|
243
|
+
}
|
|
244
|
+
return `eeu:${input.parserVersion}:${digest.digest("hex")}`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function sourceLines(markdown: string, sourcePath: string): readonly SourceLine[] {
|
|
248
|
+
const lines: SourceLine[] = [];
|
|
249
|
+
let startCharacter = 0;
|
|
250
|
+
let startByte = 0;
|
|
251
|
+
|
|
252
|
+
while (startCharacter < markdown.length) {
|
|
253
|
+
let contentEnd = startCharacter;
|
|
254
|
+
while (
|
|
255
|
+
contentEnd < markdown.length
|
|
256
|
+
&& markdown[contentEnd] !== "\n"
|
|
257
|
+
&& markdown[contentEnd] !== "\r"
|
|
258
|
+
) contentEnd += 1;
|
|
259
|
+
|
|
260
|
+
let endCharacter = contentEnd;
|
|
261
|
+
if (markdown[endCharacter] === "\r" && markdown[endCharacter + 1] === "\n") {
|
|
262
|
+
endCharacter += 2;
|
|
263
|
+
} else if (markdown[endCharacter] === "\r" || markdown[endCharacter] === "\n") {
|
|
264
|
+
endCharacter += 1;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const raw = markdown.slice(startCharacter, endCharacter);
|
|
268
|
+
const byteLength = Buffer.byteLength(raw, "utf8");
|
|
269
|
+
if (byteLength > MAX_EVALUATION_EVIDENCE_LINE_BYTES) {
|
|
270
|
+
throw new RangeError(
|
|
271
|
+
`${sourcePath} has a line larger than ${MAX_EVALUATION_EVIDENCE_LINE_BYTES} UTF-8 bytes.`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
lines.push(Object.freeze({
|
|
275
|
+
number: lines.length + 1,
|
|
276
|
+
content: markdown.slice(startCharacter, contentEnd),
|
|
277
|
+
startCharacter,
|
|
278
|
+
endCharacter,
|
|
279
|
+
startByte,
|
|
280
|
+
endByte: startByte + byteLength,
|
|
281
|
+
}));
|
|
282
|
+
if (lines.length > MAX_EVALUATION_EVIDENCE_LINES_PER_DOCUMENT) {
|
|
283
|
+
throw new RangeError(
|
|
284
|
+
`${sourcePath} has more than ${MAX_EVALUATION_EVIDENCE_LINES_PER_DOCUMENT} lines.`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
startCharacter = endCharacter;
|
|
288
|
+
startByte += byteLength;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return Object.freeze(lines);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function frontmatterBounds(lines: readonly SourceLine[]): { readonly close: number; readonly body: number } | undefined {
|
|
295
|
+
const first = lines[0]?.content.replace(/^\uFEFF/u, "");
|
|
296
|
+
if (first !== "---") return undefined;
|
|
297
|
+
for (let index = 1; index < lines.length; index += 1) {
|
|
298
|
+
if (/^(?:---|\.\.\.)[ \t]*$/u.test(lines[index]?.content ?? "")) {
|
|
299
|
+
return Object.freeze({ close: index, body: index + 1 });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// A missing delimiter is ordinary malformed Markdown, not synthesized frontmatter.
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function frontmatterField(line: string): string | undefined {
|
|
307
|
+
const match = /^([^ \t#][^:]{0,255}?):(?:[ \t]|$)/u.exec(line);
|
|
308
|
+
const field = match?.[1]?.trim();
|
|
309
|
+
return field === undefined || field === "" ? undefined : field;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function atxHeading(line: string): { readonly level: number; readonly text: string } | undefined {
|
|
313
|
+
const match = /^ {0,3}(#{1,6})(?:[ \t]+(.*)|[ \t]*)$/u.exec(line);
|
|
314
|
+
const hashes = match?.[1];
|
|
315
|
+
if (hashes === undefined) return undefined;
|
|
316
|
+
const text = (match?.[2] ?? "")
|
|
317
|
+
.replace(/[ \t]+#+[ \t]*$/u, "")
|
|
318
|
+
.trim();
|
|
319
|
+
return Object.freeze({ level: hashes.length, text });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function setextLevel(line: string): 1 | 2 | undefined {
|
|
323
|
+
if (/^ {0,3}=+[ \t]*$/u.test(line)) return 1;
|
|
324
|
+
if (/^ {0,3}-+[ \t]*$/u.test(line)) return 2;
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function thematicBreak(line: string): boolean {
|
|
329
|
+
const compact = line.replace(/[ \t]/gu, "");
|
|
330
|
+
return /^(?:\*{3,}|-{3,}|_{3,})$/u.test(compact);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function fenceOpening(line: string): { readonly marker: "`" | "~"; readonly length: number } | undefined {
|
|
334
|
+
const match = /^ {0,3}(`{3,}|~{3,})[^\r\n]*$/u.exec(line);
|
|
335
|
+
const fence = match?.[1];
|
|
336
|
+
if (fence === undefined) return undefined;
|
|
337
|
+
return Object.freeze({ marker: fence[0] as "`" | "~", length: fence.length });
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function closesFence(
|
|
341
|
+
line: string,
|
|
342
|
+
opening: { readonly marker: "`" | "~"; readonly length: number },
|
|
343
|
+
): boolean {
|
|
344
|
+
const match = /^ {0,3}(`+|~+)[ \t]*$/u.exec(line);
|
|
345
|
+
const fence = match?.[1];
|
|
346
|
+
return fence !== undefined
|
|
347
|
+
&& fence[0] === opening.marker
|
|
348
|
+
&& fence.length >= opening.length;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function listItem(line: string): boolean {
|
|
352
|
+
return /^ {0,3}(?:[-+*]|[0-9]{1,9}[.)])(?:[ \t]+|$)/u.test(line);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function indented(line: string): boolean {
|
|
356
|
+
return /^(?: {2,}|\t)/u.test(line);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function indentedCode(line: string): boolean {
|
|
360
|
+
return /^(?: {4}|\t)/u.test(line);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function unescapedPipeCount(line: string): number {
|
|
364
|
+
let count = 0;
|
|
365
|
+
let slashRun = 0;
|
|
366
|
+
for (const character of line) {
|
|
367
|
+
if (character === "\\") {
|
|
368
|
+
slashRun += 1;
|
|
369
|
+
} else {
|
|
370
|
+
if (character === "|" && slashRun % 2 === 0) count += 1;
|
|
371
|
+
slashRun = 0;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return count;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function tableDelimiter(line: string): boolean {
|
|
378
|
+
let content = line.trim();
|
|
379
|
+
if (content.startsWith("|")) content = content.slice(1);
|
|
380
|
+
if (content.endsWith("|")) content = content.slice(0, -1);
|
|
381
|
+
const cells = content.split(/(?<!\\)\|/u).map((cell) => cell.trim());
|
|
382
|
+
return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/u.test(cell));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function startsTable(lines: readonly SourceLine[], index: number): boolean {
|
|
386
|
+
const header = lines[index]?.content;
|
|
387
|
+
const delimiter = lines[index + 1]?.content;
|
|
388
|
+
return header !== undefined
|
|
389
|
+
&& delimiter !== undefined
|
|
390
|
+
&& unescapedPipeCount(header) > 0
|
|
391
|
+
&& tableDelimiter(delimiter);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function pdfPage(line: string): number | undefined {
|
|
395
|
+
const value = PDF_PAGE_MARKER.exec(line)?.[1];
|
|
396
|
+
if (value === undefined) return undefined;
|
|
397
|
+
const page = Number(value);
|
|
398
|
+
return Number.isSafeInteger(page) ? page : undefined;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function headingPath(stack: readonly HeadingEntry[]): readonly string[] {
|
|
402
|
+
return Object.freeze(stack.map((entry) => entry.text));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function updateHeadingStack(
|
|
406
|
+
stack: HeadingEntry[],
|
|
407
|
+
heading: { readonly level: number; readonly text: string },
|
|
408
|
+
): void {
|
|
409
|
+
while ((stack.at(-1)?.level ?? 0) >= heading.level) stack.pop();
|
|
410
|
+
stack.push(Object.freeze({ level: heading.level, text: heading.text }));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function compareUnits(left: EvaluationEvidenceUnit, right: EvaluationEvidenceUnit): number {
|
|
414
|
+
return compareText(left.documentId, right.documentId)
|
|
415
|
+
|| left.byteRange.start - right.byteRange.start
|
|
416
|
+
|| left.byteRange.end - right.byteRange.end
|
|
417
|
+
|| KIND_ORDER[left.kind] - KIND_ORDER[right.kind]
|
|
418
|
+
|| compareText(left.id, right.id);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function freezeUnit(input: {
|
|
422
|
+
readonly id: string;
|
|
423
|
+
readonly parserVersion: string;
|
|
424
|
+
readonly kind: EvaluationEvidenceUnitKind;
|
|
425
|
+
readonly documentId: string;
|
|
426
|
+
readonly sourcePath: string;
|
|
427
|
+
readonly byteStart: number;
|
|
428
|
+
readonly byteEnd: number;
|
|
429
|
+
readonly lineStart: number;
|
|
430
|
+
readonly lineEnd: number;
|
|
431
|
+
readonly headingAncestry: readonly string[];
|
|
432
|
+
readonly pdfPage?: number;
|
|
433
|
+
readonly frontmatterField?: string;
|
|
434
|
+
readonly sha256: string;
|
|
435
|
+
readonly trustClass: string;
|
|
436
|
+
readonly text: string;
|
|
437
|
+
}): EvaluationEvidenceUnit {
|
|
438
|
+
return Object.freeze({
|
|
439
|
+
id: input.id,
|
|
440
|
+
parserVersion: input.parserVersion,
|
|
441
|
+
kind: input.kind,
|
|
442
|
+
documentId: input.documentId,
|
|
443
|
+
sourcePath: input.sourcePath,
|
|
444
|
+
byteRange: Object.freeze({ start: input.byteStart, end: input.byteEnd }),
|
|
445
|
+
lineRange: Object.freeze({ start: input.lineStart, end: input.lineEnd }),
|
|
446
|
+
headingAncestry: Object.freeze([...input.headingAncestry]),
|
|
447
|
+
...(input.pdfPage === undefined ? {} : { pdfPage: input.pdfPage }),
|
|
448
|
+
...(input.frontmatterField === undefined ? {} : { frontmatterField: input.frontmatterField }),
|
|
449
|
+
sha256: input.sha256,
|
|
450
|
+
trustClass: input.trustClass,
|
|
451
|
+
text: input.text,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function parseDocument(
|
|
456
|
+
document: EvaluationEvidenceDocumentSnapshot,
|
|
457
|
+
analysisVersion: string,
|
|
458
|
+
): readonly EvaluationEvidenceUnit[] {
|
|
459
|
+
const lines = sourceLines(document.markdown, document.sourcePath);
|
|
460
|
+
const units: EvaluationEvidenceUnit[] = [];
|
|
461
|
+
|
|
462
|
+
const addExactSpan = (
|
|
463
|
+
kind: EvaluationEvidenceUnitKind,
|
|
464
|
+
startLine: number,
|
|
465
|
+
endLine: number,
|
|
466
|
+
context: UnitContext,
|
|
467
|
+
): void => {
|
|
468
|
+
if (startLine < 0 || endLine <= startLine || endLine > lines.length) {
|
|
469
|
+
throw new RangeError(`Parser produced an invalid source range for ${document.sourcePath}.`);
|
|
470
|
+
}
|
|
471
|
+
const first = lines[startLine];
|
|
472
|
+
const last = lines[endLine - 1];
|
|
473
|
+
if (first === undefined || last === undefined) {
|
|
474
|
+
throw new RangeError(`Parser produced an unresolved source range for ${document.sourcePath}.`);
|
|
475
|
+
}
|
|
476
|
+
const text = document.markdown.slice(first.startCharacter, last.endCharacter);
|
|
477
|
+
const byteLength = last.endByte - first.startByte;
|
|
478
|
+
if (byteLength <= 0 || byteLength > MAX_EVALUATION_EVIDENCE_UNIT_BYTES) {
|
|
479
|
+
throw new RangeError(`Parser produced an oversized source unit for ${document.sourcePath}.`);
|
|
480
|
+
}
|
|
481
|
+
if (Buffer.byteLength(text, "utf8") !== byteLength) {
|
|
482
|
+
throw new Error(`Parser lost UTF-8 byte fidelity for ${document.sourcePath}.`);
|
|
483
|
+
}
|
|
484
|
+
const sliceSha256 = sha256(Buffer.from(text, "utf8"));
|
|
485
|
+
units.push(freezeUnit({
|
|
486
|
+
id: unitId({
|
|
487
|
+
parserVersion: analysisVersion,
|
|
488
|
+
documentId: document.documentId,
|
|
489
|
+
kind,
|
|
490
|
+
byteStart: first.startByte,
|
|
491
|
+
byteEnd: last.endByte,
|
|
492
|
+
sliceSha256,
|
|
493
|
+
}),
|
|
494
|
+
parserVersion: analysisVersion,
|
|
495
|
+
kind,
|
|
496
|
+
documentId: document.documentId,
|
|
497
|
+
sourcePath: document.sourcePath,
|
|
498
|
+
byteStart: first.startByte,
|
|
499
|
+
byteEnd: last.endByte,
|
|
500
|
+
lineStart: first.number,
|
|
501
|
+
lineEnd: last.number,
|
|
502
|
+
headingAncestry: context.headingAncestry,
|
|
503
|
+
...(context.pdfPage === undefined ? {} : { pdfPage: context.pdfPage }),
|
|
504
|
+
...(context.frontmatterField === undefined
|
|
505
|
+
? {}
|
|
506
|
+
: { frontmatterField: context.frontmatterField }),
|
|
507
|
+
sha256: sliceSha256,
|
|
508
|
+
trustClass: document.trustClass,
|
|
509
|
+
text,
|
|
510
|
+
}));
|
|
511
|
+
if (units.length > MAX_EVALUATION_EVIDENCE_UNITS_PER_DOCUMENT) {
|
|
512
|
+
throw new RangeError(
|
|
513
|
+
`${document.sourcePath} produced more than ${MAX_EVALUATION_EVIDENCE_UNITS_PER_DOCUMENT} evidence units.`,
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const addBoundedSpan = (
|
|
519
|
+
kind: EvaluationEvidenceUnitKind,
|
|
520
|
+
startLine: number,
|
|
521
|
+
endLine: number,
|
|
522
|
+
context: UnitContext,
|
|
523
|
+
maximumLines = Number.POSITIVE_INFINITY,
|
|
524
|
+
): void => {
|
|
525
|
+
let chunkStart = startLine;
|
|
526
|
+
while (chunkStart < endLine) {
|
|
527
|
+
const first = lines[chunkStart];
|
|
528
|
+
if (first === undefined) {
|
|
529
|
+
throw new RangeError(`Parser produced an unresolved source range for ${document.sourcePath}.`);
|
|
530
|
+
}
|
|
531
|
+
let chunkEnd = chunkStart;
|
|
532
|
+
while (chunkEnd < endLine && chunkEnd - chunkStart < maximumLines) {
|
|
533
|
+
const line = lines[chunkEnd];
|
|
534
|
+
if (line === undefined) break;
|
|
535
|
+
if (line.endByte - first.startByte > MAX_EVALUATION_EVIDENCE_UNIT_BYTES) break;
|
|
536
|
+
chunkEnd += 1;
|
|
537
|
+
}
|
|
538
|
+
if (chunkEnd === chunkStart) {
|
|
539
|
+
throw new RangeError(
|
|
540
|
+
`${document.sourcePath} contains a source line too large for an evidence unit.`,
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
addExactSpan(kind, chunkStart, chunkEnd, context);
|
|
544
|
+
chunkStart = chunkEnd;
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
const frontmatter = frontmatterBounds(lines);
|
|
549
|
+
if (frontmatter !== undefined) {
|
|
550
|
+
let index = 1;
|
|
551
|
+
while (index < frontmatter.close) {
|
|
552
|
+
const field = frontmatterField(lines[index]?.content ?? "");
|
|
553
|
+
if (field === undefined) {
|
|
554
|
+
index += 1;
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
let end = index + 1;
|
|
558
|
+
while (
|
|
559
|
+
end < frontmatter.close
|
|
560
|
+
&& frontmatterField(lines[end]?.content ?? "") === undefined
|
|
561
|
+
) end += 1;
|
|
562
|
+
addBoundedSpan("frontmatter-field", index, end, {
|
|
563
|
+
headingAncestry: Object.freeze([]),
|
|
564
|
+
frontmatterField: field,
|
|
565
|
+
});
|
|
566
|
+
index = end;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const headings: HeadingEntry[] = [];
|
|
571
|
+
let activePage: MutablePageSpan | undefined;
|
|
572
|
+
let index = frontmatter?.body ?? 0;
|
|
573
|
+
|
|
574
|
+
const context = (): UnitContext => Object.freeze({
|
|
575
|
+
headingAncestry: headingPath(headings),
|
|
576
|
+
...(activePage === undefined ? {} : { pdfPage: activePage.page }),
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
const finishPage = (endLine: number): void => {
|
|
580
|
+
if (activePage === undefined || endLine <= activePage.startLine) return;
|
|
581
|
+
addBoundedSpan("pdf-page-span", activePage.startLine, endLine, {
|
|
582
|
+
headingAncestry: activePage.headingAncestry,
|
|
583
|
+
pdfPage: activePage.page,
|
|
584
|
+
});
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
while (index < lines.length) {
|
|
588
|
+
const line = lines[index]?.content ?? "";
|
|
589
|
+
const markerPage = pdfPage(line);
|
|
590
|
+
if (markerPage !== undefined) {
|
|
591
|
+
finishPage(index);
|
|
592
|
+
activePage = Object.freeze({
|
|
593
|
+
page: markerPage,
|
|
594
|
+
startLine: index,
|
|
595
|
+
headingAncestry: headingPath(headings),
|
|
596
|
+
});
|
|
597
|
+
index += 1;
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const opening = fenceOpening(line);
|
|
602
|
+
if (opening !== undefined) {
|
|
603
|
+
let end = index + 1;
|
|
604
|
+
while (end < lines.length && !closesFence(lines[end]?.content ?? "", opening)) end += 1;
|
|
605
|
+
if (end < lines.length) end += 1;
|
|
606
|
+
addBoundedSpan("code-block", index, end, context());
|
|
607
|
+
index = end;
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const heading = atxHeading(line);
|
|
612
|
+
if (heading !== undefined) {
|
|
613
|
+
updateHeadingStack(headings, heading);
|
|
614
|
+
addBoundedSpan("heading", index, index + 1, context());
|
|
615
|
+
index += 1;
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const nextSetextLevel = setextLevel(lines[index + 1]?.content ?? "");
|
|
620
|
+
if (
|
|
621
|
+
line.trim() !== ""
|
|
622
|
+
&& nextSetextLevel !== undefined
|
|
623
|
+
&& !listItem(line)
|
|
624
|
+
&& !indentedCode(line)
|
|
625
|
+
) {
|
|
626
|
+
updateHeadingStack(headings, { level: nextSetextLevel, text: line.trim() });
|
|
627
|
+
addBoundedSpan("heading", index, index + 2, context());
|
|
628
|
+
index += 2;
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (startsTable(lines, index)) {
|
|
633
|
+
let end = index + 2;
|
|
634
|
+
while (
|
|
635
|
+
end < lines.length
|
|
636
|
+
&& (lines[end]?.content.trim() ?? "") !== ""
|
|
637
|
+
&& unescapedPipeCount(lines[end]?.content ?? "") > 0
|
|
638
|
+
) end += 1;
|
|
639
|
+
addBoundedSpan(
|
|
640
|
+
"table",
|
|
641
|
+
index,
|
|
642
|
+
end,
|
|
643
|
+
context(),
|
|
644
|
+
MAX_EVALUATION_EVIDENCE_TABLE_LINES_PER_UNIT,
|
|
645
|
+
);
|
|
646
|
+
index = end;
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (listItem(line)) {
|
|
651
|
+
let end = index + 1;
|
|
652
|
+
while (end < lines.length) {
|
|
653
|
+
const candidate = lines[end]?.content ?? "";
|
|
654
|
+
if (candidate.trim() === "") {
|
|
655
|
+
let next = end + 1;
|
|
656
|
+
while (next < lines.length && (lines[next]?.content.trim() ?? "") === "") next += 1;
|
|
657
|
+
const continuation = lines[next]?.content ?? "";
|
|
658
|
+
if (next < lines.length && (listItem(continuation) || indented(continuation))) {
|
|
659
|
+
end = next;
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
if (!listItem(candidate) && !indented(candidate)) break;
|
|
665
|
+
end += 1;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
let chunkStart = index;
|
|
669
|
+
let itemCount = 0;
|
|
670
|
+
for (let cursor = index; cursor < end; cursor += 1) {
|
|
671
|
+
if (!listItem(lines[cursor]?.content ?? "")) continue;
|
|
672
|
+
if (itemCount === MAX_EVALUATION_EVIDENCE_LIST_ITEMS_PER_UNIT) {
|
|
673
|
+
addBoundedSpan("list", chunkStart, cursor, context());
|
|
674
|
+
chunkStart = cursor;
|
|
675
|
+
itemCount = 0;
|
|
676
|
+
}
|
|
677
|
+
itemCount += 1;
|
|
678
|
+
}
|
|
679
|
+
addBoundedSpan("list", chunkStart, end, context());
|
|
680
|
+
index = end;
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (indentedCode(line)) {
|
|
685
|
+
let end = index + 1;
|
|
686
|
+
let lastCodeLine = end;
|
|
687
|
+
while (end < lines.length) {
|
|
688
|
+
const candidate = lines[end]?.content ?? "";
|
|
689
|
+
if (indentedCode(candidate)) {
|
|
690
|
+
end += 1;
|
|
691
|
+
lastCodeLine = end;
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
if (candidate.trim() === "") {
|
|
695
|
+
end += 1;
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
addBoundedSpan("code-block", index, lastCodeLine, context());
|
|
701
|
+
index = end;
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (line.trim() === "" || thematicBreak(line)) {
|
|
706
|
+
index += 1;
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
let end = index + 1;
|
|
711
|
+
while (end < lines.length) {
|
|
712
|
+
const candidate = lines[end]?.content ?? "";
|
|
713
|
+
const candidateHeading = atxHeading(candidate);
|
|
714
|
+
if (
|
|
715
|
+
candidate.trim() === ""
|
|
716
|
+
|| pdfPage(candidate) !== undefined
|
|
717
|
+
|| fenceOpening(candidate) !== undefined
|
|
718
|
+
|| candidateHeading !== undefined
|
|
719
|
+
|| listItem(candidate)
|
|
720
|
+
|| indentedCode(candidate)
|
|
721
|
+
|| thematicBreak(candidate)
|
|
722
|
+
|| startsTable(lines, end)
|
|
723
|
+
) break;
|
|
724
|
+
end += 1;
|
|
725
|
+
}
|
|
726
|
+
addBoundedSpan("paragraph", index, end, context());
|
|
727
|
+
index = end;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
finishPage(lines.length);
|
|
731
|
+
units.sort(compareUnits);
|
|
732
|
+
|
|
733
|
+
const ids = new Set<string>();
|
|
734
|
+
const ranges = new Set<string>();
|
|
735
|
+
for (const unit of units) {
|
|
736
|
+
if (ids.has(unit.id)) {
|
|
737
|
+
throw new Error(`Parser produced duplicate evidence unit ID ${unit.id}.`);
|
|
738
|
+
}
|
|
739
|
+
ids.add(unit.id);
|
|
740
|
+
const rangeKey = `${unit.kind}:${unit.byteRange.start}:${unit.byteRange.end}`;
|
|
741
|
+
if (ranges.has(rangeKey)) {
|
|
742
|
+
throw new Error(`Parser produced duplicate ${unit.kind} source range in ${document.sourcePath}.`);
|
|
743
|
+
}
|
|
744
|
+
ranges.add(rangeKey);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
return Object.freeze(units);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function documentSnapshot(value: EvaluationEvidenceDocument, index: number): EvaluationEvidenceDocumentSnapshot {
|
|
751
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
752
|
+
throw new TypeError(`documents[${index}] must be an object.`);
|
|
753
|
+
}
|
|
754
|
+
const documentId = boundedSingleLine(value.documentId, `documents[${index}].documentId`, 4_096);
|
|
755
|
+
const sourcePath = confinedSourcePath(value.sourcePath);
|
|
756
|
+
const trustClass = boundedSingleLine(value.trustClass, `documents[${index}].trustClass`, 256);
|
|
757
|
+
if (typeof value.markdown !== "string" || hasUnpairedSurrogate(value.markdown)) {
|
|
758
|
+
throw new TypeError(`documents[${index}].markdown must be a well-formed Unicode string.`);
|
|
759
|
+
}
|
|
760
|
+
if (value.markdown.includes("\0")) {
|
|
761
|
+
throw new TypeError(`documents[${index}].markdown must not contain NUL bytes.`);
|
|
762
|
+
}
|
|
763
|
+
const byteLength = Buffer.byteLength(value.markdown, "utf8");
|
|
764
|
+
if (byteLength > MAX_EVALUATION_EVIDENCE_DOCUMENT_BYTES) {
|
|
765
|
+
throw new RangeError(
|
|
766
|
+
`documents[${index}].markdown exceeds ${MAX_EVALUATION_EVIDENCE_DOCUMENT_BYTES} UTF-8 bytes.`,
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
return Object.freeze({
|
|
770
|
+
documentId,
|
|
771
|
+
sourcePath,
|
|
772
|
+
markdown: value.markdown,
|
|
773
|
+
trustClass,
|
|
774
|
+
byteLength,
|
|
775
|
+
sourceSha256: sha256(Buffer.from(value.markdown, "utf8")),
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
export function buildEvaluationEvidenceRegistry(input: {
|
|
780
|
+
readonly documents: readonly EvaluationEvidenceDocument[];
|
|
781
|
+
readonly parserVersion?: string;
|
|
782
|
+
}): EvaluationEvidenceRegistry {
|
|
783
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
784
|
+
throw new TypeError("Evidence registry input must be an object.");
|
|
785
|
+
}
|
|
786
|
+
if (!Array.isArray(input.documents)) {
|
|
787
|
+
throw new TypeError("documents must be an array.");
|
|
788
|
+
}
|
|
789
|
+
if (input.documents.length > MAX_EVALUATION_EVIDENCE_DOCUMENTS) {
|
|
790
|
+
throw new RangeError(`documents must contain at most ${MAX_EVALUATION_EVIDENCE_DOCUMENTS} entries.`);
|
|
791
|
+
}
|
|
792
|
+
const analysisVersion = parserVersion(
|
|
793
|
+
input.parserVersion ?? EVALUATION_EVIDENCE_PARSER_VERSION,
|
|
794
|
+
);
|
|
795
|
+
const documents = input.documents.map(documentSnapshot).toSorted((left, right) => (
|
|
796
|
+
compareText(left.documentId, right.documentId)
|
|
797
|
+
|| compareText(left.sourcePath, right.sourcePath)
|
|
798
|
+
));
|
|
799
|
+
const documentIds = new Set<string>();
|
|
800
|
+
const sourcePaths = new Set<string>();
|
|
801
|
+
let totalBytes = 0;
|
|
802
|
+
for (const document of documents) {
|
|
803
|
+
if (documentIds.has(document.documentId)) {
|
|
804
|
+
throw new TypeError(`documents contains duplicate documentId ${document.documentId}.`);
|
|
805
|
+
}
|
|
806
|
+
if (sourcePaths.has(document.sourcePath)) {
|
|
807
|
+
throw new TypeError(`documents contains duplicate sourcePath ${document.sourcePath}.`);
|
|
808
|
+
}
|
|
809
|
+
documentIds.add(document.documentId);
|
|
810
|
+
sourcePaths.add(document.sourcePath);
|
|
811
|
+
totalBytes += document.byteLength;
|
|
812
|
+
if (totalBytes > MAX_EVALUATION_EVIDENCE_TOTAL_BYTES) {
|
|
813
|
+
throw new RangeError(
|
|
814
|
+
`documents exceed ${MAX_EVALUATION_EVIDENCE_TOTAL_BYTES} total UTF-8 bytes.`,
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const frozenDocuments = Object.freeze(documents);
|
|
820
|
+
const parsedUnits: EvaluationEvidenceUnit[] = [];
|
|
821
|
+
for (const document of frozenDocuments) {
|
|
822
|
+
const documentUnits = parseDocument(document, analysisVersion);
|
|
823
|
+
if (parsedUnits.length + documentUnits.length > MAX_EVALUATION_EVIDENCE_TOTAL_UNITS) {
|
|
824
|
+
throw new RangeError(
|
|
825
|
+
`documents produce more than ${MAX_EVALUATION_EVIDENCE_TOTAL_UNITS} total evidence units.`,
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
parsedUnits.push(...documentUnits);
|
|
829
|
+
}
|
|
830
|
+
const units = Object.freeze(parsedUnits.toSorted(compareUnits));
|
|
831
|
+
return Object.freeze({
|
|
832
|
+
schemaVersion: EVALUATION_EVIDENCE_SCHEMA_VERSION,
|
|
833
|
+
parserVersion: analysisVersion,
|
|
834
|
+
documents: frozenDocuments,
|
|
835
|
+
units,
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function canonicalValue(
|
|
840
|
+
value: unknown,
|
|
841
|
+
seen: Set<object>,
|
|
842
|
+
): string {
|
|
843
|
+
if (value === null) return "null";
|
|
844
|
+
if (typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
|
|
845
|
+
if (typeof value === "number") {
|
|
846
|
+
if (!Number.isFinite(value)) throw new TypeError("Registry contains a non-finite number.");
|
|
847
|
+
return JSON.stringify(value);
|
|
848
|
+
}
|
|
849
|
+
if (typeof value !== "object") throw new TypeError("Registry contains an unsupported value.");
|
|
850
|
+
if (seen.has(value)) throw new TypeError("Registry must not contain cycles.");
|
|
851
|
+
seen.add(value);
|
|
852
|
+
let result: string;
|
|
853
|
+
if (Array.isArray(value)) {
|
|
854
|
+
result = `[${value.map((entry) => canonicalValue(entry, seen)).join(",")}]`;
|
|
855
|
+
} else {
|
|
856
|
+
const record = value as Readonly<Record<string, unknown>>;
|
|
857
|
+
result = `{${Object.keys(record).toSorted().map((key) => (
|
|
858
|
+
`${JSON.stringify(key)}:${canonicalValue(record[key], seen)}`
|
|
859
|
+
)).join(",")}}`;
|
|
860
|
+
}
|
|
861
|
+
seen.delete(value);
|
|
862
|
+
return result;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function canonicalRegistry(value: unknown): string {
|
|
866
|
+
return canonicalValue(value, new Set());
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
export function validateEvaluationEvidenceRegistry(
|
|
870
|
+
registry: unknown,
|
|
871
|
+
options: {
|
|
872
|
+
readonly documents?: readonly EvaluationEvidenceDocument[];
|
|
873
|
+
readonly parserVersion?: string;
|
|
874
|
+
} = {},
|
|
875
|
+
): asserts registry is EvaluationEvidenceRegistry {
|
|
876
|
+
if (registry === null || typeof registry !== "object" || Array.isArray(registry)) {
|
|
877
|
+
throw new TypeError("Evaluation evidence registry must be an object.");
|
|
878
|
+
}
|
|
879
|
+
const candidate = registry as Partial<EvaluationEvidenceRegistry>;
|
|
880
|
+
const expectedVersion = parserVersion(
|
|
881
|
+
options.parserVersion ?? EVALUATION_EVIDENCE_PARSER_VERSION,
|
|
882
|
+
);
|
|
883
|
+
if (candidate.parserVersion !== expectedVersion) {
|
|
884
|
+
throw new Error(
|
|
885
|
+
`Evaluation evidence parser version drift: expected ${expectedVersion}, received ${String(candidate.parserVersion)}.`,
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
const candidateDocuments: unknown = candidate.documents;
|
|
889
|
+
const candidateUnits: unknown = candidate.units;
|
|
890
|
+
if (!Array.isArray(candidateDocuments) || !Array.isArray(candidateUnits)) {
|
|
891
|
+
throw new TypeError("Evaluation evidence registry must contain document and unit arrays.");
|
|
892
|
+
}
|
|
893
|
+
const declaredDocuments = candidate.documents;
|
|
894
|
+
if (declaredDocuments === undefined) {
|
|
895
|
+
throw new TypeError("Evaluation evidence registry must contain document snapshots.");
|
|
896
|
+
}
|
|
897
|
+
const sourceDocuments = options.documents ?? declaredDocuments.map((document) => ({
|
|
898
|
+
documentId: document.documentId,
|
|
899
|
+
sourcePath: document.sourcePath,
|
|
900
|
+
markdown: document.markdown,
|
|
901
|
+
trustClass: document.trustClass,
|
|
902
|
+
}));
|
|
903
|
+
const rebuilt = buildEvaluationEvidenceRegistry({
|
|
904
|
+
documents: sourceDocuments,
|
|
905
|
+
parserVersion: expectedVersion,
|
|
906
|
+
});
|
|
907
|
+
if (canonicalRegistry(candidate) !== canonicalRegistry(rebuilt)) {
|
|
908
|
+
throw new Error(
|
|
909
|
+
"Evaluation evidence registry validation failed: source bytes, paths, trust, ranges, hashes, or IDs drifted.",
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function integerInRange(value: unknown, label: string, minimum: number, maximum: number): number {
|
|
915
|
+
if (!Number.isSafeInteger(value) || (value as number) < minimum || (value as number) > maximum) {
|
|
916
|
+
throw new RangeError(`${label} must be an integer from ${minimum} through ${maximum}.`);
|
|
917
|
+
}
|
|
918
|
+
return value as number;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function usefulUnitOrder(left: EvaluationEvidenceUnit, right: EvaluationEvidenceUnit): number {
|
|
922
|
+
const leftPage = left.kind === "pdf-page-span" ? 1 : 0;
|
|
923
|
+
const rightPage = right.kind === "pdf-page-span" ? 1 : 0;
|
|
924
|
+
return leftPage - rightPage
|
|
925
|
+
|| (left.byteRange.end - left.byteRange.start) - (right.byteRange.end - right.byteRange.start)
|
|
926
|
+
|| compareUnits(left, right);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function nearestUnitByByte(
|
|
930
|
+
units: readonly EvaluationEvidenceUnit[],
|
|
931
|
+
byteOffset: number,
|
|
932
|
+
): EvaluationEvidenceUnit | undefined {
|
|
933
|
+
return units.toSorted((left, right) => {
|
|
934
|
+
const leftDistance = byteOffset < left.byteRange.start
|
|
935
|
+
? left.byteRange.start - byteOffset
|
|
936
|
+
: byteOffset >= left.byteRange.end
|
|
937
|
+
? byteOffset - left.byteRange.end + 1
|
|
938
|
+
: 0;
|
|
939
|
+
const rightDistance = byteOffset < right.byteRange.start
|
|
940
|
+
? right.byteRange.start - byteOffset
|
|
941
|
+
: byteOffset >= right.byteRange.end
|
|
942
|
+
? byteOffset - right.byteRange.end + 1
|
|
943
|
+
: 0;
|
|
944
|
+
return leftDistance - rightDistance || usefulUnitOrder(left, right);
|
|
945
|
+
})[0];
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function nearestUnitByLine(
|
|
949
|
+
units: readonly EvaluationEvidenceUnit[],
|
|
950
|
+
line: number,
|
|
951
|
+
): EvaluationEvidenceUnit | undefined {
|
|
952
|
+
return units.toSorted((left, right) => {
|
|
953
|
+
const leftDistance = line < left.lineRange.start
|
|
954
|
+
? left.lineRange.start - line
|
|
955
|
+
: line > left.lineRange.end
|
|
956
|
+
? line - left.lineRange.end
|
|
957
|
+
: 0;
|
|
958
|
+
const rightDistance = line < right.lineRange.start
|
|
959
|
+
? right.lineRange.start - line
|
|
960
|
+
: line > right.lineRange.end
|
|
961
|
+
? line - right.lineRange.end
|
|
962
|
+
: 0;
|
|
963
|
+
return leftDistance - rightDistance || usefulUnitOrder(left, right);
|
|
964
|
+
})[0];
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function resolvePrimary(
|
|
968
|
+
registry: EvaluationEvidenceRegistry,
|
|
969
|
+
locator: EvaluationEvidenceLocator,
|
|
970
|
+
): EvaluationEvidenceUnit {
|
|
971
|
+
if (locator === null || typeof locator !== "object" || Array.isArray(locator)) {
|
|
972
|
+
throw new TypeError("Evidence locator must be an object.");
|
|
973
|
+
}
|
|
974
|
+
const documentId = boundedSingleLine(locator.documentId, "locator.documentId", 4_096);
|
|
975
|
+
const document = registry.documents.find((entry) => entry.documentId === documentId);
|
|
976
|
+
if (document === undefined) throw new Error(`Unknown evidence document ${documentId}.`);
|
|
977
|
+
const units = registry.units.filter((unit) => unit.documentId === documentId);
|
|
978
|
+
const keys = ["unitId", "byteOffset", "line", "pdfPage"].filter((key) => (
|
|
979
|
+
Object.prototype.hasOwnProperty.call(locator, key)
|
|
980
|
+
));
|
|
981
|
+
if (keys.length !== 1) {
|
|
982
|
+
throw new TypeError("Evidence locator must define exactly one of unitId, byteOffset, line, or pdfPage.");
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if ("unitId" in locator) {
|
|
986
|
+
const id = boundedSingleLine(locator.unitId, "locator.unitId", 256);
|
|
987
|
+
if (!UNIT_ID.test(id)) throw new TypeError("locator.unitId is not an evidence unit ID.");
|
|
988
|
+
const unit = units.find((candidate) => candidate.id === id);
|
|
989
|
+
if (unit === undefined) throw new Error(`Unknown evidence unit ${id} in ${documentId}.`);
|
|
990
|
+
return unit;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
const nonPageUnits = units.filter((unit) => unit.kind !== "pdf-page-span");
|
|
994
|
+
if ("byteOffset" in locator) {
|
|
995
|
+
const byteOffset = integerInRange(
|
|
996
|
+
locator.byteOffset,
|
|
997
|
+
"locator.byteOffset",
|
|
998
|
+
0,
|
|
999
|
+
Math.max(0, document.byteLength - 1),
|
|
1000
|
+
);
|
|
1001
|
+
const containing = nonPageUnits.filter((unit) => (
|
|
1002
|
+
unit.byteRange.start <= byteOffset && byteOffset < unit.byteRange.end
|
|
1003
|
+
)).toSorted(usefulUnitOrder)[0];
|
|
1004
|
+
const unit = containing
|
|
1005
|
+
?? nearestUnitByByte(nonPageUnits, byteOffset)
|
|
1006
|
+
?? nearestUnitByByte(units, byteOffset);
|
|
1007
|
+
if (unit === undefined) throw new Error(`${documentId} has no evidence unit at byte ${byteOffset}.`);
|
|
1008
|
+
return unit;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
if ("line" in locator) {
|
|
1012
|
+
const lineCount = sourceLines(document.markdown, document.sourcePath).length;
|
|
1013
|
+
const line = integerInRange(locator.line, "locator.line", 1, lineCount);
|
|
1014
|
+
const containing = nonPageUnits.filter((unit) => (
|
|
1015
|
+
unit.lineRange.start <= line && line <= unit.lineRange.end
|
|
1016
|
+
)).toSorted(usefulUnitOrder)[0];
|
|
1017
|
+
const unit = containing
|
|
1018
|
+
?? nearestUnitByLine(nonPageUnits, line)
|
|
1019
|
+
?? nearestUnitByLine(units, line);
|
|
1020
|
+
if (unit === undefined) throw new Error(`${documentId} has no evidence unit at line ${line}.`);
|
|
1021
|
+
return unit;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
const page = integerInRange(locator.pdfPage, "locator.pdfPage", 1, 999_999_999);
|
|
1025
|
+
const pageSpans = units.filter((unit) => unit.kind === "pdf-page-span" && unit.pdfPage === page);
|
|
1026
|
+
const unit = pageSpans.toSorted(usefulUnitOrder)[0]
|
|
1027
|
+
?? units.filter((candidate) => candidate.pdfPage === page).toSorted(usefulUnitOrder)[0];
|
|
1028
|
+
if (unit === undefined) throw new Error(`${documentId} has no evidence for PDF page ${page}.`);
|
|
1029
|
+
return unit;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function sameHeadingPath(left: readonly string[], right: readonly string[]): boolean {
|
|
1033
|
+
return left.length === right.length && left.every((entry, index) => entry === right[index]);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function neighborCandidates(
|
|
1037
|
+
registry: EvaluationEvidenceRegistry,
|
|
1038
|
+
primary: EvaluationEvidenceUnit,
|
|
1039
|
+
): readonly NeighborCandidate[] {
|
|
1040
|
+
const documentUnits = registry.units.filter((unit) => unit.documentId === primary.documentId);
|
|
1041
|
+
const blockUnits = documentUnits.filter((unit) => unit.kind !== "pdf-page-span");
|
|
1042
|
+
const candidates: NeighborCandidate[] = [];
|
|
1043
|
+
const seen = new Set([primary.id]);
|
|
1044
|
+
const add = (candidate: NeighborCandidate | undefined): void => {
|
|
1045
|
+
if (candidate === undefined || seen.has(candidate.unit.id)) return;
|
|
1046
|
+
seen.add(candidate.unit.id);
|
|
1047
|
+
candidates.push(Object.freeze({
|
|
1048
|
+
relation: candidate.relation,
|
|
1049
|
+
...(candidate.direction === undefined ? {} : { direction: candidate.direction }),
|
|
1050
|
+
unit: candidate.unit,
|
|
1051
|
+
}));
|
|
1052
|
+
};
|
|
1053
|
+
|
|
1054
|
+
const maximumHeadingDepth = primary.kind === "heading"
|
|
1055
|
+
? primary.headingAncestry.length - 1
|
|
1056
|
+
: primary.headingAncestry.length;
|
|
1057
|
+
for (let depth = maximumHeadingDepth; depth > 0; depth -= 1) {
|
|
1058
|
+
const prefix = primary.headingAncestry.slice(0, depth);
|
|
1059
|
+
for (let index = blockUnits.length - 1; index >= 0; index -= 1) {
|
|
1060
|
+
const unit = blockUnits[index];
|
|
1061
|
+
if (
|
|
1062
|
+
unit !== undefined
|
|
1063
|
+
&& unit.kind === "heading"
|
|
1064
|
+
&& unit.byteRange.start <= primary.byteRange.start
|
|
1065
|
+
&& sameHeadingPath(unit.headingAncestry, prefix)
|
|
1066
|
+
) {
|
|
1067
|
+
add({ relation: "parent-heading", unit });
|
|
1068
|
+
break;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
const primaryBlockIndex = blockUnits.findIndex((unit) => unit.id === primary.id);
|
|
1074
|
+
if (primaryBlockIndex >= 0) {
|
|
1075
|
+
const before = blockUnits[primaryBlockIndex - 1];
|
|
1076
|
+
const after = blockUnits[primaryBlockIndex + 1];
|
|
1077
|
+
if (before !== undefined) add({ relation: "adjacent-block", direction: "before", unit: before });
|
|
1078
|
+
if (after !== undefined) add({ relation: "adjacent-block", direction: "after", unit: after });
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
if (primary.pdfPage !== undefined) {
|
|
1082
|
+
for (const unit of blockUnits) {
|
|
1083
|
+
if (unit.pdfPage === primary.pdfPage) add({ relation: "same-page", unit });
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
return Object.freeze(candidates);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
export function resolveEvaluationEvidenceNeighborhood(
|
|
1090
|
+
registry: EvaluationEvidenceRegistry,
|
|
1091
|
+
locator: EvaluationEvidenceLocator,
|
|
1092
|
+
options: {
|
|
1093
|
+
readonly maxBytes: number;
|
|
1094
|
+
readonly maxNeighbors?: number;
|
|
1095
|
+
},
|
|
1096
|
+
): EvaluationEvidenceNeighborhood {
|
|
1097
|
+
validateEvaluationEvidenceRegistry(registry);
|
|
1098
|
+
if (options === null || typeof options !== "object" || Array.isArray(options)) {
|
|
1099
|
+
throw new TypeError("Evidence neighborhood options must be an object.");
|
|
1100
|
+
}
|
|
1101
|
+
const maxBytes = integerInRange(
|
|
1102
|
+
options.maxBytes,
|
|
1103
|
+
"maxBytes",
|
|
1104
|
+
1,
|
|
1105
|
+
MAX_EVALUATION_EVIDENCE_NEIGHBORHOOD_BYTES,
|
|
1106
|
+
);
|
|
1107
|
+
const maxNeighbors = integerInRange(
|
|
1108
|
+
options.maxNeighbors ?? 32,
|
|
1109
|
+
"maxNeighbors",
|
|
1110
|
+
0,
|
|
1111
|
+
MAX_EVALUATION_EVIDENCE_NEIGHBORS,
|
|
1112
|
+
);
|
|
1113
|
+
const primary = resolvePrimary(registry, locator);
|
|
1114
|
+
const primaryBytes = primary.byteRange.end - primary.byteRange.start;
|
|
1115
|
+
if (primaryBytes > maxBytes) {
|
|
1116
|
+
throw new RangeError(
|
|
1117
|
+
`maxBytes ${maxBytes} cannot fit primary evidence unit ${primary.id} (${primaryBytes} bytes).`,
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
const candidates = neighborCandidates(registry, primary);
|
|
1122
|
+
const neighbors: EvaluationEvidenceNeighbor[] = [];
|
|
1123
|
+
let bytesUsed = primaryBytes;
|
|
1124
|
+
let truncated = false;
|
|
1125
|
+
for (const candidate of candidates) {
|
|
1126
|
+
const bytes = candidate.unit.byteRange.end - candidate.unit.byteRange.start;
|
|
1127
|
+
if (neighbors.length >= maxNeighbors || bytesUsed + bytes > maxBytes) {
|
|
1128
|
+
truncated = true;
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
neighbors.push(Object.freeze({
|
|
1132
|
+
relation: candidate.relation,
|
|
1133
|
+
...(candidate.direction === undefined ? {} : { direction: candidate.direction }),
|
|
1134
|
+
unit: candidate.unit,
|
|
1135
|
+
}));
|
|
1136
|
+
bytesUsed += bytes;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
return Object.freeze({
|
|
1140
|
+
primary,
|
|
1141
|
+
neighbors: Object.freeze(neighbors),
|
|
1142
|
+
bytesUsed,
|
|
1143
|
+
maxBytes,
|
|
1144
|
+
candidateCount: candidates.length,
|
|
1145
|
+
truncated,
|
|
1146
|
+
});
|
|
1147
|
+
}
|