@agentxm/knowledge-query 0.28.4-bootstrap.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.
@@ -0,0 +1,373 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Data from "effect/Data";
3
+ import * as Result from "effect/Result";
4
+ import * as ServiceMap from "effect/Context";
5
+ import { createHash } from "node:crypto";
6
+ import {} from "@agentxm/extension-model/unstable/knowledge/concept-ref";
7
+ import { resolveKnowledgeFrontmatterPointer } from "./knowledge-projection.js";
8
+ import { projectKnowledgeConcepts, } from "./knowledge-projection.js";
9
+ import { knowledgeQueryIdentity, } from "./knowledge-query.js";
10
+ import { computeKnowledgeCorpusFingerprint, computeKnowledgeProjectionRevision, } from "./knowledge-revision.js";
11
+ import { tokenizeKnowledgeSearchText } from "@agentxm/registry-protocol/unstable/knowledge/knowledge-search";
12
+ export class KnowledgeCursorInvalidError extends Data.TaggedError("KnowledgeCursorInvalidError") {
13
+ }
14
+ const CURSOR_VERSION = "axm-knowledge-cursor-v1";
15
+ const CURSOR_MAX_AGE_MS = 86_400_000;
16
+ const queryDigest = (query) => createHash("sha256")
17
+ .update(JSON.stringify(knowledgeQueryIdentity(query)))
18
+ .digest("hex");
19
+ const encodeCursor = (payload) => Buffer.from(JSON.stringify(payload)).toString("base64url");
20
+ const isCursorPayload = (value) => {
21
+ if (typeof value !== "object" || value === null || Array.isArray(value))
22
+ return false;
23
+ return ("version" in value &&
24
+ value.version === CURSOR_VERSION &&
25
+ "corpusFingerprint" in value &&
26
+ typeof value.corpusFingerprint === "string" &&
27
+ "queryDigest" in value &&
28
+ typeof value.queryDigest === "string" &&
29
+ "offset" in value &&
30
+ typeof value.offset === "number" &&
31
+ Number.isSafeInteger(value.offset) &&
32
+ value.offset >= 0 &&
33
+ "issuedAt" in value &&
34
+ typeof value.issuedAt === "number" &&
35
+ Number.isSafeInteger(value.issuedAt));
36
+ };
37
+ const decodeCursor = (cursor, snapshot, query, now) => {
38
+ let decoded;
39
+ try {
40
+ if (!/^[A-Za-z0-9_-]+$/u.test(cursor)) {
41
+ return Result.fail(new KnowledgeCursorInvalidError({ reason: "invalid" }));
42
+ }
43
+ decoded = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
44
+ }
45
+ catch {
46
+ return Result.fail(new KnowledgeCursorInvalidError({ reason: "invalid" }));
47
+ }
48
+ if (!isCursorPayload(decoded)) {
49
+ return Result.fail(new KnowledgeCursorInvalidError({ reason: "invalid" }));
50
+ }
51
+ if (decoded.issuedAt > now || now - decoded.issuedAt > CURSOR_MAX_AGE_MS) {
52
+ return Result.fail(new KnowledgeCursorInvalidError({ reason: "expired" }));
53
+ }
54
+ if (decoded.corpusFingerprint !== snapshot.fingerprint) {
55
+ return Result.fail(new KnowledgeCursorInvalidError({ reason: "corpus-changed" }));
56
+ }
57
+ if (decoded.queryDigest !== queryDigest(query)) {
58
+ return Result.fail(new KnowledgeCursorInvalidError({ reason: "query-changed" }));
59
+ }
60
+ return Result.succeed(decoded);
61
+ };
62
+ const sourceForConcept = (bundle, concept) => bundle.sources.find((source) => source.relativePath === concept.relativePath);
63
+ export const makeKnowledgeIndexSnapshot = (bundles) => {
64
+ const sources = bundles.flatMap(({ sources }) => sources);
65
+ const concepts = [];
66
+ for (const bundle of [...bundles].sort((left, right) => left.bundle.localeCompare(right.bundle))) {
67
+ const bundleFingerprint = computeKnowledgeCorpusFingerprint(bundle.sources);
68
+ const projected = projectKnowledgeConcepts(bundle.bundle, bundle.inspection.concepts);
69
+ for (const [index, source] of bundle.inspection.concepts.entries()) {
70
+ const projection = projected[index];
71
+ const captured = sourceForConcept(bundle, source);
72
+ if (projection === undefined || captured === undefined)
73
+ continue;
74
+ concepts.push({
75
+ ref: {
76
+ bundle: bundle.bundle,
77
+ conceptId: source.id,
78
+ bundleVersion: bundle.version,
79
+ bundleFingerprint,
80
+ contentRevision: captured.sourceRevision,
81
+ },
82
+ projectionRevision: computeKnowledgeProjectionRevision(projection),
83
+ projected: projection,
84
+ source,
85
+ sourceBytes: captured.bytes,
86
+ });
87
+ }
88
+ }
89
+ return {
90
+ fingerprint: computeKnowledgeCorpusFingerprint(sources),
91
+ concepts: concepts.sort((left, right) => left.ref.bundle.localeCompare(right.ref.bundle) ||
92
+ left.ref.conceptId.localeCompare(right.ref.conceptId)),
93
+ };
94
+ };
95
+ const normalizedLiteral = (value) => value.normalize("NFKC").toUpperCase().toLowerCase();
96
+ const containsPhrase = (fieldTokens, phraseTokens) => {
97
+ if (phraseTokens.length > fieldTokens.length)
98
+ return false;
99
+ for (let start = 0; start <= fieldTokens.length - phraseTokens.length; start += 1) {
100
+ if (phraseTokens.every((token, offset) => fieldTokens[start + offset] === token))
101
+ return true;
102
+ }
103
+ return false;
104
+ };
105
+ const textMatches = (text, clause) => {
106
+ switch (clause.kind) {
107
+ case "term":
108
+ return tokenizeKnowledgeSearchText(text).includes(tokenizeKnowledgeSearchText(clause.value)[0] ?? "");
109
+ case "phrase":
110
+ return containsPhrase(tokenizeKnowledgeSearchText(text), tokenizeKnowledgeSearchText(clause.value));
111
+ case "literal":
112
+ return normalizedLiteral(text).includes(normalizedLiteral(clause.value));
113
+ default:
114
+ return clause;
115
+ }
116
+ };
117
+ const valueMatches = (candidate, operator, expected) => {
118
+ const left = normalizedLiteral(candidate);
119
+ const right = normalizedLiteral(expected);
120
+ if (operator === "equals")
121
+ return left === right;
122
+ if (operator === "not-equals")
123
+ return left !== right;
124
+ return left.includes(right);
125
+ };
126
+ const stringValues = (value) => {
127
+ if (typeof value === "string")
128
+ return [value];
129
+ if (typeof value === "number" || typeof value === "boolean")
130
+ return [String(value)];
131
+ if (Array.isArray(value))
132
+ return value.flatMap(stringValues);
133
+ return [];
134
+ };
135
+ const metadataValues = (concept, field) => {
136
+ switch (field) {
137
+ case "bundle":
138
+ return [concept.bundle];
139
+ case "conceptId":
140
+ return [concept.conceptId];
141
+ case "kind":
142
+ return [concept.kind];
143
+ case "title":
144
+ return concept.title === undefined ? [] : [concept.title];
145
+ case "description":
146
+ return concept.description === undefined ? [] : [concept.description];
147
+ case "tag":
148
+ return concept.tags ?? [];
149
+ case "type":
150
+ return concept.type === undefined ? [] : [concept.type];
151
+ case "resource":
152
+ return concept.resource === undefined ? [] : [concept.resource];
153
+ default:
154
+ return field;
155
+ }
156
+ };
157
+ const lifecycleValues = (concept, field) => {
158
+ switch (field) {
159
+ case "status":
160
+ return concept.status === undefined ? [] : [concept.status];
161
+ case "staleAfter":
162
+ return concept.staleAfter === undefined ? [] : [concept.staleAfter];
163
+ case "generated":
164
+ return concept.generated === undefined ? [] : [concept.generated.by];
165
+ case "verified":
166
+ return concept.verified?.map(({ by }) => by) ?? [];
167
+ case "trust":
168
+ return concept.trust === undefined ? [] : [concept.trust];
169
+ default:
170
+ return field;
171
+ }
172
+ };
173
+ const collectionMatches = (values, operator, expected) => operator === "not-equals"
174
+ ? values.every((value) => valueMatches(value, operator, expected))
175
+ : values.some((value) => valueMatches(value, operator, expected));
176
+ const matchingUnits = (concept, clause) => {
177
+ if (clause.kind === "term" || clause.kind === "phrase" || clause.kind === "literal") {
178
+ return concept.searchableUnits.filter((unit) => textMatches(unit.text, clause));
179
+ }
180
+ if (clause.kind === "field") {
181
+ return concept.searchableUnits.filter((unit) => unit.field === clause.field && textMatches(unit.text, clause.clause));
182
+ }
183
+ return [];
184
+ };
185
+ const clauseMatches = (concept, clause) => {
186
+ switch (clause.kind) {
187
+ case "term":
188
+ case "phrase":
189
+ case "literal":
190
+ case "field":
191
+ return matchingUnits(concept, clause).length > 0;
192
+ case "metadata":
193
+ return collectionMatches(metadataValues(concept, clause.field), clause.operator, clause.value);
194
+ case "lifecycle":
195
+ return collectionMatches(lifecycleValues(concept, clause.field), clause.operator, clause.value);
196
+ case "property": {
197
+ const resolved = resolveKnowledgeFrontmatterPointer(concept.frontmatter, clause.pointer);
198
+ return (resolved.found &&
199
+ collectionMatches(stringValues(resolved.value), clause.operator, clause.value));
200
+ }
201
+ default:
202
+ return clause;
203
+ }
204
+ };
205
+ const hasExplicitFilter = (query, kind, field) => query.clauses.some((clause) => clause.kind === kind && clause.field === field);
206
+ const fieldWeight = (field) => {
207
+ switch (field) {
208
+ case "title":
209
+ return 8;
210
+ case "conceptId":
211
+ case "tag":
212
+ return 6;
213
+ case "description":
214
+ case "type":
215
+ return 4;
216
+ case "body":
217
+ return 2;
218
+ default:
219
+ return 1;
220
+ }
221
+ };
222
+ const locateTokenSequence = (text, tokens) => {
223
+ if (tokens.length === 0)
224
+ return undefined;
225
+ const normalized = normalizedLiteral(text);
226
+ const visit = (tokenIndex, searchStart, firstStart) => {
227
+ const token = tokens[tokenIndex];
228
+ if (token === undefined) {
229
+ return firstStart === undefined ? undefined : { start: firstStart, end: searchStart };
230
+ }
231
+ const needle = normalizedLiteral(token);
232
+ let start = normalized.indexOf(needle, searchStart);
233
+ while (start >= 0) {
234
+ const between = text.slice(searchStart, start);
235
+ if (tokenIndex === 0 || tokenizeKnowledgeSearchText(between).length === 0) {
236
+ const end = start + needle.length;
237
+ const found = visit(tokenIndex + 1, end, firstStart ?? start);
238
+ if (found !== undefined)
239
+ return found;
240
+ }
241
+ start = normalized.indexOf(needle, start + 1);
242
+ }
243
+ return undefined;
244
+ };
245
+ return visit(0, 0);
246
+ };
247
+ const exactSpan = (text, clause, clauseIndex, field) => {
248
+ if (clause.kind === "literal") {
249
+ const needle = normalizedLiteral(clause.value);
250
+ const start = normalizedLiteral(text).indexOf(needle);
251
+ if (start < 0)
252
+ return undefined;
253
+ return { clauseIndex, field, start, end: Math.min(text.length, start + needle.length) };
254
+ }
255
+ const located = locateTokenSequence(text, tokenizeKnowledgeSearchText(clause.value));
256
+ return located === undefined ? undefined : { clauseIndex, field, ...located };
257
+ };
258
+ const rankConcept = (concept, query) => {
259
+ if (!hasExplicitFilter(query, "metadata", "kind") && concept.projected.kind !== "concept") {
260
+ return undefined;
261
+ }
262
+ if (!hasExplicitFilter(query, "lifecycle", "status") &&
263
+ normalizedLiteral(concept.projected.status ?? "") === "deprecated") {
264
+ return undefined;
265
+ }
266
+ if (!query.clauses.every((clause) => clauseMatches(concept.projected, clause)))
267
+ return undefined;
268
+ const fields = new Set();
269
+ const passageSpans = new Map();
270
+ let score = 0;
271
+ for (const [clauseIndex, clause] of query.clauses.entries()) {
272
+ const units = matchingUnits(concept.projected, clause);
273
+ const clauseFields = new Set(units.map(({ field }) => field));
274
+ for (const field of clauseFields)
275
+ score += fieldWeight(field);
276
+ for (const unit of units) {
277
+ fields.add(unit.field);
278
+ if (unit.passageIndex === undefined)
279
+ continue;
280
+ const textClause = clause.kind === "field" ? clause.clause : clause;
281
+ if (textClause.kind !== "term" &&
282
+ textClause.kind !== "phrase" &&
283
+ textClause.kind !== "literal") {
284
+ continue;
285
+ }
286
+ const span = exactSpan(unit.text, textClause, clauseIndex, unit.field);
287
+ if (span === undefined)
288
+ continue;
289
+ const spans = passageSpans.get(unit.passageIndex) ?? [];
290
+ spans.push(span);
291
+ passageSpans.set(unit.passageIndex, spans);
292
+ }
293
+ }
294
+ const passages = [...passageSpans]
295
+ .sort(([left], [right]) => left - right)
296
+ .slice(0, query.passageLimit)
297
+ .flatMap(([passageIndex, spans]) => {
298
+ const passage = concept.projected.bodyPassages[passageIndex];
299
+ if (passage === undefined)
300
+ return [];
301
+ const text = passage.text.slice(0, query.passageLength);
302
+ return [{ ...passage, text, spans: spans.filter(({ start }) => start < text.length) }];
303
+ });
304
+ return { concept, score, matchedFields: [...fields].sort(), passages };
305
+ };
306
+ const toResult = (ranked) => {
307
+ const { projected } = ranked.concept;
308
+ return {
309
+ ref: ranked.concept.ref,
310
+ orderingKey: `${ranked.concept.ref.bundle}#${ranked.concept.ref.conceptId}`,
311
+ matchedFields: ranked.matchedFields,
312
+ passages: ranked.passages,
313
+ kind: projected.kind,
314
+ ...(projected.title === undefined ? {} : { title: projected.title }),
315
+ ...(projected.description === undefined ? {} : { description: projected.description }),
316
+ ...(projected.status === undefined ? {} : { status: projected.status }),
317
+ ...(projected.staleAfter === undefined ? {} : { staleAfter: projected.staleAfter }),
318
+ ...(projected.generated === undefined ? {} : { generated: projected.generated }),
319
+ ...(projected.verified === undefined ? {} : { verified: projected.verified }),
320
+ ...(projected.trust === undefined ? {} : { trust: projected.trust }),
321
+ relativePath: projected.relativePath,
322
+ };
323
+ };
324
+ export const queryKnowledgeIndexResult = (snapshot, query, now) => {
325
+ const ranked = snapshot.concepts.flatMap((concept) => {
326
+ const candidate = rankConcept(concept, query);
327
+ return candidate === undefined ? [] : [candidate];
328
+ });
329
+ ranked.sort((left, right) => {
330
+ if (query.ordering === "relevance" && left.score !== right.score)
331
+ return right.score - left.score;
332
+ return (left.concept.ref.bundle.localeCompare(right.concept.ref.bundle) ||
333
+ left.concept.ref.conceptId.localeCompare(right.concept.ref.conceptId));
334
+ });
335
+ const decoded = query.cursor === undefined
336
+ ? Result.succeed({ offset: 0 })
337
+ : decodeCursor(query.cursor, snapshot, query, now);
338
+ if (!Result.isSuccess(decoded))
339
+ return Result.fail(decoded.failure);
340
+ const offset = decoded.success.offset;
341
+ if (offset > ranked.length) {
342
+ return Result.fail(new KnowledgeCursorInvalidError({ reason: "invalid" }));
343
+ }
344
+ const items = ranked.slice(offset, offset + query.resultLimit).map(toResult);
345
+ const nextOffset = offset + items.length;
346
+ const hasMore = nextOffset < ranked.length;
347
+ return Result.succeed({
348
+ items,
349
+ count: ranked.length,
350
+ hasMore,
351
+ ...(hasMore
352
+ ? {
353
+ cursor: encodeCursor({
354
+ version: CURSOR_VERSION,
355
+ corpusFingerprint: snapshot.fingerprint,
356
+ queryDigest: queryDigest(query),
357
+ offset: nextOffset,
358
+ issuedAt: now,
359
+ }),
360
+ }
361
+ : {}),
362
+ });
363
+ };
364
+ export const queryKnowledgeIndex = (snapshot, query, now) => {
365
+ const result = queryKnowledgeIndexResult(snapshot, query, now);
366
+ if (Result.isSuccess(result))
367
+ return result.success;
368
+ throw result.failure;
369
+ };
370
+ export const getKnowledgeIndexConcept = (snapshot, bundle, conceptId) => snapshot.concepts.find((concept) => concept.ref.bundle === bundle && concept.ref.conceptId === conceptId);
371
+ export class KnowledgeIndex extends ServiceMap.Service()("@agentxm/knowledge-query/knowledge-index/KnowledgeIndex") {
372
+ }
373
+ //# sourceMappingURL=knowledge-index.js.map
@@ -0,0 +1,61 @@
1
+ import type { KnowledgeActorRecord, KnowledgeAuthoredLink, KnowledgeConcept, KnowledgeDocumentKind, KnowledgeTrustTier } from "@agentxm/registry-protocol/unstable/knowledge/okf";
2
+ export type KnowledgeSearchableField = "bundle" | "conceptId" | "title" | "description" | "tag" | "type" | "body" | "resource" | "status" | "staleAfter" | "generated" | "verified" | "trust";
3
+ export interface KnowledgeBodyPassage {
4
+ readonly text: string;
5
+ readonly section: ReadonlyArray<string>;
6
+ /** One-based line within the frontmatter-stripped body. */
7
+ readonly startLine: number;
8
+ /** One-based inclusive line within the frontmatter-stripped body. */
9
+ readonly endLine: number;
10
+ }
11
+ export interface KnowledgeSearchableUnit {
12
+ readonly field: KnowledgeSearchableField;
13
+ readonly text: string;
14
+ readonly passageIndex?: number;
15
+ }
16
+ export interface KnowledgeOutgoingLink extends KnowledgeAuthoredLink {
17
+ readonly origin: "authored";
18
+ readonly sourceConceptId: string;
19
+ readonly sourceRelativePath: string;
20
+ }
21
+ export interface KnowledgeBacklink {
22
+ readonly origin: "derived-backlink";
23
+ readonly sourceConceptId: string;
24
+ readonly sourceRelativePath: string;
25
+ readonly targetConceptId: string;
26
+ readonly line: number;
27
+ }
28
+ export interface KnowledgeProjectedConcept {
29
+ readonly bundle: string;
30
+ readonly conceptId: string;
31
+ readonly kind: KnowledgeDocumentKind;
32
+ readonly title?: string;
33
+ readonly description?: string;
34
+ readonly tags?: ReadonlyArray<string>;
35
+ readonly type?: string;
36
+ readonly resource?: string;
37
+ readonly status?: string;
38
+ readonly staleAfter?: string;
39
+ readonly generated?: KnowledgeActorRecord;
40
+ readonly verified?: ReadonlyArray<KnowledgeActorRecord>;
41
+ readonly trust?: KnowledgeTrustTier;
42
+ /** Complete parsed frontmatter, including producer-defined extension properties. */
43
+ readonly frontmatter?: Readonly<Record<string, unknown>>;
44
+ readonly relativePath: string;
45
+ readonly bodyPassages: ReadonlyArray<KnowledgeBodyPassage>;
46
+ readonly searchableUnits: ReadonlyArray<KnowledgeSearchableUnit>;
47
+ readonly outgoingLinks: ReadonlyArray<KnowledgeOutgoingLink>;
48
+ readonly backlinks: ReadonlyArray<KnowledgeBacklink>;
49
+ }
50
+ export type KnowledgeFrontmatterPointerResult = {
51
+ readonly found: true;
52
+ readonly value: unknown;
53
+ } | {
54
+ readonly found: false;
55
+ readonly reason: "invalid-pointer" | "not-found";
56
+ };
57
+ /** Resolve one RFC 6901 JSON Pointer against the preserved frontmatter mapping. */
58
+ export declare const resolveKnowledgeFrontmatterPointer: (frontmatter: Readonly<Record<string, unknown>> | undefined, pointer: string) => KnowledgeFrontmatterPointerResult;
59
+ /** Build one bundle's immutable, graph-aware discovery projection. */
60
+ export declare const projectKnowledgeConcepts: (bundle: string, concepts: ReadonlyArray<KnowledgeConcept>) => ReadonlyArray<KnowledgeProjectedConcept>;
61
+ //# sourceMappingURL=knowledge-projection.d.ts.map
@@ -0,0 +1,161 @@
1
+ const pointerToken = (token) => {
2
+ if (/~(?:[^01]|$)/u.test(token))
3
+ return undefined;
4
+ return token.replace(/~1/gu, "/").replace(/~0/gu, "~");
5
+ };
6
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
7
+ /** Resolve one RFC 6901 JSON Pointer against the preserved frontmatter mapping. */
8
+ export const resolveKnowledgeFrontmatterPointer = (frontmatter, pointer) => {
9
+ if (frontmatter === undefined)
10
+ return { found: false, reason: "not-found" };
11
+ if (pointer === "")
12
+ return { found: true, value: frontmatter };
13
+ if (!pointer.startsWith("/"))
14
+ return { found: false, reason: "invalid-pointer" };
15
+ let current = frontmatter;
16
+ for (const encoded of pointer.slice(1).split("/")) {
17
+ const token = pointerToken(encoded);
18
+ if (token === undefined)
19
+ return { found: false, reason: "invalid-pointer" };
20
+ if (Array.isArray(current)) {
21
+ if (!/^(?:0|[1-9][0-9]*)$/u.test(token)) {
22
+ return { found: false, reason: "not-found" };
23
+ }
24
+ const index = Number(token);
25
+ if (!Number.isSafeInteger(index) || index >= current.length) {
26
+ return { found: false, reason: "not-found" };
27
+ }
28
+ current = current[index];
29
+ continue;
30
+ }
31
+ if (isRecord(current)) {
32
+ if (!Object.prototype.hasOwnProperty.call(current, token)) {
33
+ return { found: false, reason: "not-found" };
34
+ }
35
+ current = current[token];
36
+ continue;
37
+ }
38
+ return { found: false, reason: "not-found" };
39
+ }
40
+ return { found: true, value: current };
41
+ };
42
+ const bodyPassages = (body) => {
43
+ const lines = body.split(/\r?\n/u);
44
+ const headings = [];
45
+ const passages = [];
46
+ let passageLines = [];
47
+ let passageStart = 1;
48
+ const flush = (endLine) => {
49
+ let leading = 0;
50
+ while (passageLines[leading]?.trim().length === 0)
51
+ leading += 1;
52
+ let trailing = passageLines.length;
53
+ while (trailing > leading && passageLines[trailing - 1]?.trim().length === 0)
54
+ trailing -= 1;
55
+ if (trailing > leading) {
56
+ passages.push({
57
+ text: passageLines.slice(leading, trailing).join("\n"),
58
+ section: headings.map(({ title }) => title),
59
+ startLine: passageStart + leading,
60
+ endLine: endLine - (passageLines.length - trailing),
61
+ });
62
+ }
63
+ passageLines = [];
64
+ };
65
+ for (let index = 0; index < lines.length; index += 1) {
66
+ const line = lines[index] ?? "";
67
+ const heading = /^(#{1,6})\s+(.+?)\s*$/u.exec(line);
68
+ if (heading?.[1] !== undefined && heading[2] !== undefined) {
69
+ flush(index);
70
+ const level = heading[1].length;
71
+ while ((headings.at(-1)?.level ?? 0) >= level)
72
+ headings.pop();
73
+ headings.push({ level, title: heading[2] });
74
+ passageStart = index + 2;
75
+ continue;
76
+ }
77
+ if (passageLines.length === 0)
78
+ passageStart = index + 1;
79
+ passageLines.push(line);
80
+ }
81
+ flush(lines.length);
82
+ return passages;
83
+ };
84
+ const actorText = (actor) => actor.at === undefined ? actor.by : `${actor.by} ${actor.at}`;
85
+ const projectionBase = (bundle, concept) => {
86
+ const passages = bodyPassages(concept.body);
87
+ const units = [];
88
+ const addUnit = (field, text, passageIndex) => {
89
+ if (text === undefined || text.length === 0)
90
+ return;
91
+ units.push({ field, text, ...(passageIndex === undefined ? {} : { passageIndex }) });
92
+ };
93
+ addUnit("bundle", bundle);
94
+ addUnit("conceptId", concept.id);
95
+ addUnit("title", concept.authoredTitle);
96
+ addUnit("description", concept.description);
97
+ for (const tag of concept.tags ?? [])
98
+ addUnit("tag", tag);
99
+ addUnit("type", concept.type);
100
+ for (const [passageIndex, passage] of passages.entries()) {
101
+ addUnit("body", passage.text, passageIndex);
102
+ }
103
+ addUnit("resource", concept.resource);
104
+ addUnit("status", concept.status);
105
+ addUnit("staleAfter", concept.staleAfter);
106
+ if (concept.generated !== undefined)
107
+ addUnit("generated", actorText(concept.generated));
108
+ for (const verified of concept.verified ?? [])
109
+ addUnit("verified", actorText(verified));
110
+ addUnit("trust", concept.trust);
111
+ return {
112
+ bundle,
113
+ conceptId: concept.id,
114
+ kind: concept.kind,
115
+ ...(concept.authoredTitle === undefined ? {} : { title: concept.authoredTitle }),
116
+ ...(concept.description === undefined ? {} : { description: concept.description }),
117
+ ...(concept.tags === undefined ? {} : { tags: concept.tags }),
118
+ ...(concept.type === undefined ? {} : { type: concept.type }),
119
+ ...(concept.resource === undefined ? {} : { resource: concept.resource }),
120
+ ...(concept.status === undefined ? {} : { status: concept.status }),
121
+ ...(concept.staleAfter === undefined ? {} : { staleAfter: concept.staleAfter }),
122
+ ...(concept.generated === undefined ? {} : { generated: concept.generated }),
123
+ ...(concept.verified === undefined ? {} : { verified: concept.verified }),
124
+ ...(concept.trust === undefined ? {} : { trust: concept.trust }),
125
+ ...(concept.frontmatter === undefined ? {} : { frontmatter: concept.frontmatter }),
126
+ relativePath: concept.relativePath,
127
+ bodyPassages: passages,
128
+ searchableUnits: units,
129
+ outgoingLinks: concept.authoredLinks.map((link) => ({
130
+ ...link,
131
+ origin: "authored",
132
+ sourceConceptId: concept.id,
133
+ sourceRelativePath: concept.relativePath,
134
+ })),
135
+ };
136
+ };
137
+ /** Build one bundle's immutable, graph-aware discovery projection. */
138
+ export const projectKnowledgeConcepts = (bundle, concepts) => {
139
+ const projected = concepts.map((concept) => projectionBase(bundle, concept));
140
+ const backlinks = new Map();
141
+ for (const concept of projected) {
142
+ for (const link of concept.outgoingLinks) {
143
+ if (link.resolvedConceptId === undefined)
144
+ continue;
145
+ const current = backlinks.get(link.resolvedConceptId) ?? [];
146
+ current.push({
147
+ origin: "derived-backlink",
148
+ sourceConceptId: concept.conceptId,
149
+ sourceRelativePath: concept.relativePath,
150
+ targetConceptId: link.resolvedConceptId,
151
+ line: link.line,
152
+ });
153
+ backlinks.set(link.resolvedConceptId, current);
154
+ }
155
+ }
156
+ return projected.map((concept) => ({
157
+ ...concept,
158
+ backlinks: [...(backlinks.get(concept.conceptId) ?? [])].sort((left, right) => left.sourceConceptId.localeCompare(right.sourceConceptId) || left.line - right.line),
159
+ }));
160
+ };
161
+ //# sourceMappingURL=knowledge-projection.js.map