@hawkeyexl/manni 4.0.3 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/dist/{chunk-DEP5AMVA.js → chunk-BNHQWIAE.js} +11 -42
- package/dist/chunk-BNHQWIAE.js.map +1 -0
- package/dist/{chunk-TQG3WCJY.js → chunk-TH565O2N.js} +1450 -750
- package/dist/chunk-TH565O2N.js.map +1 -0
- package/dist/chunk-UYDRDFYF.js +26136 -0
- package/dist/chunk-UYDRDFYF.js.map +1 -0
- package/dist/cli.js +1332 -127
- package/dist/cli.js.map +1 -1
- package/dist/docmeta.js +2 -2
- package/dist/graph/embed.d.ts +154 -0
- package/dist/graph/embed.js +137 -0
- package/dist/graph/embed.js.map +1 -0
- package/dist/graph/runtime.d.ts +751 -0
- package/dist/graph/runtime.js +3067 -0
- package/dist/graph/runtime.js.map +1 -0
- package/dist/index.d.ts +5186 -1339
- package/dist/index.js +526 -6
- package/dist/index.js.map +1 -1
- package/package.json +40 -5
- package/shapes/graph/shapes-1.0.0.ttl +689 -0
- package/dist/chunk-DEP5AMVA.js.map +0 -1
- package/dist/chunk-JYRY7SKD.js +0 -13908
- package/dist/chunk-JYRY7SKD.js.map +0 -1
- package/dist/chunk-TQG3WCJY.js.map +0 -1
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph derivation: DocModel[] → deduplicated Quad[]. This file is the
|
|
3
|
+
* vocabulary mapping — the deterministic rules for what documentation
|
|
4
|
+
* structure becomes which triples.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
type Term = {
|
|
8
|
+
kind: "iri";
|
|
9
|
+
value: string;
|
|
10
|
+
} | {
|
|
11
|
+
kind: "literal";
|
|
12
|
+
value: string;
|
|
13
|
+
datatype?: string;
|
|
14
|
+
};
|
|
15
|
+
interface Quad {
|
|
16
|
+
s: string;
|
|
17
|
+
p: string;
|
|
18
|
+
o: Term;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* `GraphIndex` — the browser-native in-memory graph (ADR 01018).
|
|
23
|
+
*
|
|
24
|
+
* Built from `graph.jsonld` (the Phase 6 export) with **no RDF parser**: that
|
|
25
|
+
* artifact is plain `JSON.parse`-able, deterministically sorted, and free of
|
|
26
|
+
* blank nodes, so a `Map`-based adjacency index is all the runtime needs. Node
|
|
27
|
+
* callers can also build from quads (`fromQuads`) so the CLI can read Turtle.
|
|
28
|
+
*
|
|
29
|
+
* Platform-neutral: no `node:` imports, no npm dependencies, no I/O — the
|
|
30
|
+
* caller supplies the bytes. Type-only imports are erased at build time.
|
|
31
|
+
*
|
|
32
|
+
* Every accessor returns deterministically sorted results, so traversal order
|
|
33
|
+
* (and therefore the trace) is byte-stable for a given graph.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** An object value: an IRI reference or a literal. */
|
|
37
|
+
type Value = {
|
|
38
|
+
kind: "iri";
|
|
39
|
+
value: string;
|
|
40
|
+
} | {
|
|
41
|
+
kind: "literal";
|
|
42
|
+
value: string;
|
|
43
|
+
datatype?: string;
|
|
44
|
+
};
|
|
45
|
+
interface GraphNode {
|
|
46
|
+
id: string;
|
|
47
|
+
/** Full class IRIs, sorted. */
|
|
48
|
+
types: string[];
|
|
49
|
+
/** Full predicate IRI → values, in insertion-normalized sorted order. */
|
|
50
|
+
props: Map<string, Value[]>;
|
|
51
|
+
}
|
|
52
|
+
declare class GraphIndex {
|
|
53
|
+
private readonly nodes;
|
|
54
|
+
/** subject → predicate → object IRIs (sorted). */
|
|
55
|
+
private readonly outbound;
|
|
56
|
+
/** object → predicate → subject IRIs (sorted). */
|
|
57
|
+
private readonly inbound;
|
|
58
|
+
/** class IRI → instance IRIs (sorted). */
|
|
59
|
+
private readonly byType;
|
|
60
|
+
/** prefix → namespace, from the artifact's own `@context`. */
|
|
61
|
+
readonly context: Record<string, string>;
|
|
62
|
+
private constructor();
|
|
63
|
+
/**
|
|
64
|
+
* Build from a `graph.jsonld` document — a JSON string or the parsed object.
|
|
65
|
+
* CURIE keys and `@type` values are expanded using the document's own
|
|
66
|
+
* `@context`, so the index is self-describing.
|
|
67
|
+
*/
|
|
68
|
+
static fromJsonLd(input: string | object): GraphIndex;
|
|
69
|
+
/** Build from dockg's internal quad shape (the Node/CLI path). */
|
|
70
|
+
static fromQuads(quads: Quad[], context?: Record<string, string>): GraphIndex;
|
|
71
|
+
/** Expand a CURIE (`dcterms:title`) to a full IRI using `@context`. */
|
|
72
|
+
expand(term: string): string;
|
|
73
|
+
/** Compact a full IRI to a CURIE when a context namespace matches. */
|
|
74
|
+
compact(iri: string): string;
|
|
75
|
+
private addJsonLdNode;
|
|
76
|
+
private toValue;
|
|
77
|
+
private ensureNode;
|
|
78
|
+
private addTriple;
|
|
79
|
+
/** Sort every collection so traversal order is deterministic. */
|
|
80
|
+
private finalize;
|
|
81
|
+
/** Every node IRI, sorted. */
|
|
82
|
+
ids(): string[];
|
|
83
|
+
size(): number;
|
|
84
|
+
node(iri: string): GraphNode | undefined;
|
|
85
|
+
has(iri: string): boolean;
|
|
86
|
+
/** Class IRIs of a node, sorted. */
|
|
87
|
+
types(iri: string): string[];
|
|
88
|
+
/** Instances of a class, sorted. */
|
|
89
|
+
instancesOf(classIri: string): string[];
|
|
90
|
+
/** All values for a predicate, sorted. */
|
|
91
|
+
values(iri: string, predicate: string): Value[];
|
|
92
|
+
/** Literal lexical forms for a predicate, sorted. */
|
|
93
|
+
literals(iri: string, predicate: string): string[];
|
|
94
|
+
/** The first literal for a predicate, if any. */
|
|
95
|
+
literal(iri: string, predicate: string): string | undefined;
|
|
96
|
+
/** Outbound IRI targets: all predicates, or one. Sorted. */
|
|
97
|
+
out(iri: string, predicate?: string): Array<{
|
|
98
|
+
predicate: string;
|
|
99
|
+
target: string;
|
|
100
|
+
}>;
|
|
101
|
+
/** Inbound IRI sources (who points at this node). Sorted. */
|
|
102
|
+
in(iri: string, predicate?: string): Array<{
|
|
103
|
+
predicate: string;
|
|
104
|
+
target: string;
|
|
105
|
+
}>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Query trace — the explainability contract (ADR 01018). Every runtime stage
|
|
110
|
+
* appends to a trace, and no API returns results without one: retrieval
|
|
111
|
+
* provenance is part of the contract exactly as PROV provenance is part of the
|
|
112
|
+
* build. The trace answers "how and why did this result come back": which nodes
|
|
113
|
+
* were seeded, every hop walked, every node scope excluded (and by which rule),
|
|
114
|
+
* and every content resolution attempted.
|
|
115
|
+
*
|
|
116
|
+
* Events are appended in traversal order, which is itself deterministic, so the
|
|
117
|
+
* trace is byte-stable for a given graph + query.
|
|
118
|
+
*/
|
|
119
|
+
/** How a seed was found. `hybrid` means several legs agreed on it. */
|
|
120
|
+
type EntryVia = "explicit" | "lexical" | "vector" | "hybrid";
|
|
121
|
+
/** A starting node, with the score/reason that made it a seed. */
|
|
122
|
+
interface EntryCandidate {
|
|
123
|
+
iri: string;
|
|
124
|
+
/**
|
|
125
|
+
* Relevance score: 1 for an explicitly requested seed, otherwise the fused
|
|
126
|
+
* `rrfMerge` score. Note that fusion is **rank**-based, so a searched seed's
|
|
127
|
+
* score reflects its position (1/(60 + rank)), not the engine's relevance —
|
|
128
|
+
* it orders candidates but is not a magnitude to threshold on.
|
|
129
|
+
*/
|
|
130
|
+
score: number;
|
|
131
|
+
via: EntryVia;
|
|
132
|
+
}
|
|
133
|
+
/** One edge walked, in either direction. */
|
|
134
|
+
interface Hop {
|
|
135
|
+
from: string;
|
|
136
|
+
predicate: string;
|
|
137
|
+
to: string;
|
|
138
|
+
/** Hops from the nearest seed (seeds are depth 0; their neighbors are 1). */
|
|
139
|
+
depth: number;
|
|
140
|
+
direction: "out" | "in";
|
|
141
|
+
}
|
|
142
|
+
/** A node the scope filter removed, and why. */
|
|
143
|
+
interface ScopeExclusion {
|
|
144
|
+
node: string;
|
|
145
|
+
/**
|
|
146
|
+
* The predicate that disqualified the node: a `graph:not*` IRI when an
|
|
147
|
+
* explicit negative named the target, or the positive predicate (e.g.
|
|
148
|
+
* `iirds:relates-to-product-variant`) when the node declared a scope set that
|
|
149
|
+
* omits the target.
|
|
150
|
+
*/
|
|
151
|
+
rule: string;
|
|
152
|
+
/**
|
|
153
|
+
* The filter target being applied — the variant or subject IRI the query
|
|
154
|
+
* asked for. Under a `graph:not*` rule the node named this explicitly; under
|
|
155
|
+
* a positive rule this is what the node's declared set was missing.
|
|
156
|
+
*/
|
|
157
|
+
value: string;
|
|
158
|
+
}
|
|
159
|
+
/** One content-resolution attempt. */
|
|
160
|
+
interface Resolution {
|
|
161
|
+
iri: string;
|
|
162
|
+
sourceUrl: string;
|
|
163
|
+
ok: boolean;
|
|
164
|
+
/** Present when `ok` is false. */
|
|
165
|
+
error?: string;
|
|
166
|
+
}
|
|
167
|
+
interface QueryTrace {
|
|
168
|
+
entry: EntryCandidate[];
|
|
169
|
+
hops: Hop[];
|
|
170
|
+
exclusions: ScopeExclusion[];
|
|
171
|
+
resolutions: Resolution[];
|
|
172
|
+
}
|
|
173
|
+
declare function createTrace(): QueryTrace;
|
|
174
|
+
/**
|
|
175
|
+
* Every node named anywhere in the trace as reached — seeds plus hop targets.
|
|
176
|
+
* The trace-completeness contract: a traversal's results must be a subset of
|
|
177
|
+
* this, so every returned node is explainable by recorded events.
|
|
178
|
+
*/
|
|
179
|
+
declare function reachedNodes(trace: QueryTrace): Set<string>;
|
|
180
|
+
|
|
181
|
+
type Direction = "out" | "in" | "both";
|
|
182
|
+
interface ScopeFilter {
|
|
183
|
+
/** Product variant: an IRI, a `dcterms:title` (e.g. "SP-X100"), or a slug. */
|
|
184
|
+
variant?: string;
|
|
185
|
+
/** Software subject: an IRI or a `graph.about-product-aspect` value (e.g. "architecture"). */
|
|
186
|
+
subject?: string;
|
|
187
|
+
/**
|
|
188
|
+
* BCP-47 tag (ADR 01037). Matched against `dcterms:language` after both are
|
|
189
|
+
* canonicalized, since tags are case-insensitive. There is no fallback:
|
|
190
|
+
* `de-AT` is not `de`, because relatedness between locales is an inference
|
|
191
|
+
* and this runtime does not infer.
|
|
192
|
+
*/
|
|
193
|
+
language?: string;
|
|
194
|
+
}
|
|
195
|
+
interface TraverseOptions extends ScopeFilter {
|
|
196
|
+
/** Starting nodes (IRIs). */
|
|
197
|
+
seeds: string[];
|
|
198
|
+
/** Maximum hops from a seed. Default 1. */
|
|
199
|
+
depth?: number;
|
|
200
|
+
/** Restrict to these predicates (full IRIs). Default: every predicate. */
|
|
201
|
+
predicates?: string[];
|
|
202
|
+
/** Which edge directions to follow. Default "out". */
|
|
203
|
+
direction?: Direction;
|
|
204
|
+
/** Stop after this many nodes (seeds included). */
|
|
205
|
+
limit?: number;
|
|
206
|
+
/**
|
|
207
|
+
* Follow `rdf:type` edges into class nodes. Default false, deliberately:
|
|
208
|
+
* class nodes are schema, not content, and every document shares them — so
|
|
209
|
+
* traversing them makes every document reachable from every other in two
|
|
210
|
+
* hops (`a → graph:Document → b`). That is exactly the edge contamination
|
|
211
|
+
* graph-governed retrieval exists to avoid. A `predicates` list that names
|
|
212
|
+
* `rdf:type` follows them too, because it asked for them by name.
|
|
213
|
+
*/
|
|
214
|
+
includeTypeEdges?: boolean;
|
|
215
|
+
/** Append to an existing trace instead of starting a new one. */
|
|
216
|
+
trace?: QueryTrace;
|
|
217
|
+
}
|
|
218
|
+
interface TraversedNode {
|
|
219
|
+
iri: string;
|
|
220
|
+
/** Hops from the nearest seed; seeds are 0. */
|
|
221
|
+
depth: number;
|
|
222
|
+
}
|
|
223
|
+
interface TraverseResult {
|
|
224
|
+
nodes: TraversedNode[];
|
|
225
|
+
trace: QueryTrace;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Resolve a variant input to its IRI: an exact node IRI, a `dcterms:title`
|
|
229
|
+
* match on an `iirds:ProductVariant`, or a trailing-segment slug match.
|
|
230
|
+
*/
|
|
231
|
+
declare function resolveVariant(graph: GraphIndex, input: string): string | undefined;
|
|
232
|
+
/** Resolve a software-subject input to its iiRDS IRI. */
|
|
233
|
+
declare function resolveSubject(graph: GraphIndex, input: string): string | undefined;
|
|
234
|
+
/**
|
|
235
|
+
* Decide whether a node survives the scope filter, and if not, why.
|
|
236
|
+
*
|
|
237
|
+
* Rules, in the spirit of ADR 01014 (absence of a positive claim is not a
|
|
238
|
+
* negative claim, so both polarities are explicit):
|
|
239
|
+
* - An explicit negative (`graph:notApplicableToVariant` /
|
|
240
|
+
* `graph:notSoftwareSubject`) naming the target **excludes** the node.
|
|
241
|
+
* - A node that declares a positive set which omits the target is **excluded**
|
|
242
|
+
* — it scoped itself elsewhere — recorded under the positive predicate so
|
|
243
|
+
* the trace distinguishes this from an explicit negative.
|
|
244
|
+
* - A node that makes no claim is **kept**: unscoped content applies broadly.
|
|
245
|
+
*/
|
|
246
|
+
declare function scopeExclusion(graph: GraphIndex, iri: string, scope: {
|
|
247
|
+
variantIri?: string;
|
|
248
|
+
subjectIri?: string;
|
|
249
|
+
language?: string;
|
|
250
|
+
}): ScopeExclusion | undefined;
|
|
251
|
+
/** Breadth-first traversal with scope filtering and full trace recording. */
|
|
252
|
+
declare function traverse(graph: GraphIndex, options: TraverseOptions): TraverseResult;
|
|
253
|
+
/** Who links to this node (one hop inbound over `dcterms:references`). */
|
|
254
|
+
declare function reverseReferences(graph: GraphIndex, iri: string, options?: Omit<TraverseOptions, "seeds" | "direction" | "predicates">): TraverseResult;
|
|
255
|
+
/**
|
|
256
|
+
* Transitive inbound reach — "what is affected if this node changes".
|
|
257
|
+
* Excludes the node itself from the returned set. Defaults to depth 3 rather
|
|
258
|
+
* than the walker's 1, because impact analysis is only useful transitively.
|
|
259
|
+
*/
|
|
260
|
+
declare function impact(graph: GraphIndex, iri: string, options?: Omit<TraverseOptions, "seeds" | "direction">): TraverseResult;
|
|
261
|
+
|
|
262
|
+
interface ResolvedContent {
|
|
263
|
+
iri: string;
|
|
264
|
+
text: string;
|
|
265
|
+
sourceUrl: string;
|
|
266
|
+
title?: string;
|
|
267
|
+
}
|
|
268
|
+
interface ContentResolver {
|
|
269
|
+
/** Text for a node, or undefined when the node has no content. */
|
|
270
|
+
resolve(iri: string): Promise<ResolvedContent | undefined>;
|
|
271
|
+
}
|
|
272
|
+
type FetchLike = (url: string) => Promise<{
|
|
273
|
+
ok: boolean;
|
|
274
|
+
status: number;
|
|
275
|
+
text(): Promise<string>;
|
|
276
|
+
}>;
|
|
277
|
+
interface FetchResolverOptions {
|
|
278
|
+
/** Prefix joined to each `graph:path`, e.g. "https://site/raw/". */
|
|
279
|
+
baseUrl?: string;
|
|
280
|
+
/** Full control over path → URL. Overrides `baseUrl`. */
|
|
281
|
+
pathToUrl?: (path: string) => string;
|
|
282
|
+
/** Injectable fetch (defaults to the global). */
|
|
283
|
+
fetch?: FetchLike;
|
|
284
|
+
/** Record resolutions here. */
|
|
285
|
+
trace?: QueryTrace;
|
|
286
|
+
}
|
|
287
|
+
/** Split a section IRI into its document IRI and fragment. */
|
|
288
|
+
declare function splitFragment(iri: string): {
|
|
289
|
+
doc: string;
|
|
290
|
+
fragment?: string;
|
|
291
|
+
};
|
|
292
|
+
/**
|
|
293
|
+
* Sections of a document in true document order, reconstructed from the
|
|
294
|
+
* `dcterms:hasPart` tree ordered by `graph:order` within each parent.
|
|
295
|
+
*
|
|
296
|
+
* Needed because heading text is not unique: a document with two `## Install`
|
|
297
|
+
* headings produces two section nodes, and matching by title alone would give
|
|
298
|
+
* both the *first* heading's text — wrong content under a confident citation.
|
|
299
|
+
*/
|
|
300
|
+
declare function documentSectionOrder(graph: GraphIndex, docIri: string): string[];
|
|
301
|
+
/**
|
|
302
|
+
* Occurrence number of every section of a document, in one walk: section IRI →
|
|
303
|
+
* how many earlier sections share its heading text and level.
|
|
304
|
+
*
|
|
305
|
+
* Batched because the per-section form is quadratic — a document with N
|
|
306
|
+
* sections would otherwise rebuild the whole `hasPart` order N times, once per
|
|
307
|
+
* section indexed or resolved.
|
|
308
|
+
*/
|
|
309
|
+
declare function sectionOccurrences(graph: GraphIndex, docIri: string): Map<string, number>;
|
|
310
|
+
/**
|
|
311
|
+
* Which occurrence of its heading text a section is (0-based) — the number of
|
|
312
|
+
* earlier same-title, same-level sections in the same document. A section the
|
|
313
|
+
* `hasPart` tree does not reach counts as the first (0).
|
|
314
|
+
*/
|
|
315
|
+
declare function sectionOccurrence(graph: GraphIndex, sectionIri: string): number;
|
|
316
|
+
/**
|
|
317
|
+
* The prose before a document's first heading — the text that belongs to no
|
|
318
|
+
* section, and so would otherwise be indexed nowhere in a document that has
|
|
319
|
+
* sections.
|
|
320
|
+
*
|
|
321
|
+
* Fence-aware for the same reason `sliceSection` is: a `#` comment inside a
|
|
322
|
+
* leading code block is not a heading, and treating it as one would cut the
|
|
323
|
+
* preamble short. Frontmatter is *not* stripped here — the caller decides,
|
|
324
|
+
* since only the search index cares about that.
|
|
325
|
+
*/
|
|
326
|
+
declare function documentPreamble(markdown: string): string | undefined;
|
|
327
|
+
/**
|
|
328
|
+
* Slice a markdown document at the heading whose text matches `title`,
|
|
329
|
+
* returning that heading through to the next heading of the same or higher
|
|
330
|
+
* rank. Returns undefined when no such heading exists. Handles CRLF, and
|
|
331
|
+
* ignores `#` lines inside fenced code blocks.
|
|
332
|
+
*
|
|
333
|
+
* `occurrence` selects among repeated headings (0 = the first). Heading text is
|
|
334
|
+
* not unique, so callers with a specific section node in hand should pass
|
|
335
|
+
* `sectionOccurrence(graph, iri)` rather than defaulting to the first match.
|
|
336
|
+
*/
|
|
337
|
+
declare function sliceSection(markdown: string, title: string, level?: number, occurrence?: number): string | undefined;
|
|
338
|
+
/**
|
|
339
|
+
* The text a section *owns*: its heading down to the next heading of **any**
|
|
340
|
+
* rank, so nested subsections are excluded.
|
|
341
|
+
*
|
|
342
|
+
* `sliceSection` keeps the subtree because that is what retrieval wants — ask
|
|
343
|
+
* for "Configuration" and you should get its subsections too. Indexing wants
|
|
344
|
+
* the opposite: with the subtree, a parent section matches everything its
|
|
345
|
+
* children match and outranks them, so an H1 section shadows every heading
|
|
346
|
+
* beneath it. Same granularity rule as Document vs Section, one level down.
|
|
347
|
+
*/
|
|
348
|
+
declare function sectionOwnText(markdown: string, title: string, level?: number, occurrence?: number): string | undefined;
|
|
349
|
+
/**
|
|
350
|
+
* A resolver that fetches document sources over HTTP. Documents map through
|
|
351
|
+
* `graph:path`; sections slice their parent document by heading.
|
|
352
|
+
*/
|
|
353
|
+
declare function createFetchResolver(graph: GraphIndex, options?: FetchResolverOptions): ContentResolver;
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Context assembly (ADR 01018) — the runtime's terminal stage.
|
|
357
|
+
*
|
|
358
|
+
* Turns traversed nodes into the bundle an inference engine consumes:
|
|
359
|
+
* `{ context, citations, trace, refusal? }`. **dockg stops here**: it never
|
|
360
|
+
* calls a model. A different tool (an agent over MCP, a site backend) takes the
|
|
361
|
+
* bundle onward, which keeps this runtime fully deterministic and keeps API
|
|
362
|
+
* keys and inference cost outside dockg entirely.
|
|
363
|
+
*
|
|
364
|
+
* When nothing survives retrieval the result is a *structured refusal*, never
|
|
365
|
+
* empty context — a caller must be able to tell "no route exists" apart from
|
|
366
|
+
* "here is nothing, proceed anyway" (the disciplined-silence contract).
|
|
367
|
+
*
|
|
368
|
+
* Platform-neutral: no `node:` imports, no npm dependencies.
|
|
369
|
+
*/
|
|
370
|
+
|
|
371
|
+
interface ContextBlock {
|
|
372
|
+
iri: string;
|
|
373
|
+
title?: string;
|
|
374
|
+
text: string;
|
|
375
|
+
sourceUrl: string;
|
|
376
|
+
/** Hops from the nearest seed — the relevance signal, carried through. */
|
|
377
|
+
depth: number;
|
|
378
|
+
}
|
|
379
|
+
interface Citation {
|
|
380
|
+
iri: string;
|
|
381
|
+
title?: string;
|
|
382
|
+
sourceUrl: string;
|
|
383
|
+
}
|
|
384
|
+
type RefusalReason = "no-route" | "no-content";
|
|
385
|
+
interface Refusal {
|
|
386
|
+
reason: RefusalReason;
|
|
387
|
+
detail: string;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* The rankings that produced the seeds, carried through to the caller.
|
|
391
|
+
*
|
|
392
|
+
* Retrieval answers two different questions — "what matched the query?" and
|
|
393
|
+
* "what is the graph connected to it?" — and a consumer needs both. Returning
|
|
394
|
+
* only the graph result would make the match rankings inferrable solely from the
|
|
395
|
+
* trace, which is diagnostic rather than a result surface (ADR 01020).
|
|
396
|
+
*/
|
|
397
|
+
interface EntryRankings {
|
|
398
|
+
/** The lexical leg's own ranking. */
|
|
399
|
+
lexical: EntryCandidate[];
|
|
400
|
+
/** The vector leg's own ranking; empty when no embedder was in play. */
|
|
401
|
+
vector: EntryCandidate[];
|
|
402
|
+
/** The fused ranking that actually seeded traversal. */
|
|
403
|
+
merged: EntryCandidate[];
|
|
404
|
+
}
|
|
405
|
+
interface RetrievalBundle {
|
|
406
|
+
/** What matched the query, per leg. Absent when seeds were given explicitly. */
|
|
407
|
+
entry?: EntryRankings;
|
|
408
|
+
context: ContextBlock[];
|
|
409
|
+
citations: Citation[];
|
|
410
|
+
trace: QueryTrace;
|
|
411
|
+
/** Present when retrieval found nothing usable. `context` is then empty. */
|
|
412
|
+
refusal?: Refusal;
|
|
413
|
+
/** True when the character budget dropped otherwise-eligible blocks. */
|
|
414
|
+
truncated: boolean;
|
|
415
|
+
}
|
|
416
|
+
interface AssembleOptions {
|
|
417
|
+
/** Character budget across all blocks. Default: unbounded. */
|
|
418
|
+
maxChars?: number;
|
|
419
|
+
/** Append to an existing trace (normally the traversal's). */
|
|
420
|
+
trace?: QueryTrace;
|
|
421
|
+
/**
|
|
422
|
+
* The entry rankings that produced these nodes, passed through to the bundle
|
|
423
|
+
* so callers see matches and graph results side by side.
|
|
424
|
+
*/
|
|
425
|
+
entry?: EntryRankings;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Resolve traversed nodes to text and assemble the retrieval bundle. Nodes are
|
|
429
|
+
* kept in traversal order (nearest-first), which is deterministic, and every
|
|
430
|
+
* block carries the IRI it came from so citations are structural, not inferred.
|
|
431
|
+
*/
|
|
432
|
+
declare function assemble(resolver: ContentResolver, nodes: TraversedNode[], options?: AssembleOptions): Promise<RetrievalBundle>;
|
|
433
|
+
|
|
434
|
+
interface RdfTerm {
|
|
435
|
+
termType: "NamedNode" | "Literal" | "DefaultGraph";
|
|
436
|
+
value: string;
|
|
437
|
+
language?: string;
|
|
438
|
+
datatype?: RdfTerm;
|
|
439
|
+
equals(other?: RdfTerm | null): boolean;
|
|
440
|
+
}
|
|
441
|
+
interface RdfQuad {
|
|
442
|
+
termType: "Quad";
|
|
443
|
+
subject: RdfTerm;
|
|
444
|
+
predicate: RdfTerm;
|
|
445
|
+
object: RdfTerm;
|
|
446
|
+
graph: RdfTerm;
|
|
447
|
+
equals(other?: RdfQuad | null): boolean;
|
|
448
|
+
}
|
|
449
|
+
declare function namedNode(value: string): RdfTerm;
|
|
450
|
+
declare function defaultGraph(): RdfTerm;
|
|
451
|
+
declare function literal(value: string, datatype?: string): RdfTerm;
|
|
452
|
+
/**
|
|
453
|
+
* The whole index as RDF/JS quads, in the index's deterministic order. Feed to
|
|
454
|
+
* any RDF/JS store (`new Store(rdfjsQuads(graph))`) to run SPARQL.
|
|
455
|
+
*/
|
|
456
|
+
declare function rdfjsQuads(graph: GraphIndex): RdfQuad[];
|
|
457
|
+
/**
|
|
458
|
+
* Triple-pattern match returning RDF/JS quads. A null/undefined position is a
|
|
459
|
+
* wildcard. Convenience for callers who want RDF/JS terms without pulling in a
|
|
460
|
+
* store; the walker API is the richer (and traced) way to query.
|
|
461
|
+
*/
|
|
462
|
+
declare function matchQuads(graph: GraphIndex, subject?: string | null, predicate?: string | null, object?: string | null): RdfQuad[];
|
|
463
|
+
|
|
464
|
+
/** One indexable node. Empty fields are omitted so the artifact stays tight. */
|
|
465
|
+
interface SearchEntry {
|
|
466
|
+
/** Node IRI. */
|
|
467
|
+
id: string;
|
|
468
|
+
/** Compacted class IRI, e.g. `graph:Section`. */
|
|
469
|
+
type: string;
|
|
470
|
+
title?: string;
|
|
471
|
+
/** Space-joined alternative labels, deduped case-insensitively. */
|
|
472
|
+
labels?: string;
|
|
473
|
+
description?: string;
|
|
474
|
+
/** Body text, per the granularity rule. */
|
|
475
|
+
text?: string;
|
|
476
|
+
}
|
|
477
|
+
interface SearchIndexDoc {
|
|
478
|
+
version: 1;
|
|
479
|
+
entries: SearchEntry[];
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
interface LexicalSearchOptions {
|
|
483
|
+
/** Maximum candidates to return. Default 10. */
|
|
484
|
+
limit?: number;
|
|
485
|
+
}
|
|
486
|
+
interface LexicalIndex {
|
|
487
|
+
/** Ranked candidates, best first, ties broken by IRI. */
|
|
488
|
+
search(query: string, options?: LexicalSearchOptions): EntryCandidate[];
|
|
489
|
+
/** Indexed entry count. */
|
|
490
|
+
size(): number;
|
|
491
|
+
/** The entry behind an IRI, for callers that want its title/type. */
|
|
492
|
+
entry(iri: string): SearchEntry | undefined;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Build a lexical index from the search artifact — a JSON string or the parsed
|
|
496
|
+
* document.
|
|
497
|
+
*/
|
|
498
|
+
declare function createLexicalIndex(input: string | SearchIndexDoc): LexicalIndex;
|
|
499
|
+
|
|
500
|
+
interface VectorIndexHeader {
|
|
501
|
+
/** Embedding model id, e.g. an HF repo path. */
|
|
502
|
+
model: string;
|
|
503
|
+
/** Model-weight quantization the vectors were produced under. */
|
|
504
|
+
dtype: string;
|
|
505
|
+
/** Vector dimensions. Never assumed — always read from here. */
|
|
506
|
+
dims: number;
|
|
507
|
+
/** Number of vectors; equals `ids.length`. */
|
|
508
|
+
count: number;
|
|
509
|
+
/** Node IRIs, sorted, in payload order. */
|
|
510
|
+
ids: string[];
|
|
511
|
+
/** Digest of the `search.<lang>.json` these were built from (staleness detection). */
|
|
512
|
+
source: string;
|
|
513
|
+
/**
|
|
514
|
+
* BCP-47 tag of the index these cover, or `und` (ADR 01038). Recorded so a
|
|
515
|
+
* host that fetched the wrong pair is refused rather than ranked against a
|
|
516
|
+
* model that never saw the language.
|
|
517
|
+
*/
|
|
518
|
+
language: string;
|
|
519
|
+
}
|
|
520
|
+
interface VectorIndexDoc {
|
|
521
|
+
header: VectorIndexHeader;
|
|
522
|
+
/** `count * dims` floats, L2-normalized, row-major in `ids` order. */
|
|
523
|
+
vectors: Float32Array;
|
|
524
|
+
}
|
|
525
|
+
/** Thrown for malformed input so callers can map it to an exit code / refusal. */
|
|
526
|
+
declare class VectorIndexError extends Error {
|
|
527
|
+
constructor(message: string);
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* L2-normalize a vector in place and return it. A zero vector is left alone —
|
|
531
|
+
* dividing by its norm would produce NaNs that silently poison every later dot
|
|
532
|
+
* product.
|
|
533
|
+
*/
|
|
534
|
+
declare function normalize(vector: Float32Array): Float32Array;
|
|
535
|
+
/**
|
|
536
|
+
* Encode entries into the sidecar. Entries are sorted by IRI first, so the bytes
|
|
537
|
+
* depend on the set of entries and not on the order they were produced in.
|
|
538
|
+
*/
|
|
539
|
+
declare function encodeVectorIndex(entries: Array<{
|
|
540
|
+
id: string;
|
|
541
|
+
vector: Float32Array;
|
|
542
|
+
}>, meta: {
|
|
543
|
+
model: string;
|
|
544
|
+
dtype: string;
|
|
545
|
+
source: string;
|
|
546
|
+
language: string;
|
|
547
|
+
}): Uint8Array;
|
|
548
|
+
/** Decode a sidecar, rejecting anything malformed rather than mis-reading it. */
|
|
549
|
+
declare function decodeVectorIndex(bytes: Uint8Array): VectorIndexDoc;
|
|
550
|
+
|
|
551
|
+
interface VectorSearchOptions {
|
|
552
|
+
/** Maximum candidates to return. Default 10. */
|
|
553
|
+
limit?: number;
|
|
554
|
+
/**
|
|
555
|
+
* Drop candidates scoring below this. Default 0 — every model has its own
|
|
556
|
+
* useful range, so dockg does not impose a threshold the user did not choose.
|
|
557
|
+
*/
|
|
558
|
+
minScore?: number;
|
|
559
|
+
}
|
|
560
|
+
/** Why a vector index cannot be used with the embedder or corpus at hand. */
|
|
561
|
+
interface VectorMismatch {
|
|
562
|
+
reason: "model" | "dtype" | "dims" | "stale-source";
|
|
563
|
+
detail: string;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Thrown when a query would be ranked against vectors it cannot be compared to.
|
|
567
|
+
*
|
|
568
|
+
* A distinct type so hosts can catch exactly this — and so it cannot be mistaken
|
|
569
|
+
* for "no results". Ranking on regardless is the silent-wrong-answer failure
|
|
570
|
+
* ADR 01020 exists to prevent, so this is loud by design.
|
|
571
|
+
*/
|
|
572
|
+
declare class VectorMismatchError extends Error {
|
|
573
|
+
readonly reason: VectorMismatch["reason"];
|
|
574
|
+
constructor(mismatch: VectorMismatch);
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* The digest `manni graph embed` records as a sidecar's `source`, computed the same
|
|
578
|
+
* way in a browser — SHA-256 over the **raw bytes of `search.json` exactly as
|
|
579
|
+
* fetched**, hex, prefixed `sha256:`.
|
|
580
|
+
*
|
|
581
|
+
* Exists so a host can pass `source` to `findEntry` and get the staleness half
|
|
582
|
+
* of the refusal (ADR 01020) without reconstructing the recipe. Re-serializing
|
|
583
|
+
* the parsed JSON would change the bytes and so the digest — pass the response
|
|
584
|
+
* text, not `JSON.stringify(doc)`.
|
|
585
|
+
*
|
|
586
|
+
* Uses Web Crypto, which is platform-neutral: browsers and Node 18+ both have
|
|
587
|
+
* it on `globalThis`.
|
|
588
|
+
*/
|
|
589
|
+
declare function searchIndexDigest(raw: string): Promise<string>;
|
|
590
|
+
interface VectorIndex {
|
|
591
|
+
/**
|
|
592
|
+
* Rank nodes against a query vector. The vector is normalized here, so
|
|
593
|
+
* callers need not — an unnormalized query would otherwise scale every score.
|
|
594
|
+
*/
|
|
595
|
+
search(query: Float32Array, options?: VectorSearchOptions): EntryCandidate[];
|
|
596
|
+
/**
|
|
597
|
+
* Check this index against the embedder and corpus in play. Returns the
|
|
598
|
+
* mismatch, or undefined when they agree. Ranking against vectors from a
|
|
599
|
+
* different model compares outputs of two different functions (ADR 01020), so
|
|
600
|
+
* callers should refuse rather than proceed.
|
|
601
|
+
*/
|
|
602
|
+
check(expected: {
|
|
603
|
+
model?: string;
|
|
604
|
+
dtype?: string;
|
|
605
|
+
dims?: number;
|
|
606
|
+
source?: string;
|
|
607
|
+
}): VectorMismatch | undefined;
|
|
608
|
+
readonly model: string;
|
|
609
|
+
/**
|
|
610
|
+
* Weight quantization the corpus vectors were produced under. The query side
|
|
611
|
+
* must embed at the *same* dtype — q8 and fp32 weights are different
|
|
612
|
+
* functions, so mixing them compares incomparable vectors (ADR 01020).
|
|
613
|
+
*/
|
|
614
|
+
readonly dtype: string;
|
|
615
|
+
readonly dims: number;
|
|
616
|
+
readonly source: string;
|
|
617
|
+
size(): number;
|
|
618
|
+
/** The IRI at a payload row, for callers walking the index directly. */
|
|
619
|
+
idAt(row: number): string | undefined;
|
|
620
|
+
}
|
|
621
|
+
/** Build a vector index from encoded sidecar bytes. */
|
|
622
|
+
declare function createVectorIndex(bytes: Uint8Array): VectorIndex;
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Enough of an embedder to verify it against a vector index. Structural rather
|
|
626
|
+
* than the `Embedder` interface itself, so the runtime stays independent of
|
|
627
|
+
* `@hawkeyexl/manni/graph/embed` — any object with these three fields works.
|
|
628
|
+
*/
|
|
629
|
+
interface QueryEmbedder {
|
|
630
|
+
readonly model: string;
|
|
631
|
+
readonly dtype: string;
|
|
632
|
+
embed(text: string): Promise<Float32Array> | Float32Array;
|
|
633
|
+
}
|
|
634
|
+
interface FindEntryOptions {
|
|
635
|
+
/** The lexical index to query. */
|
|
636
|
+
lexical: LexicalIndex;
|
|
637
|
+
/** The vector index. Without it (and an embedder), entry is lexical-only. */
|
|
638
|
+
vectors?: VectorIndex;
|
|
639
|
+
/**
|
|
640
|
+
* The embedder for the query side. **Prefer this over `embedQuery`**: it
|
|
641
|
+
* carries the model and dtype, so `findEntry` can verify they match the
|
|
642
|
+
* vector index and refuse rather than rank against incomparable vectors.
|
|
643
|
+
*/
|
|
644
|
+
embedder?: QueryEmbedder;
|
|
645
|
+
/**
|
|
646
|
+
* Embed the query with a bare function. An escape hatch for hosts wiring
|
|
647
|
+
* their own model — but it carries no identity, so **the model/dtype match
|
|
648
|
+
* cannot be verified**; only the dimension check inside `search` applies.
|
|
649
|
+
* Prefer `embedder`.
|
|
650
|
+
*/
|
|
651
|
+
embedQuery?: (query: string) => Promise<Float32Array> | Float32Array;
|
|
652
|
+
/**
|
|
653
|
+
* Digest of the `search.json` this caller loaded, from `searchIndexDigest`.
|
|
654
|
+
* Supplied, a sidecar built from a *different* corpus is refused rather than
|
|
655
|
+
* ranked — its IRIs may no longer exist, and it misses everything added
|
|
656
|
+
* since. Omitted, that half of the check simply does not run: the runtime
|
|
657
|
+
* cannot derive the digest, since it never sees the raw bytes.
|
|
658
|
+
*/
|
|
659
|
+
source?: string;
|
|
660
|
+
/** Maximum seeds to return. Default 10. */
|
|
661
|
+
limit?: number;
|
|
662
|
+
/** Append candidates here instead of starting a new trace. */
|
|
663
|
+
trace?: QueryTrace;
|
|
664
|
+
}
|
|
665
|
+
interface EntryResult {
|
|
666
|
+
/** The fused ranking — what seeds traversal. */
|
|
667
|
+
candidates: EntryCandidate[];
|
|
668
|
+
/** The lexical leg's own ranking. */
|
|
669
|
+
lexical: EntryCandidate[];
|
|
670
|
+
/** The vector leg's own ranking; empty when no embedder was supplied. */
|
|
671
|
+
vector: EntryCandidate[];
|
|
672
|
+
trace: QueryTrace;
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Reciprocal rank fusion over N ranked lists: score = Σ 1/(k + rank).
|
|
676
|
+
*
|
|
677
|
+
* Rank-based rather than score-based on purpose — lexical BM25 scores and
|
|
678
|
+
* cosine similarities are not on a comparable scale, so fusing raw scores would
|
|
679
|
+
* let whichever leg happens to produce bigger numbers dominate. `k = 60` is the
|
|
680
|
+
* standard damping constant.
|
|
681
|
+
*
|
|
682
|
+
* Deterministic: input list order does not affect the result, and ties break by
|
|
683
|
+
* IRI.
|
|
684
|
+
*/
|
|
685
|
+
declare function rrfMerge(rankings: EntryCandidate[][], k?: number): EntryCandidate[];
|
|
686
|
+
/**
|
|
687
|
+
* Find seed nodes for a question, returning each ranking leg and their fusion.
|
|
688
|
+
*
|
|
689
|
+
* Async because embedding a query is: local models are a WASM forward pass.
|
|
690
|
+
* Uniformly async rather than sync-when-lexical-only, so callers have one
|
|
691
|
+
* contract to hold rather than a return type that changes with the options.
|
|
692
|
+
*
|
|
693
|
+
* The vector leg runs only when both a `vectors` index and an `embedQuery` are
|
|
694
|
+
* supplied. Absent either, this is lexical entry exactly as Phase 8 shipped it —
|
|
695
|
+
* additive, never a new failure mode (ADR 01009's degrade-don't-fail rule).
|
|
696
|
+
*/
|
|
697
|
+
declare function findEntry(query: string, options: FindEntryOptions): Promise<EntryResult>;
|
|
698
|
+
|
|
699
|
+
/** Conventional filename, written beside the graph. */
|
|
700
|
+
declare const LOCALIZATIONS_FILENAME = "localizations.json";
|
|
701
|
+
/**
|
|
702
|
+
* BCP-47's tag for undetermined, used as the bucket for documents that declare
|
|
703
|
+
* no language. A real tag rather than a dockg invention, so a consumer that
|
|
704
|
+
* knows BCP-47 already knows what it means — and so the bucket sorts, filters,
|
|
705
|
+
* and names a file like any other language.
|
|
706
|
+
*/
|
|
707
|
+
declare const UNDETERMINED = "und";
|
|
708
|
+
/** Where one language's lexical index lives, and what it holds. */
|
|
709
|
+
interface SearchArtifact {
|
|
710
|
+
/** Filename, relative to the manifest. */
|
|
711
|
+
path: string;
|
|
712
|
+
/** Indexed nodes. */
|
|
713
|
+
entries: number;
|
|
714
|
+
/** `sha256:…` of the file, so a consumer can detect a stale pair. */
|
|
715
|
+
digest: string;
|
|
716
|
+
}
|
|
717
|
+
/** Where one language's vector sidecar lives, and what produced it. */
|
|
718
|
+
interface VectorArtifact {
|
|
719
|
+
path: string;
|
|
720
|
+
/** The embedding model this language was embedded with. */
|
|
721
|
+
model: string;
|
|
722
|
+
dtype: string;
|
|
723
|
+
dims: number;
|
|
724
|
+
count: number;
|
|
725
|
+
}
|
|
726
|
+
interface LocalizationEntry {
|
|
727
|
+
/** BCP-47 tag, or `und` for the undeclared bucket. */
|
|
728
|
+
language: string;
|
|
729
|
+
/** Documents in this language (sections excluded). */
|
|
730
|
+
documents: number;
|
|
731
|
+
search: SearchArtifact;
|
|
732
|
+
/** Absent until `manni graph embed` has run for this language. */
|
|
733
|
+
vectors?: VectorArtifact;
|
|
734
|
+
}
|
|
735
|
+
interface LocalizationsDoc {
|
|
736
|
+
version: 1;
|
|
737
|
+
/** One entry per language present, sorted by tag. */
|
|
738
|
+
languages: LocalizationEntry[];
|
|
739
|
+
}
|
|
740
|
+
/** `search.de.json` — the lexical index for one language. */
|
|
741
|
+
declare function searchIndexFilename(language: string): string;
|
|
742
|
+
/** `vectors.de.bin` — the vector sidecar for one language. */
|
|
743
|
+
declare function vectorIndexFilename(language: string): string;
|
|
744
|
+
/**
|
|
745
|
+
* Serialize the manifest: sorted by language, two-space indent, trailing
|
|
746
|
+
* newline — byte-stable like every other dockg artifact.
|
|
747
|
+
*/
|
|
748
|
+
declare function emitLocalizations(doc: LocalizationsDoc): string;
|
|
749
|
+
declare function parseLocalizations(text: string): LocalizationsDoc | undefined;
|
|
750
|
+
|
|
751
|
+
export { type AssembleOptions, type Citation, type ContentResolver, type ContextBlock, type Direction, type EntryCandidate, type EntryRankings, type EntryResult, type EntryVia, type FetchResolverOptions, type FindEntryOptions, GraphIndex, type GraphNode, type Hop, LOCALIZATIONS_FILENAME, type LexicalIndex, type LexicalSearchOptions, type LocalizationEntry, type LocalizationsDoc, type QueryEmbedder, type QueryTrace, type RdfQuad, type RdfTerm, type Refusal, type RefusalReason, type Resolution, type ResolvedContent, type RetrievalBundle, type ScopeExclusion, type ScopeFilter, type SearchArtifact, type SearchEntry, type SearchIndexDoc, type TraverseOptions, type TraverseResult, type TraversedNode, UNDETERMINED, type Value, type VectorArtifact, type VectorIndex, type VectorIndexDoc, VectorIndexError, type VectorIndexHeader, type VectorMismatch, VectorMismatchError, type VectorSearchOptions, assemble, createFetchResolver, createLexicalIndex, createTrace, createVectorIndex, decodeVectorIndex, defaultGraph, documentPreamble, documentSectionOrder, emitLocalizations, encodeVectorIndex, findEntry, impact, literal, matchQuads, namedNode, normalize, parseLocalizations, rdfjsQuads, reachedNodes, resolveSubject, resolveVariant, reverseReferences, rrfMerge, scopeExclusion, searchIndexDigest, searchIndexFilename, sectionOccurrence, sectionOccurrences, sectionOwnText, sliceSection, splitFragment, traverse, vectorIndexFilename };
|