@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.
@@ -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
+ }