@avantf/dsh-mem 0.6.4 → 0.8.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.
Files changed (150) hide show
  1. package/README.md +19 -10
  2. package/lib/client.js +1665 -3466
  3. package/lib/dsh-build.json +6 -5
  4. package/lib/engine/graph-recall/in-memory.d.ts +93 -0
  5. package/lib/engine/graph-recall/index.d.ts +22 -0
  6. package/lib/engine/graph-recall/traverse.d.ts +119 -0
  7. package/lib/engine/graph-recall/types.d.ts +188 -0
  8. package/lib/engine/mem/config/config_files.d.ts +1 -1
  9. package/lib/engine/mem/config/loader.d.ts +2 -3
  10. package/lib/engine/mem/config/paths.d.ts +2 -13
  11. package/lib/engine/mem/db/dao/contradictions.d.ts +4 -22
  12. package/lib/engine/mem/db/dao/entities.d.ts +80 -7
  13. package/lib/engine/mem/db/dao/fact_sources.d.ts +3 -13
  14. package/lib/engine/mem/db/dao/facts.d.ts +311 -56
  15. package/lib/engine/mem/db/dao/triples.d.ts +1 -26
  16. package/lib/engine/mem/db/graph.d.ts +249 -0
  17. package/lib/engine/mem/db/journal_notes.d.ts +35 -0
  18. package/lib/engine/mem/db/migrations.d.ts +9 -0
  19. package/lib/engine/mem/db/port.d.ts +22 -3
  20. package/lib/engine/mem/db/schema.d.ts +119 -0
  21. package/lib/engine/mem/db/tokenizer.d.ts +9 -0
  22. package/lib/engine/mem/dbdrivers/fake/driver.d.ts +75 -0
  23. package/lib/engine/mem/dbdrivers/fake/index.d.ts +7 -0
  24. package/lib/engine/mem/dbdrivers/fake/surface.d.ts +34 -0
  25. package/lib/engine/mem/dbdrivers/fake/transport.d.ts +61 -0
  26. package/lib/engine/mem/dbdrivers/sqlite/connection.d.ts +14 -0
  27. package/lib/engine/mem/dbdrivers/sqlite/contradictions.d.ts +35 -0
  28. package/lib/engine/mem/dbdrivers/sqlite/dao/index.d.ts +16 -0
  29. package/lib/engine/mem/dbdrivers/sqlite/driver.d.ts +54 -0
  30. package/lib/engine/mem/dbdrivers/sqlite/facts.d.ts +117 -0
  31. package/lib/engine/mem/dbdrivers/sqlite/index.d.ts +6 -0
  32. package/lib/engine/mem/dbdrivers/sqlite/migrations.d.ts +15 -0
  33. package/lib/engine/mem/dbdrivers/sqlite/relations.d.ts +62 -0
  34. package/lib/engine/mem/dbdrivers/sqlite/search.d.ts +56 -0
  35. package/lib/engine/mem/dbdrivers/sqlite/sources.d.ts +37 -0
  36. package/lib/engine/mem/defaults.d.ts +3 -4
  37. package/lib/engine/mem/dispatch.d.ts +7 -32
  38. package/lib/engine/mem/entities/extract.d.ts +44 -47
  39. package/lib/engine/mem/eval/derived_runner.d.ts +6 -1
  40. package/lib/engine/mem/eval/relation_gold.d.ts +251 -0
  41. package/lib/engine/mem/eval/write_metrics.d.ts +1 -1
  42. package/lib/engine/mem/facts_io.d.ts +328 -0
  43. package/lib/engine/mem/index.d.ts +10 -2
  44. package/lib/engine/mem/interfaces/capabilities.d.ts +30 -0
  45. package/lib/engine/mem/interfaces/common.d.ts +11 -0
  46. package/lib/engine/mem/interfaces/errors.d.ts +65 -0
  47. package/lib/engine/mem/interfaces/index.d.ts +18 -0
  48. package/lib/engine/mem/interfaces/migrations.d.ts +21 -0
  49. package/lib/engine/mem/interfaces/repositories.d.ts +471 -0
  50. package/lib/engine/mem/interfaces/search_index.d.ts +113 -0
  51. package/lib/engine/mem/interfaces/storage_backend.d.ts +84 -0
  52. package/lib/engine/mem/interfaces/storage_driver.d.ts +43 -0
  53. package/lib/engine/mem/interfaces/version.d.ts +34 -0
  54. package/lib/engine/mem/lifecycle/contradiction.d.ts +2 -2
  55. package/lib/engine/mem/maintenance_paths.d.ts +41 -0
  56. package/lib/engine/mem/modelBootstrap.d.ts +1 -1
  57. package/lib/engine/mem/relations/confidence.d.ts +39 -0
  58. package/lib/engine/mem/relations/extract.d.ts +128 -0
  59. package/lib/engine/mem/relations/index.d.ts +32 -0
  60. package/lib/engine/mem/relations/name-quality.d.ts +78 -0
  61. package/lib/engine/mem/relations/normalize.d.ts +275 -0
  62. package/lib/engine/mem/relations/patterns.d.ts +67 -0
  63. package/lib/engine/mem/relations/statement.d.ts +88 -0
  64. package/lib/engine/mem/relations/types.d.ts +79 -0
  65. package/lib/engine/mem/runtime.d.ts +117 -90
  66. package/lib/engine/mem/storage/create.d.ts +42 -0
  67. package/lib/engine/mem/storage/errors.d.ts +65 -0
  68. package/lib/engine/mem/storage/index.d.ts +15 -0
  69. package/lib/engine/mem/storage/ports.d.ts +472 -0
  70. package/lib/engine/mem/storage/registry.d.ts +37 -0
  71. package/lib/engine/mem/storage/sqlite.d.ts +28 -0
  72. package/lib/engine/mem/store/common.d.ts +1 -1
  73. package/lib/engine/mem/store/entity_leg.d.ts +59 -2
  74. package/lib/engine/mem/store/entity_sweep.d.ts +4 -3
  75. package/lib/engine/mem/store/explicit_relations.d.ts +52 -0
  76. package/lib/engine/mem/store/facts_restore.d.ts +124 -0
  77. package/lib/engine/mem/store/floors.d.ts +4 -1
  78. package/lib/engine/mem/store/graph_reader.d.ts +146 -0
  79. package/lib/engine/mem/store/hybrid.d.ts +50 -13
  80. package/lib/engine/mem/store/kinds.d.ts +47 -0
  81. package/lib/engine/mem/store/legs.d.ts +24 -29
  82. package/lib/engine/mem/store/lexical.d.ts +36 -45
  83. package/lib/engine/mem/store/memory.d.ts +486 -82
  84. package/lib/engine/mem/store/recall_policy.d.ts +49 -0
  85. package/lib/engine/mem/store/relation_backfill.d.ts +210 -0
  86. package/lib/engine/mem/store/relation_planner.d.ts +191 -0
  87. package/lib/engine/mem/store/secret_guard.d.ts +0 -1
  88. package/lib/engine/mem/store/self_query.d.ts +4 -2
  89. package/lib/engine/mem/store/time_filter.d.ts +121 -0
  90. package/lib/engine/mem/store/titles.d.ts +52 -0
  91. package/lib/engine/mem/store/vector_repair.d.ts +44 -54
  92. package/lib/engine/mem-contract/config.d.ts +44 -38
  93. package/lib/engine/mem-contract/text.d.ts +1 -1
  94. package/lib/engine/mem-contract/tools.d.ts +184 -117
  95. package/lib/engine/mem-contract/types.d.ts +364 -288
  96. package/lib/index.js +13898 -11500
  97. package/lib/interface-version.json +1 -1
  98. package/lib/types/client/index.d.ts +2 -2
  99. package/lib/types/client/index.d.ts.map +1 -1
  100. package/lib/types/client/paging.d.ts +3 -12
  101. package/lib/types/client/paging.d.ts.map +1 -1
  102. package/lib/types/client/summarize.d.ts.map +1 -1
  103. package/lib/types/command.d.ts +61 -0
  104. package/lib/types/command.d.ts.map +1 -0
  105. package/lib/types/envinit-bootstrap.d.ts +1 -1
  106. package/lib/types/envinit.d.ts +2 -6
  107. package/lib/types/envinit.d.ts.map +1 -1
  108. package/lib/types/hints.d.ts +28 -24
  109. package/lib/types/hints.d.ts.map +1 -1
  110. package/lib/types/index.d.ts +21 -40
  111. package/lib/types/index.d.ts.map +1 -1
  112. package/lib/types/inject.d.ts +3 -2
  113. package/lib/types/inject.d.ts.map +1 -1
  114. package/lib/types/interface_gate.d.ts +9 -9
  115. package/lib/types/prompt.d.ts +17 -41
  116. package/lib/types/prompt.d.ts.map +1 -1
  117. package/lib/types/provision.d.ts +3 -0
  118. package/lib/types/provision.d.ts.map +1 -1
  119. package/lib/types/remote.d.ts +23 -9
  120. package/lib/types/remote.d.ts.map +1 -1
  121. package/lib/types/render.d.ts +3 -3
  122. package/lib/types/tool_schema.d.ts +4 -0
  123. package/lib/types/tool_schema.d.ts.map +1 -1
  124. package/lib/types/vectorMigration.d.ts +5 -7
  125. package/lib/types/vectorMigration.d.ts.map +1 -1
  126. package/package.json +16 -15
  127. package/lib/engine/mem/config/domains.d.ts +0 -20
  128. package/lib/engine/mem/db/dao/chunks.d.ts +0 -231
  129. package/lib/engine/mem/db/dao/documents.d.ts +0 -51
  130. package/lib/engine/mem/db/hash.d.ts +0 -2
  131. package/lib/engine/mem/db/knowledge.d.ts +0 -17
  132. package/lib/engine/mem/eval/index.d.ts +0 -4
  133. package/lib/engine/mem/git.d.ts +0 -73
  134. package/lib/engine/mem/router.d.ts +0 -51
  135. package/lib/engine/mem/store/doc_files.d.ts +0 -125
  136. package/lib/engine/mem/store/document_text.d.ts +0 -47
  137. package/lib/engine/mem/store/ingest_guard.d.ts +0 -25
  138. package/lib/engine/mem/store/knowledge.d.ts +0 -598
  139. package/lib/engine/mem/store/source_picker.d.ts +0 -43
  140. package/lib/engine/mem-retrieval/adapters/local_reranker.d.ts +0 -52
  141. package/lib/engine/mem-retrieval/adapters/none_reranker.d.ts +0 -10
  142. package/lib/engine/mem-retrieval/rerank.d.ts +0 -13
  143. package/lib/types/client/domains.d.ts +0 -53
  144. package/lib/types/client/domains.d.ts.map +0 -1
  145. package/lib/types/client/ingest.d.ts +0 -24
  146. package/lib/types/client/ingest.d.ts.map +0 -1
  147. package/lib/types/open.d.ts +0 -77
  148. package/lib/types/open.d.ts.map +0 -1
  149. package/lib/types/reconcile.d.ts +0 -64
  150. package/lib/types/reconcile.d.ts.map +0 -1
@@ -1,6 +1,55 @@
1
1
  /** Shared domain types for avantf-mem. Single source of truth in @avantf/mem-contract. */
2
- export declare const CATEGORY_VALUES: readonly ["user_pref", "user_env", "project", "tool", "general"];
3
- export type FactCategory = (typeof CATEGORY_VALUES)[number] | string;
2
+ /**
3
+ * The memory has TWO layers, and `kind` says which one a fact belongs to
4
+ * (JOURNAL_NOTES_REDESIGN N1/N2).
5
+ *
6
+ * - `journal` is EPISODIC: what was done / talked about / thought about, one entry per episode,
7
+ * anchored by the caller's `event_date`. A journal row without an event date cannot answer
8
+ * "what did I do yesterday", so the write path refuses one.
9
+ * - `note` is SEMANTIC: a stable fact, preference, constraint, decision or pitfall that is worth
10
+ * recalling without knowing when it was learned.
11
+ *
12
+ * This replaces the eight-value `category` vocabulary (and its `categories` lookup table): the model
13
+ * no longer classifies by topic, it picks a layer. The set lives here because four surfaces read it —
14
+ * `mem_remember`'s schema (required, enum), `mem_recall`/`mem_admin`'s filters, the store's write
15
+ * guard and the one migration that maps the old 16 free-form values onto it.
16
+ */
17
+ export declare const MEMORY_KINDS: readonly ["journal", "note"];
18
+ export type MemoryKind = (typeof MEMORY_KINDS)[number];
19
+ /**
20
+ * What each layer is FOR — the text the tool schema and the prompt section show.
21
+ *
22
+ * Spelled once beside the vocabulary so a caller reading `mem_remember`'s parameter help and one
23
+ * reading `mem_recall`'s filter help see the same two meanings.
24
+ */
25
+ export declare const MEMORY_KIND_DESCRIPTIONS: Readonly<Record<MemoryKind, string>>;
26
+ /**
27
+ * The three time AXES a `mem_recall` call may filter on (TIME_SEARCH_AND_CATEGORIES D1).
28
+ *
29
+ * `created` = when the row was written, `updated` = when it was last modified, `event` = when the
30
+ * fact was TRUE (the caller's `event_date`, stored in `valid_from`). The axes never fall back into
31
+ * one another: a fact with no event time does not participate in an `event` filter, and a row that
32
+ * was never updated does not participate in an `updated` one (D3 in the implementation doc's §3.1).
33
+ *
34
+ * Declared here (not in `tools.ts`) because both the zod schema and the engine's normalizer read it:
35
+ * one list, so a fourth axis cannot reach the tool surface without reaching the SQL.
36
+ */
37
+ export declare const TIME_FIELDS: readonly ["created", "updated", "event"];
38
+ export type TimeField = (typeof TIME_FIELDS)[number];
39
+ /**
40
+ * One explicit time filter — the shape `mem_recall.search`/`titles` accept on the wire.
41
+ *
42
+ * `field` is required (D1). Bounds: `on` (a whole day, or one minute when a time is given) XOR
43
+ * `from`/`to` (half-open range, `to` exclusive). Minute precision throughout (D3) — see
44
+ * `@avantf/mem`'s `store/time_filter.ts` for the normalization the engine applies.
45
+ */
46
+ export interface TimeFilter {
47
+ field: TimeField;
48
+ on?: string;
49
+ from?: string;
50
+ to?: string;
51
+ timezone?: string;
52
+ }
4
53
  /**
5
54
  * The fact statuses a caller may filter by, as the ONE list every surface reads: the tool schema's
6
55
  * `z.enum`, the derived MCP inputSchema and DSH parameter spec, and the CLI's `--status` flag (which
@@ -8,10 +57,34 @@ export type FactCategory = (typeof CATEGORY_VALUES)[number] | string;
8
57
  */
9
58
  export declare const FACT_STATUSES: readonly ["active", "archived"];
10
59
  export type FactStatus = (typeof FACT_STATUSES)[number];
60
+ /**
61
+ * The status FILTER vocabulary: a fact-status set plus `all`.
62
+ *
63
+ * The two filter surfaces that accept it (`mem_admin export` and `mem_recall titles`) need
64
+ * "either status", while a fact VIEW can only ever carry one of {@link FACT_STATUSES}. Declared here
65
+ * so the two filters cannot drift into two spellings of "all".
66
+ */
67
+ export declare const STATUS_FILTERS: readonly ["active", "archived", "all"];
68
+ export type StatusFilter = (typeof STATUS_FILTERS)[number];
69
+ /**
70
+ * The longest title a write may carry, in characters (contract, engine and every surface agree).
71
+ *
72
+ * 120, the upper end of the range the design allows: a title is a human handle for a journal entry
73
+ * ("which transaction was this"), not prose, and the cap is what keeps it from becoming one. The
74
+ * SEARCH query shares the bound because it is compared against the stored title with `=`/`LIKE`.
75
+ */
76
+ export declare const TITLE_MAX_CHARS = 120;
11
77
  export interface FactSummary {
12
78
  fact_id: number;
13
79
  content: string;
14
- category: string;
80
+ /**
81
+ * The short handle of a journal entry (see {@link TITLE_MAX_CHARS}); `null` on a legacy journal
82
+ * and on every note that was written without one. Display only — it never participates in the
83
+ * content relevance score.
84
+ */
85
+ title: string | null;
86
+ /** Which memory layer this fact belongs to (see {@link MEMORY_KINDS}). */
87
+ kind: MemoryKind;
15
88
  status: FactStatus;
16
89
  /**
17
90
  * Trust as of now: the decayed value for an ACTIVE fact, the stored value for a
@@ -33,6 +106,20 @@ export interface FactTriple {
33
106
  obj: string;
34
107
  confidence: number;
35
108
  }
109
+ /**
110
+ * One relation a CALLER stated on a note (`mem_remember.relations`), read back for the detail view
111
+ * (STRUCTURED_RELATIONS §2/§5).
112
+ *
113
+ * This is the EXPLICIT half of the graph resolved back to its stored names (after entity/predicate
114
+ * normalization) — the same three slots the write took. Extracted edges (`source: 'svo'`) are
115
+ * deliberately not part of this list: they are derivable from `content`, and mixing them in would
116
+ * make "what did the caller state" unanswerable. A journal has none by construction (note-only).
117
+ */
118
+ export interface FactRelation {
119
+ subject: string;
120
+ predicate: string;
121
+ object: string;
122
+ }
36
123
  /**
37
124
  * The retention diagnostics on a fact view: the trust value, the active days left before the
38
125
  * trust clock forgets it, and the reinforcement counter.
@@ -54,6 +141,15 @@ export interface FactDetail extends FactSummary {
54
141
  supersedes_id: number | null;
55
142
  entities: string[];
56
143
  triples: FactTriple[];
144
+ /**
145
+ * The relations a CALLER stated on this note (`source: 'explicit'`), in insertion order.
146
+ *
147
+ * Empty for a note nobody declared relations for and for every journal (the layer refuses them).
148
+ * `update` REPLACES this set; an update that omits `relations` and lands on the same row leaves
149
+ * it untouched, while a changed content starts a new revision with no explicit edges of its own
150
+ * (STRUCTURED_RELATIONS §5).
151
+ */
152
+ relations: FactRelation[];
57
153
  /** Active-day clock value at the last settle. */
58
154
  settle_clock: number;
59
155
  pinned_at: string | null;
@@ -94,36 +190,25 @@ export interface FactSourceView {
94
190
  kind: 'session' | 'kb_doc' | 'tool' | 'manual';
95
191
  ref: string;
96
192
  }
97
- /** A single hit from hybrid retrieval over the merged candidate pool. */
193
+ /** A single hit from hybrid retrieval over the memory corpus. */
98
194
  export interface RecallHit {
99
- kind: 'fact' | 'doc_chunk';
195
+ /** The hit type. Only memory facts remain, so this is always `fact`. */
196
+ kind: 'fact';
100
197
  ref_id: number;
101
198
  text: string;
102
199
  score: number;
103
- /**
104
- * Knowledge-base taxonomy (`domain → source`) of a `doc_chunk`, taken from the
105
- * `documents` row by the store that owns it. Both are null for memory facts.
106
- *
107
- * Carried as fields rather than re-parsed out of `source_ref`: either name may
108
- * itself contain `:` (`domain:source:docId:idx`), so any split of `source_ref`
109
- * is a guess. Filters must use these, and so must the UI.
110
- */
111
- domain: string | null;
112
- source: string | null;
113
200
  source_ref: string;
114
201
  entities: string[];
115
202
  /**
116
203
  * When this hit was first recorded: a fact's revision chain keeps the ORIGINAL row's
117
204
  * `created_at` across a supersede (`mem_remember update`), so this is when the memory was
118
- * first asserted, not when its current text was written (see `updated_at`). A doc_chunk
119
- * carries its owning document's `created_at`.
205
+ * first asserted, not when its current text was written (see `updated_at`).
120
206
  */
121
207
  created_at: string;
122
208
  /**
123
209
  * When this row was last CHANGED — an edit, pin/unpin, reinforcement, archive or restore.
124
210
  * Retrieval and the daily settle deliberately do not write it, so it never means "last
125
- * retrieved"; a copy can carry `null` on a legacy row. A doc_chunk carries its owning
126
- * document's `updated_at` (re-ingest bumps it).
211
+ * retrieved"; a copy can carry `null` on a legacy row.
127
212
  */
128
213
  updated_at: string | null;
129
214
  /**
@@ -143,11 +228,10 @@ export interface RecallHit {
143
228
  */
144
229
  scores?: Record<string, RecallLegScore | null>;
145
230
  /**
146
- * P-01: the FUSION output score for this hit, before any cross-store normalization.
231
+ * P-01: the FUSION output score for this hit, before the output budget reshapes the list.
147
232
  *
148
- * `score` is what the surface ranks by and can be re-scaled downstream (`crossQuery` min-max
149
- * normalizes over the merged pool), so a caller reasoning about "which leg carried this" needs
150
- * the fusion value itself rather than a number that means a different thing per surface.
233
+ * `score` is what the surface ranks by, so a caller reasoning about "which leg carried this"
234
+ * needs the fusion value itself rather than the rounded number a surface prints.
151
235
  */
152
236
  final?: number;
153
237
  /**
@@ -157,7 +241,240 @@ export interface RecallHit {
157
241
  */
158
242
  valid_from?: string;
159
243
  valid_to?: string;
244
+ /**
245
+ * The fact's title, when it has one (journal entries normally do, notes usually do not).
246
+ *
247
+ * A DISPLAY field: it is serialized only when non-null (the same convention as `valid_from`) and
248
+ * it is deliberately not part of any retrieval score — title search is its own action
249
+ * (`mem_recall.titles`), and mixing the two score semantics is what that action exists to
250
+ * avoid.
251
+ */
252
+ title?: string;
253
+ }
254
+ /**
255
+ * One candidate of `mem_recall.titles` — a title match, NOT a content hit.
256
+ *
257
+ * Deliberately its own shape instead of {@link RecallHit}: `RecallHit.kind` means "the hit type"
258
+ * (`fact`), while this shape needs the memory LAYER (`journal`/`note`), and the identity field the
259
+ * caller acts on is `fact_id` (`RecallHit` calls it `ref_id`). Reusing the hit would make `kind`
260
+ * carry two meanings and would let the model mistake a title match for a content relevance score.
261
+ *
262
+ * There is NO `score` field on purpose (JOURNAL_TITLE_SEARCH §11.4): the ordering (exact title >
263
+ * prefix > matched terms > recency) is computed on the server, and a number exported here would be
264
+ * read as "how relevant the CONTENT is", which is `mem_recall.search`'s answer, not this one's.
265
+ */
266
+ export interface TitleCandidate {
267
+ fact_id: number;
268
+ /** The row's CURRENT title — the handle to continue the transaction with. */
269
+ title: string;
270
+ /**
271
+ * The title text the query matched: normally `title`, and one of the aliases a CORRECTED
272
+ * transaction used to carry when the old handle is what matched (v17's controlled `retitle`).
273
+ *
274
+ * Always present, so a caller can compare the two without a special case: `matched_title !== title`
275
+ * means "this hit came from an old name" — the row is the same transaction, just renamed.
276
+ */
277
+ matched_title: string;
278
+ memory_kind: MemoryKind;
279
+ status: FactStatus;
280
+ created_at: string;
281
+ updated_at: string | null;
282
+ /** The fact's event time (`valid_from`), when it has one — the journal axis that tells two entries apart. */
283
+ event_date?: string;
284
+ /** The first characters of the content, for judging "same transaction or not"; absent when `include_excerpt` is false. */
285
+ excerpt?: string;
286
+ }
287
+ /** One page of an EXACT result — the envelope both exact surfaces report (RECALL_INTERFACE §3). */
288
+ export interface ExactPageEnvelope {
289
+ /** How many facts matched the literal query (before paging). */
290
+ total: number;
291
+ /** The page's first row index (`0`-based). */
292
+ offset: number;
293
+ /** The page size that was applied (after the 50 cap). */
294
+ limit: number;
295
+ /** `offset + hits.length < total` — there is another page. */
296
+ truncated: boolean;
297
+ }
298
+ /** `mem_recall.search` when the literal (exact) pass matched: RAW hits, never floored (RECALL_INTERFACE §2.2/§5). */
299
+ export type ExactSearchResult = ExactPageEnvelope & {
300
+ mode: 'exact';
301
+ hits: RecallHit[];
302
+ };
303
+ /**
304
+ * `mem_recall.search` when the caller gave a TIME WINDOW and no query (RECALL_INTERFACE §2.4).
305
+ *
306
+ * The window IS the question: no literal pass, no relation plan, no fuzzy legs. `hits` are the
307
+ * active facts inside the window, newest first along `time.field`, and `score` is the same constant
308
+ * 1 the exact page reports — there is no relevance number here to compare with anything.
309
+ */
310
+ export type TimeSearchResult = ExactPageEnvelope & {
311
+ mode: 'time';
312
+ hits: RecallHit[];
313
+ };
314
+ /**
315
+ * `mem_recall.search` when no literal match existed: the hybrid retrieval, unchanged except that its
316
+ * knobs are the engine's fixed rules (`min_score = 0.5`, `topK = 5`) instead of caller parameters.
317
+ */
318
+ export type FuzzySearchResult = RecallResult & {
319
+ mode: 'fuzzy';
320
+ };
321
+ /** One endpoint of a relation path — the STORED entity, by id and canonical name. */
322
+ export interface RelationNode {
323
+ /** `entities.entity_id` — stable across renames of the display label. */
324
+ entity_id: number;
325
+ /** `entities.name`, the canonical name (an alias in the query resolves to this). */
326
+ name: string;
327
+ }
328
+ /**
329
+ * One hop of a relation path: the stored `subject —predicate→ object` of ONE edge, plus the fact it
330
+ * was extracted from. `fact_id` is the evidence — the row `mem_admin detail` opens — so a path is
331
+ * never a string answer without a source.
332
+ */
333
+ export interface RelationPathEdge {
334
+ subject: string;
335
+ predicate: string;
336
+ object: string;
337
+ /**
338
+ * The ACTIVE fact this edge came from. Every edge of every path carries one: the relation stage
339
+ * never reuses a path whose evidence cannot be opened.
340
+ */
341
+ fact_id: number;
342
+ /** This edge's extractor confidence in `[0,1]`; the path's own `confidence` is the product of its edges'. */
343
+ confidence: number;
160
344
  }
345
+ /** A complete relation path (RELATIONAL_RECALL §7), from the queried node to the node walked to. */
346
+ export interface RelationPath {
347
+ start: RelationNode;
348
+ end: RelationNode;
349
+ /** Number of edges — 1 or 2 in this version ("最大 2 跳,不做任意递归"). */
350
+ hops: number;
351
+ /** The edges in walk order; `edges[0].subject` is `start.name` and the last `object` is `end.name`. */
352
+ edges: RelationPathEdge[];
353
+ /** Product of the edge confidences, rounded by the graph library. */
354
+ confidence: number;
355
+ }
356
+ /**
357
+ * The fact behind a relation answer.
358
+ *
359
+ * Deliberately NOT a {@link RecallHit}: a relation answer is not a ranked content match, so this
360
+ * shape has NO `score` (there is nothing to rank it against), and the handle a caller acts on is
361
+ * `fact_id` (`RecallHit` calls it `ref_id`). Keeping them apart is what stops a model from reading
362
+ * "this path's evidence" as "a relevance number".
363
+ */
364
+ export interface RelationEvidence {
365
+ /** The fact id every path edge and `mem_admin detail` share. */
366
+ fact_id: number;
367
+ /** The fact's content, verbatim (bounded by the retrieval output budget, like a hit). */
368
+ text: string;
369
+ /** The memory layer, so a caller knows whether it is reading a journal or a note. */
370
+ memory_kind: MemoryKind;
371
+ title?: string;
372
+ created_at: string;
373
+ updated_at: string | null;
374
+ /** Set when the output budget shortened `text` (never when the evidence itself was dropped). */
375
+ truncated?: boolean;
376
+ }
377
+ /**
378
+ * `mem_recall.search` when the query named a relation the graph could answer
379
+ * (RELATIONAL_RECALL §6–7, Phase 4).
380
+ *
381
+ * It carries STRUCTURE (`paths`, each edge pointing at its fact) and the evidence rows — never a
382
+ * natural-language answer, and never a relevance score. An empty `paths` is not a relation result
383
+ * at all: the store falls back to {@link FuzzySearchResult} instead, so `mode:"relation"` always
384
+ * means "at least one path (and therefore at least one openable fact) was found".
385
+ *
386
+ * A PURE multi-entity question ("张伟和李娜") is not a relation question: it is answered at the
387
+ * fact level by {@link FactSearchResult} (`mode:"facts"`), over `fact_entities`, so journal entries
388
+ * and notes without any relation edge are covered. The graph library's `andPaths` is still available
389
+ * to adapters that want edge evidence, but `mem_recall.search` no longer routes a query to it.
390
+ */
391
+ export interface RelationSearchResult {
392
+ mode: 'relation';
393
+ /** The paths, best first (confidence DESC, hops ASC, evidence recency DESC). */
394
+ paths: RelationPath[];
395
+ /** The distinct facts the paths' edges point at, in path order. */
396
+ evidence: RelationEvidence[];
397
+ /** Distinct candidate paths found before paging (`paths.length` may be smaller). */
398
+ total: number;
399
+ /** `true` when more paths existed than this result returns. */
400
+ truncated: boolean;
401
+ }
402
+ /**
403
+ * The citation handle of a fact row: `memory:fact:<id>`.
404
+ *
405
+ * ONE definition, because three surfaces spell this string: a {@link RecallHit}'s `source_ref`, a
406
+ * {@link FactEvidence}'s, and the model projection's fallback for a host that predates the field
407
+ * (an added field does not bump `WIRE_VERSION`, see `tools.ts#factsForModel`). The read surface
408
+ * DERIVES it — nothing is stored — so a handle that disagreed between two of them would be a
409
+ * citation the other surface cannot resolve (REVIEW-FACT-AND-R2 P2-1).
410
+ */
411
+ export declare function factSourceRef(factId: number): string;
412
+ /**
413
+ * The fact behind a fact-level AND answer (docs/GRAPH_RECALL_AND_SEMANTICS.md §7.9).
414
+ *
415
+ * Deliberately NOT a {@link RecallHit}: this is not a ranked content match, so the shape has NO
416
+ * `score` (there is nothing to rank it against), and the handle a caller acts on is `fact_id`
417
+ * (`RecallHit` calls it `ref_id`). `memory_kind` + `status` say which layer and which revision the
418
+ * fact is, and `event_date` is the journal axis (the stored `valid_from`).
419
+ */
420
+ export interface FactEvidence {
421
+ /** The fact id `mem_admin detail` opens. */
422
+ fact_id: number;
423
+ /**
424
+ * The citation handle, spelled exactly as a {@link RecallHit}'s `source_ref`
425
+ * (`memory:fact:<id>`) so an answer can cite a fact page row the same way it cites a hit.
426
+ *
427
+ * It is DERIVED from `fact_id` (the read surface builds it; nothing is stored), which is why it
428
+ * says nothing about P-08 provenance — that is `mem_admin detail`'s `sources`. Carried anyway
429
+ * because a caller that only sees this page must be able to quote a source without a second call.
430
+ */
431
+ source_ref: string;
432
+ /** The fact's content, verbatim (bounded by the retrieval output budget, like a hit). */
433
+ content: string;
434
+ /** The memory layer, so a caller knows whether it is reading a journal or a note. */
435
+ memory_kind: MemoryKind;
436
+ /** `active` for every row this stage returns; carried so the field is explicit, not implied. */
437
+ status: FactStatus;
438
+ title?: string;
439
+ /** The fact's event time (`valid_from`), when it has one. */
440
+ event_date?: string;
441
+ created_at: string;
442
+ updated_at: string | null;
443
+ /** Set when the output budget shortened `content` (never when the fact itself was dropped). */
444
+ truncated?: boolean;
445
+ }
446
+ /**
447
+ * `mem_recall.search` when the query was a PURE multi-entity connection
448
+ * (docs/GRAPH_RECALL_AND_SEMANTICS.md §7.9): every `facts` row mentions EVERY entity the query
449
+ * named, and `total`/`offset`/`limit`/`truncated` page the FACTS, not paths or edges.
450
+ *
451
+ * An empty page is not a result at all — the store continues to the relation stage and then fuzzy —
452
+ * so `mode:"facts"` always means at least one fact was found, and the caller never has to tell an
453
+ * empty answer from a failed one.
454
+ */
455
+ export interface FactSearchResult extends ExactPageEnvelope {
456
+ mode: 'facts';
457
+ /** The facts, newest row first, each mentioning every queried entity. */
458
+ facts: FactEvidence[];
459
+ }
460
+ /**
461
+ * The `mem_recall.search` payload — ONE type for the five modes, so a caller must look at `mode`
462
+ * before it reads pagination (an exact, time or facts page), the paths and evidence (a relation
463
+ * answer) or the retrieval diagnostics (a fuzzy answer).
464
+ */
465
+ export type MemorySearchResult = ExactSearchResult | TimeSearchResult | FactSearchResult | RelationSearchResult | FuzzySearchResult;
466
+ /** `mem_recall.titles` when one title is EXACTLY the query (case-sensitive; RECALL_INTERFACE §2.3). */
467
+ export type ExactTitlesResult = ExactPageEnvelope & {
468
+ mode: 'exact';
469
+ candidates: TitleCandidate[];
470
+ };
471
+ /** `mem_recall.titles` when no title equals the query: the title-index shortlist, fixed at `topK = 5`. */
472
+ export interface FuzzyTitlesResult {
473
+ mode: 'fuzzy';
474
+ candidates: TitleCandidate[];
475
+ }
476
+ /** The `mem_recall.titles` payload. */
477
+ export type TitlesResult = ExactTitlesResult | FuzzyTitlesResult;
161
478
  /** P-01: one leg's contribution to a hit (see {@link RecallHit.scores}). */
162
479
  export interface RecallLegScore {
163
480
  /** The leg's own raw score, on its own scale. */
@@ -178,7 +495,11 @@ export interface RetrievalFloors {
178
495
  semantic: number;
179
496
  /** Distinct-query-term floor for the FTS leg (`retriever.min_fts_terms`; 1 when degraded). */
180
497
  fts: number;
181
- /** Anchored query-coverage floor for the entity leg (`retriever.min_jaccard`, see `store/entity_leg.ts`). */
498
+ /**
499
+ * Configured LOWER BOUND of the entity leg's evidence bar (`retriever.min_jaccard`). The leg
500
+ * applies a per-candidate anchor-rarity bar on top of it, in the same anchored-overlap unit — see
501
+ * `store/entity_leg.ts` and DESIGN §20.19 续.
502
+ */
182
503
  jaccard: number;
183
504
  }
184
505
  /**
@@ -226,12 +547,21 @@ export interface RecallResult {
226
547
  };
227
548
  /**
228
549
  * The effective relevance floors for this query, when it ran through the hybrid orchestration.
229
- * Absent on graph-only answers (`probe`/`chain`/`reason`/`related`), which never scored a leg.
550
+ * Absent on literal-only answers (an exact page) and non-hybrid payloads, which never scored a leg.
230
551
  * When {@link RecallResult.relaxed} is true these are the RELAXED values that produced the hits.
231
552
  */
232
553
  floors?: RetrievalFloors;
233
554
  /** Per-leg candidates removed by those floors (see {@link RetrievalFloorDrops}). */
234
555
  dropped_by_floor?: RetrievalFloorDrops;
556
+ /**
557
+ * Candidates the FUSED-score floor (`retriever.min_score`, default 0.5) removed after fusion.
558
+ *
559
+ * Absent when nothing was dropped (including when the floor is off). It is NOT part of
560
+ * `dropped_by_floor`: that structure attributes drops to a leg, and this threshold is applied after
561
+ * every leg has been fused. When {@link RecallResult.relaxed} is true this is the RELAXED pass's
562
+ * count, the same convention as `floors` / `dropped_by_floor`.
563
+ */
564
+ dropped_by_score?: number;
235
565
  /**
236
566
  * The strict floors emptied this query's result and ONE relaxed pass supplied these hits (`floors`
237
567
  * are that pass's values). Absent on an ordinary answer, on an explicit `floors: 'loose'` request
@@ -242,8 +572,8 @@ export interface RecallResult {
242
572
  }
243
573
  /**
244
574
  * Weights reported when the semantic leg is unavailable (FTS+entity rebalance).
245
- * Single source for both stores and the cross-retrieval router — previously the
246
- * same literal lived in three files and could silently desync.
575
+ * Single source for the store and every surface that reports them — previously the
576
+ * same literal lived in more than one file and could silently desync.
247
577
  */
248
578
  export declare const DEGRADED_WEIGHTS: RecallResult['weights'];
249
579
  /**
@@ -264,8 +594,9 @@ export interface ContradictionRecord {
264
594
  detected_at: string;
265
595
  content_a: string;
266
596
  content_b: string;
267
- category_a: string | null;
268
- category_b: string | null;
597
+ /** The two facts' memory layers (see {@link MEMORY_KINDS}); `null` on a legacy row. */
598
+ kind_a: string | null;
599
+ kind_b: string | null;
269
600
  resolved: number;
270
601
  }
271
602
  /**
@@ -541,271 +872,16 @@ export interface VectorsFixStoreReport {
541
872
  reindexed?: number;
542
873
  }
543
874
  /**
544
- * `mem_admin vectors_fix`: what a repair did, or — with `dry_run` — what it WOULD do, per store.
875
+ * `mem_admin vectors_fix`: what a repair did, or — with `dry_run` — what it WOULD do.
545
876
  *
546
- * The report is per-store because one switch and one call now cover BOTH libraries (a model swap
547
- * leaves memory and knowledge in the same state, and the repair used to exist for memory only).
548
- * `store` on the request narrows the call to one library; the top-level `semantic_available` is the
549
- * AND over the stores actually repaired.
877
+ * The wrapper is kept separate from {@link VectorsFixStoreReport} because the repair reports a
878
+ * store-level result and a call-level one: `semantic_available` is "the model was available for
879
+ * the whole call", while the store report carries the counts.
550
880
  */
551
881
  export interface VectorsFixReport {
552
882
  stores: {
553
883
  memory?: VectorsFixStoreReport;
554
- knowledge?: VectorsFixStoreReport;
555
884
  };
556
885
  semantic_available: boolean;
557
886
  dry_run: boolean;
558
887
  }
559
- /** `kb_manage ingest`: what one document produced. */
560
- export interface IngestResult {
561
- doc_id: number;
562
- chunks: number;
563
- /**
564
- * How many chunks did NOT get a semantic vector. Absent when every chunk did, so a healthy ingest
565
- * stays as quiet as it was.
566
- *
567
- * `chunks` counts what was CHUNKED, not what became semantically searchable: an encode failure (no
568
- * model, bad input) or a write failure (`SQLITE_BUSY` under multi-process contention) skips that
569
- * chunk while the text/entity/FTS indexes stay complete. Reporting the shortfall here is what stops
570
- * "入库成功 N 段" from implying "N 段都能被语义检索到" — before this, the only way to find out was to
571
- * run `kb_reindex` and compare `vectors_stale` against `vectors_encoded`. A missing embedding model
572
- * reports every chunk, which is the honest reading of "indexed, but not semantically".
573
- */
574
- vectors_failed?: number;
575
- /**
576
- * How the source bytes became text: `utf-8`, `utf-8-bom`, `utf-16le`, `utf-16be` or `gb18030`
577
- * (absent for pasted text, for a document pulled back from its managed file, and for a PDF —
578
- * whose text comes from an extracted layer rather than from a byte encoding). Reported because
579
- * decoding a legacy-encoded file is a GUESS the caller should be able to see.
580
- */
581
- encoding?: string;
582
- /**
583
- * Which converter turned the source into Markdown — `docx`, `xlsx`, `html` or `csv` (absent for
584
- * text, PDF, and a body re-read from its managed file). The body is derived rather than read in
585
- * that case, so the caller is told what derived it.
586
- */
587
- converter?: string;
588
- /** What that conversion could not carry over (dropped media, a truncated sheet, ragged rows). */
589
- warnings?: string[];
590
- /** The managed file this ingest wrote (`knowledge.docs.dir/<domain>/<source>/<title>.md`). */
591
- file?: string;
592
- /** Why the managed file could not be written; the document itself is still searchable. */
593
- file_error?: string;
594
- }
595
- /**
596
- * One existing document an ingest would land on — the machine-readable half of a collision.
597
- *
598
- * `path` is the managed `.md` copy (`knowledge.docs.dir/<domain>/<source>/<title>.md`), which is
599
- * what the caller edits to change that document; `doc_id`/`title` are what it takes to name the
600
- * collision. Reported by BOTH the add-only refusal (as text, for the agent) and the replace
601
- * confirmation (as a list, for the UI's dialog and the CLI).
602
- */
603
- export interface KbIngestConflict {
604
- doc_id: number;
605
- title: string;
606
- path: string;
607
- }
608
- /**
609
- * What `kb_manage` answers when a write would REPLACE an existing document and the caller did not
610
- * confirm it. `conflict: true` is the flag the UI's 入库 form and the CLI dispatch on; nothing was
611
- * written when this comes back, so "取消" is a no-op rather than a rollback.
612
- *
613
- * `would_overwrite`/`would_add` are the summary the confirmation dialog shows ("将覆盖 N 篇 /
614
- * 新增 M 篇") — computed at plan time, when the targets are known, rather than re-derived by the
615
- * caller from `conflicts`.
616
- */
617
- export interface KbConflictReport {
618
- conflict: true;
619
- /** Chinese, actionable: names the colliding documents and says nothing was written. */
620
- error: string;
621
- conflicts: KbIngestConflict[];
622
- would_overwrite: number;
623
- would_add: number;
624
- }
625
- /**
626
- * `kb_manage import`: what landed, what failed (with the reason), and what was skipped.
627
- *
628
- * `skipped` is the DIRECTORY walk's rejections (files whose name is not an ingestable format) — a
629
- * silent list before this, which made "3 of 200 files imported" indistinguishable from "3 files".
630
- * Only the first `SKIPPED_REPORTED` (a `core` constant) paths are listed; `skipped_total` is the real count.
631
- */
632
- export interface ImportResult {
633
- imported: IngestResult[];
634
- failed: {
635
- path: string;
636
- error: string;
637
- }[];
638
- skipped: string[];
639
- skipped_total: number;
640
- }
641
- /**
642
- * One document's managed file, as the file system sees it right now.
643
- *
644
- * `stale` means the file's body no longer matches what the KB indexed (someone edited it and
645
- * the change has not been ingested); `missing` means there is no file for this document (never
646
- * written, deleted by hand, or replaced by another document's file).
647
- */
648
- export interface KbDocFile {
649
- doc_id: number;
650
- title: string;
651
- path: string;
652
- stale: boolean;
653
- missing: boolean;
654
- }
655
- /**
656
- * `kb_manage sync`: the managed files vs what the KB indexed.
657
- *
658
- * `stale` is the plan and `reingested` what happened (equal unless `dry_run`); `orphans` are
659
- * managed files with no document behind them — left by a hand-deleted row, and safe to delete.
660
- */
661
- export interface KbSyncReport {
662
- checked: number;
663
- stale: KbDocFile[];
664
- missing: KbDocFile[];
665
- orphans: string[];
666
- /**
667
- * Files that sit exactly where a document's path says its own copy belongs, yet do not claim that
668
- * document — a whole-file overwrite that destroyed the frontmatter.
669
- *
670
- * These ARE recovered automatically, but only behind guards: no `doc_id` at all, a non-empty
671
- * body, a stamp that holds still (see `ADOPT_SETTLE_MS`) and a path exactly one document maps
672
- * to. A candidate that fails any of them stays here, unclaimed, and `sync {adopt: true}` is the
673
- * explicit per-document way to take it back immediately.
674
- */
675
- unclaimed: KbUnclaimedFile[];
676
- reingested: number;
677
- /** How many documents this call adopted (`sync {adopt: true}`); 0 otherwise. */
678
- adopted: number;
679
- dry_run: boolean;
680
- }
681
- /** One adoptable file: its path names this document, but the file does not claim it. */
682
- export interface KbUnclaimedFile {
683
- doc_id: number;
684
- title: string;
685
- path: string;
686
- }
687
- /**
688
- * `kb_manage reindex`: what it did, and what it deliberately did NOT redo (DESIGN §20).
689
- *
690
- * `vectors_stale` is the plan and `vectors_encoded` what happened; they differ when the embedder
691
- * is unavailable, which is the case a caller needs to see instead of "nothing to do".
692
- */
693
- export interface ReindexReport {
694
- chunks: number;
695
- entities_rebuilt: number;
696
- vectors_stale: number;
697
- vectors_encoded: number;
698
- semantic_available: boolean;
699
- dry_run: boolean;
700
- }
701
- /** `kb_manage list`: the stored document columns (everything but `meta`/`status`). */
702
- export interface DocumentSummary {
703
- doc_id: number;
704
- domain: string;
705
- source: string;
706
- title: string;
707
- source_uri: string | null;
708
- created_at: string;
709
- updated_at: string;
710
- }
711
- /** The full stored document row (`kb detail` minus its chunks). */
712
- export interface DocumentRecord extends DocumentSummary {
713
- meta: string;
714
- status: string;
715
- }
716
- /** One stored chunk as `kb detail` returns it. */
717
- export interface DocumentChunk {
718
- chunk_id: number;
719
- idx: number;
720
- text: string;
721
- headings_path: string;
722
- source_ref: string;
723
- }
724
- /** `kb_manage detail`: the document row plus its chunks, in document order. */
725
- export interface DocumentDetail extends DocumentRecord {
726
- chunks: DocumentChunk[];
727
- }
728
- /** What one `kb_manage` source string turned out to be. */
729
- export type SourceKind = 'url' | 'file' | 'directory' | 'text' | 'missing';
730
- /**
731
- * The answer the UI needs to label the input and pick the right `kb_manage` action.
732
- *
733
- * It lives in the CONTRACT rather than next to the engine that computes it, because the browser half
734
- * cannot import the engine (it must not pull node into the client bundle) and used to hand-mirror this
735
- * shape — the same "mirror drifts, then a render body dereferences a field the host stopped sending"
736
- * failure that has already cost this family once.
737
- */
738
- export interface SourceClassification {
739
- readonly kind: SourceKind;
740
- /** Resolved, boundary-checked absolute paths (when `kind` is `file`/`directory`). */
741
- readonly paths: readonly string[];
742
- /** Inputs that look like paths but do not resolve, or resolve outside the allowed roots. */
743
- readonly missing: readonly string[];
744
- /** Why the missing ones are missing (the boundary's own message; they differ per input). */
745
- readonly reasons: readonly string[];
746
- /** How many files a directory holds that ingestion would actually take. */
747
- readonly files: number;
748
- }
749
- /** One row of a directory listing (the 选择 picker's `browseDir`). */
750
- export interface BrowseEntry {
751
- readonly name: string;
752
- readonly path: string;
753
- /** `dir` navigates; `ingestable` can be picked; `other` exists but ingestion would refuse it. */
754
- readonly kind: 'dir' | 'ingestable' | 'other';
755
- }
756
- /**
757
- * One directory as the browser picker sees it.
758
- *
759
- * Pinned to the engine's `browseDirectory` return BY the host Remote declaration, which annotates its
760
- * payload with this interface: the two cannot drift without a type error. That check is the reason this
761
- * shape is spelled here rather than mirrored in the browser half.
762
- */
763
- export interface BrowseListing {
764
- readonly path: string;
765
- /** The parent directory, or `null` when this is an allowed root (or the filesystem root). */
766
- readonly parent: string | null;
767
- /** The configured roots — what the picker may walk when `unrestricted` is false. */
768
- readonly roots: readonly string[];
769
- /**
770
- * `knowledge.ingest.allow_outside_workspace`: the whole filesystem is fair game, so `roots` is
771
- * informational only and the UI must say "不限制" rather than show a boundary it does not honour.
772
- */
773
- readonly unrestricted: boolean;
774
- readonly entries: readonly BrowseEntry[];
775
- }
776
- /**
777
- * The 知识页 domain picker's option list: the configured `knowledge.domains` allowlist ∪ the
778
- * domains the library already holds, plus whether the allowlist is active.
779
- *
780
- * `restricted` mirrors "the allowlist is non-empty": then the control is a closed `<select>` and a
781
- * new domain is a config change, not a second spelling of an existing one; an explicitly empty
782
- * allowlist means no restriction, so the control stays a free input. The union half is not a
783
- * compatibility layer — without it, narrowing the allowlist would make every existing document
784
- * un-re-ingestable.
785
- *
786
- * It lives in the CONTRACT rather than being mirrored on both halves for the reason every UI payload
787
- * here does: the host half declared it as an anonymous shape and the browser half hand-copied it,
788
- * which is exactly how a field goes missing. `KnowledgeStore.domainCatalog()` / `addDomain()` (the
789
- * real producers) and the picker (the real consumer) now name this one type.
790
- */
791
- export interface DomainCatalog {
792
- readonly domains: readonly string[];
793
- readonly restricted: boolean;
794
- }
795
- /** Which of the two things a document row can open (`openDoc`). */
796
- export type OpenTarget = 'file' | 'dir';
797
- /**
798
- * What `openDoc` launched, for the UI's status line.
799
- *
800
- * In the contract, not beside `openDocumentPath` (the host-side producer), for the same reason as
801
- * {@link DomainCatalog}: the browser half cannot import that module — it pulls `node:child_process` —
802
- * and used to hand-copy `{ path, opener }` (dropping `target`). The host Remote declaration annotates
803
- * its payload with this interface, so producer and wire cannot drift without a type error.
804
- */
805
- export interface OpenOutcome {
806
- /** The managed path actually handed to the opener (the file, or the directory holding it). */
807
- readonly path: string;
808
- readonly target: OpenTarget;
809
- /** The command that was actually used (`code`, `explorer.exe`, …). */
810
- readonly opener: string;
811
- }