@isparling/engram-cli 0.1.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/LICENSE +187 -0
- package/README.md +41 -0
- package/bin/engram +14 -0
- package/package.json +32 -0
- package/release/engram-release.ts +1493 -0
- package/src/atomicWrite.ts +80 -0
- package/src/candidate.ts +229 -0
- package/src/classify.ts +275 -0
- package/src/cli.ts +709 -0
- package/src/contentHash.ts +54 -0
- package/src/deepFreeze.ts +13 -0
- package/src/diff.ts +81 -0
- package/src/guardedRetrieval.ts +128 -0
- package/src/guardedRetrievalInternal.ts +321 -0
- package/src/knowledgeRecord.ts +256 -0
- package/src/knowledgeRetrieval.ts +564 -0
- package/src/knowledgeRollup.ts +479 -0
- package/src/knowledgeTransaction.ts +683 -0
- package/src/knowledgeTypes.ts +249 -0
- package/src/knowledgeValidation.ts +269 -0
- package/src/markdownRecord.ts +265 -0
- package/src/packLoader.ts +188 -0
- package/src/packTypes.ts +12 -0
- package/src/presentation.ts +673 -0
- package/src/qmdConfigGuard.ts +245 -0
- package/src/qmdRunner.ts +392 -0
- package/src/realPath.ts +47 -0
- package/src/spaceBinding.ts +37 -0
- package/src/spaceRegistry.ts +1139 -0
- package/src/submit.ts +228 -0
- package/src/symlinkGuard.ts +71 -0
- package/src/transactionLock.ts +188 -0
- package/src/types.ts +38 -0
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
import { readdir, readFile, realpath, stat } from "node:fs/promises";
|
|
2
|
+
import { basename, posix, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { runQmd, type SpawnFn } from "./qmdRunner.ts";
|
|
4
|
+
import { parseKnowledgeRecord } from "./knowledgeRecord.ts";
|
|
5
|
+
import type { ActiveSpace } from "./spaceRegistry.ts";
|
|
6
|
+
import type { KnowledgeError, KnowledgeRecord, RelatedKnowledgeRecord } from "./knowledgeTypes.ts";
|
|
7
|
+
import { deepFreeze } from "./deepFreeze.ts";
|
|
8
|
+
|
|
9
|
+
export type RetrievalExposedResult = {
|
|
10
|
+
recordId: string;
|
|
11
|
+
sourceUri: string;
|
|
12
|
+
relativePath: string;
|
|
13
|
+
sourceClasses: string[];
|
|
14
|
+
score?: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// What audience-authorization filtering withheld from this retrieval. It
|
|
18
|
+
// deliberately carries no record identities, only a count: an identity would
|
|
19
|
+
// let a caller enumerate specific restricted content by name, where a count
|
|
20
|
+
// only supports probabilistic inference over many queries.
|
|
21
|
+
//
|
|
22
|
+
// This field is INTENDED as the machine operator's audit trail. It is not yet
|
|
23
|
+
// confined to one: `cli.ts` prints the entire result, receipt included, to the
|
|
24
|
+
// requesting caller, so today the count reaches the same channel as the
|
|
25
|
+
// records. The current disclosure is intentional: a count is a weak oracle
|
|
26
|
+
// and no current space holds records whose mere existence is sensitive.
|
|
27
|
+
//
|
|
28
|
+
// The revisit trigger is exactly that condition. When a space does hold such
|
|
29
|
+
// records, this must become operator-only, and the intended shape of that
|
|
30
|
+
// change is a redaction at the caller-facing boundary — the same place
|
|
31
|
+
// safeForModel already redacts — leaving this type untouched. Keep any
|
|
32
|
+
// audience-facing disclosure structurally separate from this field so that
|
|
33
|
+
// change stays a policy edit and never becomes a schema migration.
|
|
34
|
+
export type RetrievalWithheld = {
|
|
35
|
+
audienceId: string | null;
|
|
36
|
+
count: number;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type RetrievalReceipt = {
|
|
40
|
+
schemaVersion: 0;
|
|
41
|
+
// "search" retrievals ran a qmd query; "space" retrievals enumerated the
|
|
42
|
+
// records root directly and ran no qmd process at all. `query` and
|
|
43
|
+
// `relevanceThreshold` are only meaningful for "search" — see below.
|
|
44
|
+
scope: "search" | "space";
|
|
45
|
+
// null for an enumerated ("space") retrieval: no query ran, and writing
|
|
46
|
+
// anything else here (the view id, an empty string standing in for "none")
|
|
47
|
+
// would be a fabricated query, the same defect as the fabricated space id
|
|
48
|
+
// already refused for UnresolvedRetrievalReceipt.
|
|
49
|
+
query: string | null;
|
|
50
|
+
activeSpace: string;
|
|
51
|
+
collection: string;
|
|
52
|
+
requestedSourceClasses: string[];
|
|
53
|
+
allowedSourceClasses: string[];
|
|
54
|
+
kind: "hit" | "miss";
|
|
55
|
+
locatorUris: string[];
|
|
56
|
+
recordIds: string[];
|
|
57
|
+
exposedResults: RetrievalExposedResult[];
|
|
58
|
+
// The relevance threshold actually applied to rank candidates, or null when
|
|
59
|
+
// none was. Enumeration never ranks, so an enumerated receipt always
|
|
60
|
+
// reports null here even when the pack's policy declares a threshold:
|
|
61
|
+
// reporting the declared threshold would claim a filter that never ran.
|
|
62
|
+
relevanceThreshold: number | null;
|
|
63
|
+
withheld: RetrievalWithheld;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type RetrievalFailure = {
|
|
67
|
+
kind: "failure";
|
|
68
|
+
errors: KnowledgeError[];
|
|
69
|
+
receipt: RetrievalReceipt;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type RetrievalMiss = {
|
|
73
|
+
kind: "miss";
|
|
74
|
+
records: [];
|
|
75
|
+
receipt: RetrievalReceipt;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type RetrievalHit = {
|
|
79
|
+
kind: "hit";
|
|
80
|
+
records: RelatedKnowledgeRecord[];
|
|
81
|
+
receipt: RetrievalReceipt;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export type RetrievalOutcome = RetrievalFailure | RetrievalMiss | RetrievalHit;
|
|
85
|
+
|
|
86
|
+
function retrievalError(code: string, message: string, field?: string): KnowledgeError {
|
|
87
|
+
return field === undefined
|
|
88
|
+
? { kind: "retrieval", code, message }
|
|
89
|
+
: { kind: "retrieval", code, field, message };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function safeForModel(error: KnowledgeError, guarded: boolean): KnowledgeError {
|
|
93
|
+
if (!guarded) return error;
|
|
94
|
+
return {
|
|
95
|
+
...error,
|
|
96
|
+
message: `guarded retrieval rejected qmd output (${error.code})`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function emptyReceipt(
|
|
101
|
+
binding: ActiveSpace,
|
|
102
|
+
query: string | null,
|
|
103
|
+
kind: "hit" | "miss" = "miss",
|
|
104
|
+
scope: "search" | "space" = "search",
|
|
105
|
+
): RetrievalReceipt {
|
|
106
|
+
return {
|
|
107
|
+
schemaVersion: 0,
|
|
108
|
+
scope,
|
|
109
|
+
query,
|
|
110
|
+
activeSpace: binding.spaceId,
|
|
111
|
+
collection: binding.qmdCollectionName,
|
|
112
|
+
requestedSourceClasses: [],
|
|
113
|
+
allowedSourceClasses: [],
|
|
114
|
+
kind,
|
|
115
|
+
locatorUris: [],
|
|
116
|
+
recordIds: [],
|
|
117
|
+
exposedResults: [],
|
|
118
|
+
relevanceThreshold: null,
|
|
119
|
+
withheld: { audienceId: null, count: 0 },
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type GuardedRetrievalFilter = {
|
|
124
|
+
audienceId: string;
|
|
125
|
+
requestedSourceClasses: readonly string[];
|
|
126
|
+
allowedSourceClasses: readonly string[];
|
|
127
|
+
includePresentations: false;
|
|
128
|
+
relevanceThreshold: number | null;
|
|
129
|
+
classifySource: (source: KnowledgeRecord["sources"][number]) => string;
|
|
130
|
+
isEligible: (record: KnowledgeRecord) => boolean;
|
|
131
|
+
authorize: (record: KnowledgeRecord) => boolean;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// A candidate Markdown locator awaiting the guard sequence below. `score`
|
|
135
|
+
// is qmd's ranking signal; enumeration never ranks, so its candidates never
|
|
136
|
+
// carry one.
|
|
137
|
+
export type RetrievalCandidate = {
|
|
138
|
+
file: string;
|
|
139
|
+
score?: number;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
143
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isMissing(error: unknown): boolean {
|
|
147
|
+
return error instanceof Error && error.message.includes("ENOENT");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function safeRelativeMarkdownPath(uri: string, collection: string): string | { error: KnowledgeError } {
|
|
151
|
+
const prefix = `qmd://${collection}/`;
|
|
152
|
+
if (!uri.startsWith(prefix)) return { error: retrievalError("foreign_locator", `qmd locator does not name the active collection: ${JSON.stringify(uri)}`, "file") };
|
|
153
|
+
const relativePath = uri.slice(prefix.length);
|
|
154
|
+
if (
|
|
155
|
+
relativePath.length === 0 ||
|
|
156
|
+
relativePath.includes("\\") ||
|
|
157
|
+
relativePath.includes("\u0000") ||
|
|
158
|
+
relativePath.includes("%") ||
|
|
159
|
+
relativePath.startsWith("/") ||
|
|
160
|
+
!relativePath.endsWith(".md")
|
|
161
|
+
) {
|
|
162
|
+
return { error: retrievalError("locator_invalid", `qmd locator is not a safe relative Markdown path: ${JSON.stringify(uri)}`, "file") };
|
|
163
|
+
}
|
|
164
|
+
const parts = relativePath.split("/");
|
|
165
|
+
if (parts.some((part) => part.length === 0 || part === "." || part === "..")) {
|
|
166
|
+
return { error: retrievalError("locator_escape", `qmd locator contains an unsafe path segment: ${JSON.stringify(uri)}`, "file") };
|
|
167
|
+
}
|
|
168
|
+
if (posix.normalize(relativePath) !== relativePath || posix.isAbsolute(relativePath)) {
|
|
169
|
+
return { error: retrievalError("locator_escape", `qmd locator is not normalized and relative: ${JSON.stringify(uri)}`, "file") };
|
|
170
|
+
}
|
|
171
|
+
return relativePath;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function readLocatedRecord(
|
|
175
|
+
binding: ActiveSpace,
|
|
176
|
+
sourceUri: string,
|
|
177
|
+
relativePath: string,
|
|
178
|
+
): Promise<{ kind: "record"; value: RelatedKnowledgeRecord } | { kind: "miss" } | { kind: "failure"; error: KnowledgeError }> {
|
|
179
|
+
let root: string;
|
|
180
|
+
try {
|
|
181
|
+
root = await realpath(binding.recordsRoot);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (isMissing(error)) return { kind: "miss" };
|
|
184
|
+
return { kind: "failure", error: retrievalError("records_root_unavailable", `active records root could not be resolved: ${error instanceof Error ? error.message : String(error)}`, "file") };
|
|
185
|
+
}
|
|
186
|
+
const targetPath = resolve(root, ...relativePath.split("/"));
|
|
187
|
+
const rootWithSep = root.endsWith(sep) ? root : `${root}${sep}`;
|
|
188
|
+
if (targetPath !== root && !targetPath.startsWith(rootWithSep)) {
|
|
189
|
+
return { kind: "failure", error: retrievalError("path_escape", `locator resolves outside the active records root: ${sourceUri}`, "file") };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let resolvedTarget: string;
|
|
193
|
+
try {
|
|
194
|
+
resolvedTarget = await realpath(targetPath);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (isMissing(error)) return { kind: "miss" };
|
|
197
|
+
return { kind: "failure", error: retrievalError("record_read_failed", `failed to resolve current Markdown for ${sourceUri}: ${error instanceof Error ? error.message : String(error)}`, "file") };
|
|
198
|
+
}
|
|
199
|
+
if (resolvedTarget !== root && !resolvedTarget.startsWith(rootWithSep)) {
|
|
200
|
+
return { kind: "failure", error: retrievalError("path_escape", `current Markdown path escapes the active records root: ${sourceUri}`, "file") };
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
if (!(await stat(resolvedTarget)).isFile()) {
|
|
204
|
+
return { kind: "failure", error: retrievalError("record_shape_invalid", `qmd locator does not name a regular Markdown file: ${sourceUri}`, "file") };
|
|
205
|
+
}
|
|
206
|
+
const text = await readFile(resolvedTarget, "utf8");
|
|
207
|
+
const parsed = parseKnowledgeRecord(text);
|
|
208
|
+
if (!parsed.ok) {
|
|
209
|
+
return { kind: "failure", error: retrievalError("current_markdown_invalid", `current Markdown for ${sourceUri} is invalid: ${parsed.errors.map((item) => item.message).join("; ")}`, "file") };
|
|
210
|
+
}
|
|
211
|
+
const expectedId = basename(relativePath, ".md");
|
|
212
|
+
if (parsed.value.id !== expectedId) {
|
|
213
|
+
return { kind: "failure", error: retrievalError("record_identity_mismatch", `current Markdown id ${parsed.value.id} does not match locator ${relativePath}`, "file") };
|
|
214
|
+
}
|
|
215
|
+
if (parsed.value.scope.space !== binding.spaceId) {
|
|
216
|
+
return { kind: "failure", error: retrievalError("scope_space_mismatch", `current Markdown for ${sourceUri} belongs to another space`, "scope.space") };
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
kind: "record",
|
|
220
|
+
value: { record: deepFreeze(parsed.value), relativePath, sourceUri },
|
|
221
|
+
};
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (isMissing(error)) return { kind: "miss" };
|
|
224
|
+
return { kind: "failure", error: retrievalError("record_read_failed", `failed to read current Markdown for ${sourceUri}: ${error instanceof Error ? error.message : String(error)}`, "file") };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
type CandidateFilterOutcome =
|
|
229
|
+
| { kind: "failure"; error: KnowledgeError; withheldCount: number }
|
|
230
|
+
| {
|
|
231
|
+
kind: "complete";
|
|
232
|
+
records: RelatedKnowledgeRecord[];
|
|
233
|
+
locatorUris: string[];
|
|
234
|
+
recordIds: string[];
|
|
235
|
+
exposedResults: RetrievalExposedResult[];
|
|
236
|
+
withheldCount: number;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// The guard sequence every candidate locator passes through regardless of
|
|
240
|
+
// where it came from: containment (`safeRelativeMarkdownPath` /
|
|
241
|
+
// `readLocatedRecord`), source class, presentation exclusion, relevance,
|
|
242
|
+
// eligibility, authorization, and withheld-count bookkeeping. A search
|
|
243
|
+
// candidate carries a qmd score; an enumerated candidate never does, so
|
|
244
|
+
// `applyRelevanceThreshold` lets the caller say whether a score-bearing
|
|
245
|
+
// threshold check is even meaningful for this batch — enumeration passes
|
|
246
|
+
// `false` so an unscored candidate is never mistaken for one that failed
|
|
247
|
+
// to rank, which is a distinct thing from one that was never ranked at all.
|
|
248
|
+
async function filterCandidates(
|
|
249
|
+
binding: ActiveSpace,
|
|
250
|
+
candidates: readonly RetrievalCandidate[],
|
|
251
|
+
filter: GuardedRetrievalFilter | undefined,
|
|
252
|
+
applyRelevanceThreshold: boolean,
|
|
253
|
+
): Promise<CandidateFilterOutcome> {
|
|
254
|
+
const records: RelatedKnowledgeRecord[] = [];
|
|
255
|
+
const locatorUris: string[] = [];
|
|
256
|
+
const recordIds: string[] = [];
|
|
257
|
+
const exposedResults: RetrievalExposedResult[] = [];
|
|
258
|
+
const seenPaths = new Set<string>();
|
|
259
|
+
let withheldCount = 0;
|
|
260
|
+
|
|
261
|
+
for (const candidate of candidates) {
|
|
262
|
+
const safePath = safeRelativeMarkdownPath(candidate.file, binding.qmdCollectionName);
|
|
263
|
+
if (typeof safePath !== "string") {
|
|
264
|
+
return { kind: "failure", error: safeForModel(safePath.error, filter !== undefined), withheldCount };
|
|
265
|
+
}
|
|
266
|
+
if (seenPaths.has(safePath)) continue;
|
|
267
|
+
seenPaths.add(safePath);
|
|
268
|
+
const located = await readLocatedRecord(binding, candidate.file, safePath);
|
|
269
|
+
if (located.kind === "failure") {
|
|
270
|
+
return { kind: "failure", error: safeForModel(located.error, filter !== undefined), withheldCount };
|
|
271
|
+
}
|
|
272
|
+
if (located.kind === "miss") continue;
|
|
273
|
+
|
|
274
|
+
if (filter !== undefined) {
|
|
275
|
+
let sourceClasses: string[];
|
|
276
|
+
try {
|
|
277
|
+
sourceClasses = located.value.record.sources.map((source) => filter.classifySource(source));
|
|
278
|
+
} catch (error) {
|
|
279
|
+
return {
|
|
280
|
+
kind: "failure",
|
|
281
|
+
error: safeForModel(retrievalError(
|
|
282
|
+
"source_classification_failed",
|
|
283
|
+
`source classification policy failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
284
|
+
), true),
|
|
285
|
+
withheldCount,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
const sourceClassesAllowed = sourceClasses.length > 0 && sourceClasses.every(
|
|
289
|
+
(sourceClass) => filter.allowedSourceClasses.includes(sourceClass) && filter.requestedSourceClasses.includes(sourceClass),
|
|
290
|
+
);
|
|
291
|
+
if (!sourceClassesAllowed) continue;
|
|
292
|
+
if (!filter.includePresentations && sourceClasses.includes("presentation")) continue;
|
|
293
|
+
if (
|
|
294
|
+
applyRelevanceThreshold &&
|
|
295
|
+
filter.relevanceThreshold !== null &&
|
|
296
|
+
(candidate.score === undefined || candidate.score < filter.relevanceThreshold)
|
|
297
|
+
) continue;
|
|
298
|
+
let eligible: boolean;
|
|
299
|
+
try {
|
|
300
|
+
eligible = filter.isEligible(located.value.record);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
return {
|
|
303
|
+
kind: "failure",
|
|
304
|
+
error: safeForModel(retrievalError(
|
|
305
|
+
"eligibility_policy_failed",
|
|
306
|
+
`retrieval eligibility policy failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
307
|
+
), true),
|
|
308
|
+
withheldCount,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
if (!eligible) continue;
|
|
312
|
+
let authorized: boolean;
|
|
313
|
+
try {
|
|
314
|
+
authorized = filter.authorize(located.value.record);
|
|
315
|
+
} catch (error) {
|
|
316
|
+
return {
|
|
317
|
+
kind: "failure",
|
|
318
|
+
error: safeForModel(retrievalError(
|
|
319
|
+
"authorization_policy_failed",
|
|
320
|
+
`retrieval authorization policy failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
321
|
+
), true),
|
|
322
|
+
withheldCount,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
// Authorization filters, like every other policy check above: an
|
|
326
|
+
// unauthorized record is withheld and the loop continues, exactly as
|
|
327
|
+
// source class, presentation-exclusion, relevance, and eligibility do.
|
|
328
|
+
// It must never fail the whole request — a request-wide denial would
|
|
329
|
+
// both destroy a render over an otherwise-authorized result set, and
|
|
330
|
+
// let a caller who can never read restricted content learn whether
|
|
331
|
+
// restricted content matched their query purely from outcome status.
|
|
332
|
+
// The omission is recorded (see RetrievalWithheld above), not silent.
|
|
333
|
+
if (typeof authorized !== "boolean") {
|
|
334
|
+
return {
|
|
335
|
+
kind: "failure",
|
|
336
|
+
error: safeForModel(retrievalError(
|
|
337
|
+
"authorization_policy_invalid",
|
|
338
|
+
"retrieval authorization policy must return a boolean",
|
|
339
|
+
), true),
|
|
340
|
+
withheldCount,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
if (authorized !== true) {
|
|
344
|
+
withheldCount++;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
exposedResults.push({
|
|
348
|
+
recordId: located.value.record.id,
|
|
349
|
+
sourceUri: candidate.file,
|
|
350
|
+
relativePath: safePath,
|
|
351
|
+
sourceClasses,
|
|
352
|
+
...(candidate.score === undefined ? {} : { score: candidate.score }),
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
locatorUris.push(candidate.file);
|
|
356
|
+
recordIds.push(located.value.record.id);
|
|
357
|
+
records.push(located.value);
|
|
358
|
+
}
|
|
359
|
+
return { kind: "complete", records, locatorUris, recordIds, exposedResults, withheldCount };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function withheldReceipt(base: RetrievalReceipt, count: number): RetrievalReceipt {
|
|
363
|
+
return count === 0 ? base : { ...base, withheld: { ...base.withheld, count } };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Interprets qmd's raw JSON hits as candidate locators. This is the part of
|
|
367
|
+
// retrieval that is inherently search-specific: it is the only place that
|
|
368
|
+
// trusts qmd's untyped output shape at all. Once a hit is validated it
|
|
369
|
+
// becomes a plain `{ file, score? }` candidate indistinguishable from one
|
|
370
|
+
// enumeration would have produced.
|
|
371
|
+
function validateSearchHits(parsed: unknown[], guarded: boolean): { kind: "candidates"; candidates: RetrievalCandidate[] } | { kind: "failure"; error: KnowledgeError } {
|
|
372
|
+
const candidates: RetrievalCandidate[] = [];
|
|
373
|
+
for (let index = 0; index < parsed.length; index++) {
|
|
374
|
+
const hit = parsed[index];
|
|
375
|
+
if (!isObject(hit) || typeof hit.file !== "string") {
|
|
376
|
+
return { kind: "failure", error: safeForModel(retrievalError("qmd_shape_invalid", `qmd search hit ${index} must contain a file locator`, `hits[${index}].file`), guarded) };
|
|
377
|
+
}
|
|
378
|
+
let score: number | undefined;
|
|
379
|
+
if (guarded && hit.score !== undefined) {
|
|
380
|
+
if (typeof hit.score !== "number" || !Number.isFinite(hit.score)) {
|
|
381
|
+
return { kind: "failure", error: safeForModel(retrievalError("qmd_shape_invalid", `qmd search hit ${index} score must be a finite number`, `hits[${index}].score`), true) };
|
|
382
|
+
}
|
|
383
|
+
score = hit.score;
|
|
384
|
+
}
|
|
385
|
+
candidates.push(score === undefined ? { file: hit.file } : { file: hit.file, score });
|
|
386
|
+
}
|
|
387
|
+
return { kind: "candidates", candidates };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Reads the space's records root directly, sorted by filename for
|
|
391
|
+
// determinism, and synthesizes each entry into the same `qmd://<collection>/
|
|
392
|
+
// <name>.md` locator form a search hit would carry — so
|
|
393
|
+
// `safeRelativeMarkdownPath` containment applies identically and a symlink
|
|
394
|
+
// escaping the root is caught by the same realpath check `readLocatedRecord`
|
|
395
|
+
// already runs for search. Runs no qmd subprocess. A symlink entry is
|
|
396
|
+
// included as a candidate rather than filtered out here, precisely so its
|
|
397
|
+
// escape is caught by that shared guard rather than silently skipped.
|
|
398
|
+
async function enumerateCandidates(binding: ActiveSpace): Promise<{ kind: "candidates"; candidates: RetrievalCandidate[] } | { kind: "failure"; error: KnowledgeError }> {
|
|
399
|
+
let entries;
|
|
400
|
+
try {
|
|
401
|
+
entries = await readdir(binding.recordsRoot, { withFileTypes: true });
|
|
402
|
+
} catch (error) {
|
|
403
|
+
if (isMissing(error)) return { kind: "candidates", candidates: [] };
|
|
404
|
+
return { kind: "failure", error: retrievalError("records_root_unavailable", `active records root could not be listed: ${error instanceof Error ? error.message : String(error)}`, "file") };
|
|
405
|
+
}
|
|
406
|
+
const names = entries
|
|
407
|
+
.filter((entry) => (entry.isFile() || entry.isSymbolicLink()) && entry.name.endsWith(".md"))
|
|
408
|
+
.map((entry) => entry.name)
|
|
409
|
+
.sort((left, right) => left.localeCompare(right));
|
|
410
|
+
return { kind: "candidates", candidates: names.map((name) => ({ file: `qmd://${binding.qmdCollectionName}/${name}` })) };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export async function retrieveRelatedRecords(
|
|
414
|
+
binding: ActiveSpace,
|
|
415
|
+
query: string,
|
|
416
|
+
spawnFn?: SpawnFn,
|
|
417
|
+
): Promise<RetrievalOutcome> {
|
|
418
|
+
return retrieveRecords(binding, query, undefined, spawnFn);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export async function retrieveGuardedRecords(
|
|
422
|
+
binding: ActiveSpace,
|
|
423
|
+
query: string,
|
|
424
|
+
filter: GuardedRetrievalFilter,
|
|
425
|
+
spawnFn?: SpawnFn,
|
|
426
|
+
): Promise<RetrievalOutcome> {
|
|
427
|
+
return retrieveRecords(binding, query, filter, spawnFn);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Enumerates every Markdown record under the space's records root instead of
|
|
431
|
+
// running a qmd search, then feeds the SAME candidate-filtering guard
|
|
432
|
+
// sequence `retrieveGuardedRecords` uses. A profile view is an enumeration;
|
|
433
|
+
// guarded retrieval over a ranked search is not the same operation, and no
|
|
434
|
+
// query string can stand in for "every active record".
|
|
435
|
+
export async function retrieveEnumeratedRecords(
|
|
436
|
+
binding: ActiveSpace,
|
|
437
|
+
filter: GuardedRetrievalFilter,
|
|
438
|
+
): Promise<RetrievalOutcome> {
|
|
439
|
+
const policyReceipt: RetrievalReceipt = {
|
|
440
|
+
...emptyReceipt(binding, null, "miss", "space"),
|
|
441
|
+
requestedSourceClasses: [...filter.requestedSourceClasses],
|
|
442
|
+
allowedSourceClasses: [...filter.allowedSourceClasses],
|
|
443
|
+
// Enumeration never ranks, so a threshold the pack's policy declares for
|
|
444
|
+
// its search-scoped views never applied to this result set. Reporting it
|
|
445
|
+
// here would claim a filter that did not run.
|
|
446
|
+
relevanceThreshold: null,
|
|
447
|
+
withheld: { audienceId: filter.audienceId, count: 0 },
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const enumerated = await enumerateCandidates(binding);
|
|
451
|
+
if (enumerated.kind === "failure") {
|
|
452
|
+
return { kind: "failure", errors: [safeForModel(enumerated.error, true)], receipt: policyReceipt };
|
|
453
|
+
}
|
|
454
|
+
if (enumerated.candidates.length === 0) return { kind: "miss", records: [], receipt: policyReceipt };
|
|
455
|
+
|
|
456
|
+
const filtered = await filterCandidates(binding, enumerated.candidates, filter, false);
|
|
457
|
+
if (filtered.kind === "failure") {
|
|
458
|
+
return { kind: "failure", errors: [filtered.error], receipt: withheldReceipt(policyReceipt, filtered.withheldCount) };
|
|
459
|
+
}
|
|
460
|
+
const receiptWithWithheld = withheldReceipt(policyReceipt, filtered.withheldCount);
|
|
461
|
+
if (filtered.records.length === 0) {
|
|
462
|
+
return {
|
|
463
|
+
kind: "miss",
|
|
464
|
+
records: [],
|
|
465
|
+
receipt: { ...receiptWithWithheld, locatorUris: filtered.locatorUris, recordIds: filtered.recordIds, exposedResults: filtered.exposedResults },
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
kind: "hit",
|
|
470
|
+
records: filtered.records,
|
|
471
|
+
receipt: { ...receiptWithWithheld, kind: "hit", locatorUris: filtered.locatorUris, recordIds: filtered.recordIds, exposedResults: filtered.exposedResults },
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function retrieveRecords(
|
|
476
|
+
binding: ActiveSpace,
|
|
477
|
+
query: string,
|
|
478
|
+
filter: GuardedRetrievalFilter | undefined,
|
|
479
|
+
spawnFn?: SpawnFn,
|
|
480
|
+
): Promise<RetrievalOutcome> {
|
|
481
|
+
const baseReceipt = emptyReceipt(binding, query);
|
|
482
|
+
const policyReceipt: RetrievalReceipt = filter === undefined
|
|
483
|
+
? baseReceipt
|
|
484
|
+
: {
|
|
485
|
+
...baseReceipt,
|
|
486
|
+
requestedSourceClasses: [...filter.requestedSourceClasses],
|
|
487
|
+
allowedSourceClasses: [...filter.allowedSourceClasses],
|
|
488
|
+
relevanceThreshold: filter.relevanceThreshold,
|
|
489
|
+
withheld: { audienceId: filter.audienceId, count: 0 },
|
|
490
|
+
};
|
|
491
|
+
const execution = await runQmd(
|
|
492
|
+
["search", query, "--json", "-c", binding.qmdCollectionName],
|
|
493
|
+
binding,
|
|
494
|
+
spawnFn,
|
|
495
|
+
);
|
|
496
|
+
if (!execution.ranProcess) {
|
|
497
|
+
return {
|
|
498
|
+
kind: "failure",
|
|
499
|
+
errors: [retrievalError(
|
|
500
|
+
"qmd_not_run",
|
|
501
|
+
filter === undefined ? `qmd search did not start: ${execution.stderr.trim().slice(0, 500)}` : "qmd search did not start",
|
|
502
|
+
)],
|
|
503
|
+
receipt: policyReceipt,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
if (execution.code !== 0) {
|
|
507
|
+
return {
|
|
508
|
+
kind: "failure",
|
|
509
|
+
errors: [retrievalError(
|
|
510
|
+
"qmd_exit",
|
|
511
|
+
filter === undefined ? `qmd search exited with code ${execution.code}: ${execution.stderr.trim().slice(0, 500)}` : `qmd search exited with code ${execution.code}`,
|
|
512
|
+
)],
|
|
513
|
+
receipt: policyReceipt,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
if (execution.stdout === "No results found.\n") return { kind: "miss", records: [], receipt: policyReceipt };
|
|
517
|
+
|
|
518
|
+
let parsed: unknown;
|
|
519
|
+
try {
|
|
520
|
+
parsed = JSON.parse(execution.stdout);
|
|
521
|
+
} catch (error) {
|
|
522
|
+
return {
|
|
523
|
+
kind: "failure",
|
|
524
|
+
errors: [retrievalError(
|
|
525
|
+
"qmd_output_malformed",
|
|
526
|
+
filter === undefined
|
|
527
|
+
? `qmd search returned malformed JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
528
|
+
: "qmd search returned malformed JSON",
|
|
529
|
+
)],
|
|
530
|
+
receipt: policyReceipt,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
if (!Array.isArray(parsed)) {
|
|
534
|
+
return {
|
|
535
|
+
kind: "failure",
|
|
536
|
+
errors: [retrievalError("qmd_shape_invalid", "qmd search JSON result must be an array")],
|
|
537
|
+
receipt: policyReceipt,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
if (parsed.length === 0) return { kind: "miss", records: [], receipt: policyReceipt };
|
|
541
|
+
|
|
542
|
+
const validated = validateSearchHits(parsed, filter !== undefined);
|
|
543
|
+
if (validated.kind === "failure") {
|
|
544
|
+
return { kind: "failure", errors: [validated.error], receipt: policyReceipt };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const filtered = await filterCandidates(binding, validated.candidates, filter, true);
|
|
548
|
+
if (filtered.kind === "failure") {
|
|
549
|
+
return { kind: "failure", errors: [filtered.error], receipt: withheldReceipt(policyReceipt, filtered.withheldCount) };
|
|
550
|
+
}
|
|
551
|
+
const receiptWithWithheld = withheldReceipt(policyReceipt, filtered.withheldCount);
|
|
552
|
+
if (filtered.records.length === 0) {
|
|
553
|
+
return {
|
|
554
|
+
kind: "miss",
|
|
555
|
+
records: [],
|
|
556
|
+
receipt: { ...receiptWithWithheld, locatorUris: filtered.locatorUris, recordIds: filtered.recordIds, exposedResults: filtered.exposedResults },
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
kind: "hit",
|
|
561
|
+
records: filtered.records,
|
|
562
|
+
receipt: { ...receiptWithWithheld, kind: "hit", locatorUris: filtered.locatorUris, recordIds: filtered.recordIds, exposedResults: filtered.exposedResults },
|
|
563
|
+
};
|
|
564
|
+
}
|