@avantf/dsh-mem 0.7.0 → 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 (146) hide show
  1. package/README.md +4 -0
  2. package/lib/client.js +548 -1195
  3. package/lib/dsh-build.json +1 -0
  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 +50 -10
  43. package/lib/engine/mem/index.d.ts +6 -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/modelBootstrap.d.ts +1 -1
  56. package/lib/engine/mem/relations/confidence.d.ts +39 -0
  57. package/lib/engine/mem/relations/extract.d.ts +128 -0
  58. package/lib/engine/mem/relations/index.d.ts +32 -0
  59. package/lib/engine/mem/relations/name-quality.d.ts +78 -0
  60. package/lib/engine/mem/relations/normalize.d.ts +275 -0
  61. package/lib/engine/mem/relations/patterns.d.ts +67 -0
  62. package/lib/engine/mem/relations/statement.d.ts +88 -0
  63. package/lib/engine/mem/relations/types.d.ts +79 -0
  64. package/lib/engine/mem/runtime.d.ts +61 -88
  65. package/lib/engine/mem/storage/create.d.ts +42 -0
  66. package/lib/engine/mem/storage/errors.d.ts +65 -0
  67. package/lib/engine/mem/storage/index.d.ts +15 -0
  68. package/lib/engine/mem/storage/ports.d.ts +472 -0
  69. package/lib/engine/mem/storage/registry.d.ts +37 -0
  70. package/lib/engine/mem/storage/sqlite.d.ts +28 -0
  71. package/lib/engine/mem/store/common.d.ts +1 -1
  72. package/lib/engine/mem/store/entity_leg.d.ts +59 -2
  73. package/lib/engine/mem/store/entity_sweep.d.ts +4 -3
  74. package/lib/engine/mem/store/explicit_relations.d.ts +52 -0
  75. package/lib/engine/mem/store/facts_restore.d.ts +10 -2
  76. package/lib/engine/mem/store/floors.d.ts +4 -1
  77. package/lib/engine/mem/store/graph_reader.d.ts +146 -0
  78. package/lib/engine/mem/store/hybrid.d.ts +50 -13
  79. package/lib/engine/mem/store/kinds.d.ts +47 -0
  80. package/lib/engine/mem/store/legs.d.ts +24 -29
  81. package/lib/engine/mem/store/lexical.d.ts +36 -45
  82. package/lib/engine/mem/store/memory.d.ts +471 -83
  83. package/lib/engine/mem/store/recall_policy.d.ts +49 -0
  84. package/lib/engine/mem/store/relation_backfill.d.ts +210 -0
  85. package/lib/engine/mem/store/relation_planner.d.ts +191 -0
  86. package/lib/engine/mem/store/secret_guard.d.ts +0 -1
  87. package/lib/engine/mem/store/self_query.d.ts +4 -2
  88. package/lib/engine/mem/store/time_filter.d.ts +121 -0
  89. package/lib/engine/mem/store/titles.d.ts +52 -0
  90. package/lib/engine/mem/store/vector_repair.d.ts +44 -54
  91. package/lib/engine/mem-contract/config.d.ts +44 -38
  92. package/lib/engine/mem-contract/text.d.ts +1 -1
  93. package/lib/engine/mem-contract/tools.d.ts +145 -132
  94. package/lib/engine/mem-contract/types.d.ts +364 -288
  95. package/lib/index.js +12048 -11337
  96. package/lib/interface-version.json +1 -1
  97. package/lib/types/client/index.d.ts +2 -2
  98. package/lib/types/client/index.d.ts.map +1 -1
  99. package/lib/types/client/paging.d.ts +3 -12
  100. package/lib/types/client/paging.d.ts.map +1 -1
  101. package/lib/types/client/summarize.d.ts.map +1 -1
  102. package/lib/types/envinit-bootstrap.d.ts +1 -1
  103. package/lib/types/envinit.d.ts +2 -6
  104. package/lib/types/envinit.d.ts.map +1 -1
  105. package/lib/types/hints.d.ts +28 -24
  106. package/lib/types/hints.d.ts.map +1 -1
  107. package/lib/types/index.d.ts +21 -40
  108. package/lib/types/index.d.ts.map +1 -1
  109. package/lib/types/inject.d.ts +3 -2
  110. package/lib/types/inject.d.ts.map +1 -1
  111. package/lib/types/interface_gate.d.ts +1 -1
  112. package/lib/types/prompt.d.ts +17 -41
  113. package/lib/types/prompt.d.ts.map +1 -1
  114. package/lib/types/provision.d.ts +3 -0
  115. package/lib/types/provision.d.ts.map +1 -1
  116. package/lib/types/remote.d.ts +14 -7
  117. package/lib/types/remote.d.ts.map +1 -1
  118. package/lib/types/tool_schema.d.ts +4 -0
  119. package/lib/types/tool_schema.d.ts.map +1 -1
  120. package/lib/types/vectorMigration.d.ts +5 -7
  121. package/lib/types/vectorMigration.d.ts.map +1 -1
  122. package/package.json +15 -14
  123. package/lib/engine/mem/config/domains.d.ts +0 -20
  124. package/lib/engine/mem/db/dao/chunks.d.ts +0 -231
  125. package/lib/engine/mem/db/dao/documents.d.ts +0 -51
  126. package/lib/engine/mem/db/hash.d.ts +0 -2
  127. package/lib/engine/mem/db/knowledge.d.ts +0 -17
  128. package/lib/engine/mem/eval/index.d.ts +0 -4
  129. package/lib/engine/mem/git.d.ts +0 -73
  130. package/lib/engine/mem/router.d.ts +0 -51
  131. package/lib/engine/mem/store/doc_files.d.ts +0 -125
  132. package/lib/engine/mem/store/document_text.d.ts +0 -47
  133. package/lib/engine/mem/store/ingest_guard.d.ts +0 -25
  134. package/lib/engine/mem/store/knowledge.d.ts +0 -598
  135. package/lib/engine/mem/store/source_picker.d.ts +0 -43
  136. package/lib/engine/mem-retrieval/adapters/local_reranker.d.ts +0 -52
  137. package/lib/engine/mem-retrieval/adapters/none_reranker.d.ts +0 -10
  138. package/lib/engine/mem-retrieval/rerank.d.ts +0 -13
  139. package/lib/types/client/domains.d.ts +0 -53
  140. package/lib/types/client/domains.d.ts.map +0 -1
  141. package/lib/types/client/ingest.d.ts +0 -24
  142. package/lib/types/client/ingest.d.ts.map +0 -1
  143. package/lib/types/open.d.ts +0 -77
  144. package/lib/types/open.d.ts.map +0 -1
  145. package/lib/types/reconcile.d.ts +0 -64
  146. package/lib/types/reconcile.d.ts.map +0 -1
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Structured relations a WRITE declares (docs/STRUCTURED_RELATIONS.md §2).
3
+ *
4
+ * The caller states `(subject, predicate, object)` — this module turns that statement into the same
5
+ * `RelationCandidate` shape the extraction path produces, or refuses it with a message that names the
6
+ * offending slot. It is deliberately a separate module from `relations/extract.ts`: nothing here
7
+ * GUESSES, so nothing here may silently drop. An explicit relation that cannot be written is a
8
+ * caller mistake (a refusal), not an extraction miss (a drop sample).
9
+ *
10
+ * The gates, in the order they run:
11
+ *
12
+ * 1. **layer.** `kind: "note"` only (§一: the graph expresses stable facts; a journal entry is an
13
+ * episode). The check lives HERE and runs before the write, so the CLI, the tool boundary, the
14
+ * Remote gateway and any embedder get the same refusal.
15
+ * 2. **shape.** Each slot must be non-blank, single-line and bounded, and both entity slots must
16
+ * pass `relations/name-quality.ts` — the same gate the extractor's edges pass. A name gate that
17
+ * only applied to extracted edges would let the one path a CALLER controls put a clause in the
18
+ * graph.
19
+ * 3. **self-relation** after alias resolution (§二). `老张 管理 张伟` with `老张 → 张伟` is not an
20
+ * edge, and for an explicit statement the caller must be told rather than silently ignored.
21
+ *
22
+ * What is deliberately NOT here: the confidence floor and the statement test
23
+ * (`relations/statement.ts`). Those gate a GUESS. An explicit relation carries confidence 1 and the
24
+ * `explicit` source, which is what `relations/confidence.ts#evaluateRelation` reads to skip them.
25
+ *
26
+ * @module store/explicit_relations
27
+ */
28
+ import { type MemoryKind, type RelationInput } from '../../mem-contract/index.js';
29
+ import type { Db } from '../db/port.js';
30
+ import { type RelationCandidate } from '../relations/index.js';
31
+ /**
32
+ * The refusal for `journal` + `relations` (§一/§二).
33
+ *
34
+ * It names the way out, for the same reason {@link JOURNAL_NEEDS_EVENT_DATE} does: the caller has
35
+ * already decided the relation is worth keeping, and the only thing missing is which layer to put it
36
+ * in.
37
+ */
38
+ export declare const RELATIONS_NEED_NOTE: string;
39
+ /**
40
+ * Validate + normalize the `relations` of one write, or refuse.
41
+ *
42
+ * Returns `[]` when nothing was given (an omitted or EMPTY array is "no relations", §二), so the
43
+ * caller can treat the result uniformly. The thrown messages are caller-facing: they name the
44
+ * layer, the index and the slot, because a model or an operator has to be able to fix the call.
45
+ *
46
+ * @param db - the store's connection (needed only for the read-only alias resolution of §二's
47
+ * self-relation rule).
48
+ * @param kind - the layer the revision will actually carry; the caller resolves it (for `update`
49
+ * that is the replaced row's kind when the request omits one).
50
+ * @param relations - what the caller passed, if anything.
51
+ */
52
+ export declare function explicitRelationTriples(db: Db, kind: MemoryKind, relations: readonly RelationInput[] | undefined): RelationCandidate[];
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * RESTORE IS NOT IMPORT. `import` / `merge` (see `facts_io.ts`) are CONTENT-IDENTITY UNIONS: new
6
6
  * rows get new ids, only ACTIVE facts travel, and every value the document carries beyond
7
- * category/ttl/sources/validity is counted as dropped. Restore is the opposite promise — "put this
7
+ * kind/ttl/sources/validity is counted as dropped. Restore is the opposite promise — "put this
8
8
  * library back exactly as it was" — which is only answerable if the target is EMPTY:
9
9
  *
10
10
  * - `facts` is written DIRECTLY, one transaction, keeping `fact_id`, status, timestamps,
@@ -41,6 +41,14 @@
41
41
  * Zero would make every restored fact decay by the host's whole clock in one settlement; the current
42
42
  * clock is the "no decay has happened yet" baseline, which is what a fresh restore means.
43
43
  *
44
+ * V5 DECLARED RELATIONS ARE RESTORED, AS EDGES. A note's `relations` (the relations a caller stated,
45
+ * `source: 'explicit'`) are written into `edges` inside the same transaction as the fact. They are
46
+ * the one part of the graph the entity sweep cannot re-derive (that pass re-creates extracted edges
47
+ * and PRESERVES explicit ones), so treating them like the rest of the derived state would lose them.
48
+ * The field is counted in `metadata_missing` for notes only: a journal refuses relations by contract,
49
+ * so an absent key on a journal row states the truth instead of a loss. A journal row that DOES carry
50
+ * them is refused outright — such a row cannot be represented in this schema.
51
+ *
44
52
  * @module store/facts_restore
45
53
  */
46
54
  import type { FactsExportDocument } from '../facts_io.js';
@@ -56,7 +64,7 @@ export declare const RESTORE_VECTOR_HINT: string;
56
64
  * rather than trust). A field counts as missing when the document does not carry the key at all —
57
65
  * `null` is a value the document states ("unknown"), not an absence.
58
66
  */
59
- export declare const RESTORE_METADATA_FIELDS: readonly ["category", "created_at", "updated_at", "trust_score", "pinned", "pinned_at", "assert_count", "helpful_count", "last_reinforced_at", "ttl_days", "valid_from", "valid_to", "supersedes_source_id", "archive", "sources", "settle_clock", "bonus_count", "bonus_window_at", "retrieval_count", "last_retrieved_at", "mirror_source", "mirror_target", "entities_version", "conflict_checked"];
67
+ export declare const RESTORE_METADATA_FIELDS: readonly ["kind", "title", "created_at", "updated_at", "trust_score", "pinned", "pinned_at", "assert_count", "helpful_count", "last_reinforced_at", "ttl_days", "valid_from", "valid_to", "supersedes_source_id", "archive", "sources", "settle_clock", "bonus_count", "bonus_window_at", "retrieval_count", "last_retrieved_at", "mirror_source", "mirror_target", "entities_version", "conflict_checked", "relations"];
60
68
  export type RestoreMetadataField = (typeof RESTORE_METADATA_FIELDS)[number];
61
69
  /** Per-field count of facts whose document did not carry that field. */
62
70
  export type RestoreMetadataMissing = Record<RestoreMetadataField, number>;
@@ -99,7 +99,10 @@ export type FloorLeg = 'semantic' | 'fts' | 'jaccard';
99
99
  * that cannot produce terms is not graded at all, and one that produces a single term (every
100
100
  * 3-char CJK query, and every 2-char one through the substring fallback) is already graded
101
101
  * against that one term by the reachability clamp.
102
- * - jaccard **0.15** (positive — the previous `0` was the ungated hole this closes). The metric was
102
+ * - jaccard **0.15** (positive — the previous `0` was the ungated hole this closes). This is the
103
+ * LOWER BOUND of the entity leg's evidence bar: the leg also applies a per-candidate anchor-rarity
104
+ * bar in the same metric unit (`entityEvidenceFloor`, 2026-10-21), so "relaxed" cannot re-admit a
105
+ * generic anchor's wide-fact tail — the structural relation is not a relevance threshold. The metric was
103
106
  * recalibrated on 2026-10-04 (`store/entity_leg.ts`): the leg still scores a Jaccard ratio, but
104
107
  * over the query's ANCHOR entities and with the fact's extra-entity term SATURATED at
105
108
  * `ENTITY_UNION_CAP`. 0.15 on that unit means "shares at least one discriminative entity" for any
@@ -0,0 +1,146 @@
1
+ /**
2
+ * The memory adapter for the shared `@avantf/graph-recall` port
3
+ * (docs/GRAPH_RECALL_LIBRARY.md §6, Phase 3b).
4
+ *
5
+ * WHY THIS FILE EXISTS. The library owns the traversal algorithms (one hop, reverse, two hops, AND,
6
+ * neighbours) and knows nothing about SQLite; memory owns the storage and therefore must supply a
7
+ * {@link GraphReader}. That is this class and nothing more: it maps `edges` + `entities` +
8
+ * `predicates` rows onto `GraphEdge` / `GraphNode` and owns EVERY filtering decision the library
9
+ * refuses to guess.
10
+ *
11
+ * THE DIVISION OF LABOUR, spelled out because it is the whole point of Phase 3:
12
+ *
13
+ * - **The reader filters.** Only `facts.status = 'active'` rows are edges of the live graph; a
14
+ * `kind`, `source` (`fact_sources.ref`) or `time` filter is applied inside the SQL, before any
15
+ * `LIMIT`, exactly like the retrieval legs do it (`store/memory.ts#SearchInput`). A superseded
16
+ * revision's edges are deleted by the write path, so "archived" here means a row whose status
17
+ * moved after the fact was written — the contradiction/supersede paths — and it must not be
18
+ * walkable.
19
+ * - **The reader normalizes names.** `resolveNode` follows `relations/normalize.ts`'s order
20
+ * (explicit alias, then exact name, then — last, so a name never shadows an id — the numeric
21
+ * `entity_id`); `resolvePredicate` follows the same alias-then-name order. Walkers call
22
+ * `resolvePredicate` before reading, so an alias (`负责`) and its canonical (`管理`) are one hop
23
+ * predicate, never two.
24
+ * - **Direction is the STORED one.** `outgoing` reads `subject_id = ?`, `incoming` reads
25
+ * `object_id = ?`, and the adapter never flips endpoints: a predicate registered as `inverse` was
26
+ * already stored in its canonical orientation by the write path (`db/graph.ts`), which is what
27
+ * lets every walker read edges left to right. The ONE exception is a SYMMETRIC relation (`合作`):
28
+ * it is stored once, in the order the sentence used, and a predicate-filtered read anchors on
29
+ * either endpoint, so both ends of the relation answer (see `read`).
30
+ * - **Evidence is the fact.** `GraphEdge.factId` is `edges.fact_id`, so a `RelationPath.evidence`
31
+ * points at the row a caller can open (`mem_admin detail`), not at an edge id only this adapter
32
+ * understands.
33
+ * - **The vocabulary is the adapter's too.** `entitiesIn` / `predicatesIn` (Phase 4) are the
34
+ * `RelationVocabulary` port the planner reads: which known surfaces occur where in a query. Those
35
+ * lookups belong here because they are the same normalization (`entity_aliases` first, then
36
+ * `entities.name`; alias-then-canonical for predicates) the walks use.
37
+ *
38
+ * WHAT IS DELIBERATELY ABSENT: no query planner (that is `store/relation_planner.ts`), no answer
39
+ * generation, no new action, and no change to `titles`. `MemoryStore` builds one of these per search
40
+ * for the relation stage — the class stays an adapter with no policy of its own — and it is still not
41
+ * exported from the engine's front door, the same status `db/graph.ts` has: reachable by module path,
42
+ * not a published promise.
43
+ *
44
+ * @module store/graph_reader
45
+ */
46
+ import type { GraphEdge, GraphNode, GraphReader } from '../../graph-recall/index.js';
47
+ import type { MemoryKind, TimeFilter } from '../../mem-contract/index.js';
48
+ import type { Db } from '../db/port.js';
49
+ import { type SurfaceMatch } from './relation_planner.js';
50
+ /**
51
+ * Hard ceiling on one read's `LIMIT`.
52
+ *
53
+ * The library over-fetches (`limit × 5`, capped at its own `MAX_SCAN = 500`) and never calls a read
54
+ * without a limit, so this is the bound an omitted `limit` falls back to rather than a second policy:
55
+ * an unbounded `SELECT * FROM edges WHERE subject_id = ?` on a hub entity would be a table scan, and
56
+ * "at most `limit`" is the contract's promise, not "all of them".
57
+ */
58
+ export declare const MAX_GRAPH_EDGE_READ = 500;
59
+ /**
60
+ * Which facts a reader may walk.
61
+ *
62
+ * Every field is ANDed with the others and with `status = 'active'`; an omitted field filters
63
+ * nothing. An EMPTY list is not "no restriction" — a kind/source allowlist that names nothing
64
+ * permits nothing, and silently widening it to the whole corpus is the failure mode a filter exists
65
+ * to prevent.
66
+ */
67
+ export interface MemoryGraphFilter {
68
+ /** `facts.kind` values to keep (the two memory layers, see `store/kinds.ts`). */
69
+ kinds?: readonly MemoryKind[];
70
+ /**
71
+ * `fact_sources.ref` values to keep — the same `source` axis `mem_recall.search` exposes (P-08).
72
+ *
73
+ * Note the write-side spelling: `mem_remember source_ref: 'session:abc'` records kind `session`,
74
+ * ref `abc` (`dao/fact_sources.ts#classifySourceRef`), and `ref` is what this filter compares —
75
+ * the same value `mem_recall`'s `source=` matches.
76
+ */
77
+ sources?: readonly string[];
78
+ /**
79
+ * The caller's explicit time filter, on `facts` (`store/time_filter.ts`'s normalizer, i.e. the very
80
+ * same grammar and axes `mem_recall` uses: `created` / `updated` / `event`).
81
+ */
82
+ time?: TimeFilter;
83
+ }
84
+ /**
85
+ * A read-only {@link GraphReader} over a memory database.
86
+ *
87
+ * Construct one per query — it holds a filter and a connection, no mutable state, so concurrent
88
+ * readers on one connection cannot interfere — and hand it to the `@avantf/graph-recall` operators:
89
+ *
90
+ * ```ts
91
+ * const reader = new MemoryGraphReader(db, { kinds: ['note'] })
92
+ * const zhang = reader.resolveNode('张伟')
93
+ * if (zhang !== null) {
94
+ * const { paths } = twoHop(reader, zhang, '管理')
95
+ * for (const path of paths) console.log(path.evidence) // fact ids
96
+ * }
97
+ * ```
98
+ */
99
+ export declare class MemoryGraphReader implements GraphReader {
100
+ private readonly db;
101
+ /** The normalized time range, parsed ONCE in the constructor so a bad `time` fails fast. */
102
+ private readonly timeRange;
103
+ /** `undefined` = the axis is not filtered; `[]` = an allowlist that names nothing (matches nothing). */
104
+ private readonly kinds;
105
+ private readonly sources;
106
+ constructor(db: Db, filter?: MemoryGraphFilter);
107
+ resolveNode(ref: string): GraphNode | null;
108
+ resolvePredicate(ref: string): string | null;
109
+ /**
110
+ * The entity surfaces occurring in `text` — the planner's vocabulary port (`store/relation_planner.ts`).
111
+ *
112
+ * Only entities the graph can actually walk are reported: an entity with no `edges` row can never
113
+ * answer a relation question, so including it would let a plan form and then come back empty (a
114
+ * fuzzy fallback with extra work), and scanning the whole `entities` table on every search would
115
+ * pay for names the relation stage can never use. The alias rows are narrowed the same way.
116
+ *
117
+ * Filtering (kind/source/time) is deliberately NOT applied to this lookup: the vocabulary answers
118
+ * "is this name an entity at all", while the paths are filtered by the edge SQL. A name whose only
119
+ * edges are outside the caller's window still forms a plan — and then yields no path, which is the
120
+ * documented fallback to fuzzy.
121
+ */
122
+ entitiesIn(text: string): SurfaceMatch[];
123
+ /** The predicate surfaces occurring in `text` (canonical names and aliases) — same port as above. */
124
+ predicatesIn(text: string): SurfaceMatch[];
125
+ outgoing(node: GraphNode, predicate?: string, limit?: number): GraphEdge[];
126
+ incoming(node: GraphNode, predicate?: string, limit?: number): GraphEdge[];
127
+ edgesForFact(factId: string | number): GraphEdge[];
128
+ /** One alias-or-canonical-name lookup; `undefined` when the predicate is not in the vocabulary. */
129
+ private predicateRow;
130
+ /**
131
+ * Shared read behind {@link outgoing} / {@link incoming} / {@link edgesForFact}.
132
+ *
133
+ * `anchorColumn` is the ONE difference between the three reads (`subject_id`, `object_id`,
134
+ * `fact_id`), which is why they cannot drift apart in their filters: active status, kind, source,
135
+ * time and the page bound are appended in exactly one place.
136
+ */
137
+ private read;
138
+ /**
139
+ * Build one bounded, filtered edge read.
140
+ *
141
+ * Bind order is the SQL's own: kind list, source list, time bounds, anchor, predicate, limit.
142
+ * `ORDER BY e.edge_id` is insertion order — stable across calls, which is what the port asks for
143
+ * ("the library sorts by confidence/hops/recency itself").
144
+ */
145
+ private edgeQuery;
146
+ }
@@ -4,7 +4,7 @@
4
4
  * `MemoryStore.search` and `KnowledgeStore.search` used to carry parallel copies of this flow
5
5
  * (~80 lines each: limit normalization → weights → over-fetch → leg cap → fuse → live filter →
6
6
  * slice → output budget → health event). The legs themselves are legitimately store-specific —
7
- * different tables, different filters (`category` vs `domain`/`source`), and only memory has the
7
+ * different tables, different filters (`kind` vs `domain`/`source`), and only memory has the
8
8
  * HRR probe — but the ORCHESTRATION is one algorithm, and two copies of it drifted:
9
9
  *
10
10
  * - the `NaN` limit guard existed only in memory, so a non-finite limit reached knowledge's SQL
@@ -25,6 +25,7 @@
25
25
  */
26
26
  import { type Config, type FloorProfile, type RecallResult, type RetrievalFloorDrops, type RetrievalFloors } from '../../mem-contract/index.js';
27
27
  import { type BudgetInput, type Hit, type SemanticBackend } from '../../mem-retrieval/index.js';
28
+ import type { Awaitable } from '../interfaces/common.js';
28
29
  import { type FloorLeg } from './floors.js';
29
30
  /**
30
31
  * A retrieval input the CALLER got wrong, as opposed to a backend that failed at runtime.
@@ -107,8 +108,8 @@ export interface HybridContext {
107
108
  }
108
109
  /** What a store supplies. Generic in `H`, the caller-facing hit shape. */
109
110
  export interface HybridDeps<H> {
110
- /** Which health-event kind this search records. */
111
- kind: 'memory' | 'knowledge';
111
+ /** Which health-event bucket this search records (`retrieval.by_kind`). */
112
+ kind: 'memory';
112
113
  config: Config;
113
114
  semantic: SemanticBackend;
114
115
  /**
@@ -118,10 +119,10 @@ export interface HybridDeps<H> {
118
119
  * body, before the array is returned.
119
120
  */
120
121
  legs(ctx: HybridContext): Promise<readonly (HybridLeg | Promise<HybridLeg>)[]>;
121
- /** Text for the fused ids: the live filter and the hit bodies. */
122
- texts(ids: number[]): Map<number, string>;
122
+ /** Text for the fused ids: the live filter and the hit bodies (a repository read — DB-2). */
123
+ texts(ids: number[]): Awaitable<Map<number, string>>;
123
124
  /** Build the caller-facing hits from the ranked, live, already-sliced ids. */
124
- hits(ranked: readonly Hit[], texts: Map<number, string>): H[];
125
+ hits(ranked: readonly Hit[], texts: Map<number, string>): Awaitable<H[]>;
125
126
  /**
126
127
  * Called with the hits the caller ACTUALLY receives — after the output budget, not before it.
127
128
  *
@@ -130,7 +131,7 @@ export interface HybridDeps<H> {
130
131
  * dropped, i.e. rewarding facts the caller never saw; the cross-store path had already learned
131
132
  * that lesson (R5/R21) and the single-store path had not.
132
133
  */
133
- onReturn?(kept: readonly Budgeted<H>[]): void;
134
+ onReturn?(kept: readonly Budgeted<H>[]): Awaitable<void>;
134
135
  }
135
136
  /** One search request, before resolution. */
136
137
  export interface HybridPlan {
@@ -168,6 +169,20 @@ export interface HybridPlan {
168
169
  * too (relaxing a leg that dropped nothing admits no candidate; it only misreports `floors`).
169
170
  */
170
171
  relaxLegs?: readonly FloorLeg[];
172
+ /**
173
+ * Overrides `retriever.min_score` for this search (`0` = off). The UNIFIED cross-store entry
174
+ * (`runtime.retrieve`) passes `0` here and applies the score floor ONCE, after the merged pool has
175
+ * been normalized, because a per-store floor runs on a different scale in each store (DESIGN §7).
176
+ * A direct store search leaves this absent and keeps the configured value.
177
+ */
178
+ minScore?: number;
179
+ /**
180
+ * READ-ONLY probe mode (`AvantfRuntime.wouldFind`, DESIGN §12): no model warm, no leg-capped
181
+ * counters, no retrieval event and no `onReturn` reinforcement. The legs themselves are the same
182
+ * ones a user query runs, because the probe's answer must be exactly "would retrieval return a
183
+ * hit?"; only the SIDE EFFECTS are suppressed.
184
+ */
185
+ probe?: boolean;
171
186
  /**
172
187
  * Emit a health event for this search (default true). The cross-store router sets it false: it
173
188
  * fuses both stores into ONE user-facing query and records a single `kind: 'cross'` event, so
@@ -196,6 +211,24 @@ export interface HybridResult<H> {
196
211
  floors: RetrievalFloors;
197
212
  /** How many candidates each leg dropped below its floor. */
198
213
  dropped_by_floor: RetrievalFloorDrops;
214
+ /**
215
+ * Candidates the FUSED-score floor (`retriever.min_score`) removed after the live filter.
216
+ *
217
+ * Deliberately a scalar, not a `RetrievalFloorDrops` member: a score drop happens AFTER all legs
218
+ * have been fused, so there is no leg to attribute it to. Folding it into the per-leg structure
219
+ * would also make the retry rule and the panel's "the floors removed N" claim describe a rule that
220
+ * did not remove the candidate.
221
+ *
222
+ * Absent when nothing was dropped (including whenever `min_score` is off). The count covers every
223
+ * live candidate below the threshold, including ones the `limit` slice would have discarded anyway,
224
+ * because the threshold is applied before the slice. It is the CHOSEN pass's count: when
225
+ * {@link HybridResult.relaxed} is true it describes the relaxed pass, like `floors`.
226
+ *
227
+ * The UNIFIED entry (`runtime.retrieve`) passes `minScore: 0` and applies the floor once, after the
228
+ * merged-pool normalization (`router.crossQuery`), so this field is absent on that path; the
229
+ * router's own `dropped_by_score` is the cross-store count (DESIGN §7).
230
+ */
231
+ dropped_by_score?: number;
199
232
  /**
200
233
  * The strict pass returned nothing while having dropped candidates, and ONE relaxed pass supplied
201
234
  * these hits — `floors` are that pass's values. An explicit `floors: 'loose'` request does NOT set
@@ -233,11 +266,15 @@ export declare function hybridSearch<H extends BudgetInput>(deps: HybridDeps<H>,
233
266
  /**
234
267
  * The search-entry fields `MemoryStore.search` and `KnowledgeStore.search` hand to the orchestrator.
235
268
  * Both stores' option types (`SearchInput` / `KnowledgeSearchOptions`) satisfy this structurally;
236
- * everything else on them (`category` / `source` / `domain` / `includeHrr` / `track` / `onResult`) is
269
+ * everything else on them (`kind` / `source` / `domain` / `includeHrr` / `track` / `onResult`) is
237
270
  * leg- or domain-specific and stays at the store.
238
271
  */
239
272
  export interface HybridSearchEntryFields {
240
273
  limit?: number;
274
+ /** See {@link HybridPlan.minScore}. `0` = the caller applies the score floor itself. */
275
+ minScore?: number;
276
+ /** See {@link HybridPlan.probe}: read-only, no side effects. */
277
+ probe?: boolean;
241
278
  /**
242
279
  * Knowledge only: overrides the config-derived pool size. Memory's `SearchInput` has no such field
243
280
  * and leaves the plan key absent (see {@link runHybridSearch}).
@@ -274,9 +311,9 @@ export declare function runHybridSearch<H extends BudgetInput>(deps: HybridDeps<
274
311
  */
275
312
  export declare function hybridPlanOf(query: string, fields: HybridSearchEntryFields | undefined): HybridPlan;
276
313
  /**
277
- * Build a store's {@link HybridDeps} — the ONE construction site for the contract fields both stores
278
- * share (`kind` / `config` / `semantic` / `legs` / `texts` / `hits` / `onReturn`). The per-store half
279
- * is `parts`; the stores used to spell the whole object out twice, so a new required `HybridDeps`
280
- * field was a change to remember in two places.
314
+ * Build a store's {@link HybridDeps} — the ONE construction site for the fields every caller shares
315
+ * (`kind` / `config` / `semantic` / `legs` / `texts` / `hits` / `onReturn`). The per-caller half is
316
+ * `parts`; a store built on these legs passes its own `kind` and parts, so a new required
317
+ * `HybridDeps` field does not have to be remembered in more than one place.
281
318
  */
282
- export declare function makeHybridDeps<H>(kind: 'memory' | 'knowledge', config: Config, semantic: SemanticBackend, parts: Pick<HybridDeps<H>, 'legs' | 'texts' | 'hits' | 'onReturn'>): HybridDeps<H>;
319
+ export declare function makeHybridDeps<H>(kind: 'memory', config: Config, semantic: SemanticBackend, parts: Pick<HybridDeps<H>, 'legs' | 'texts' | 'hits' | 'onReturn'>): HybridDeps<H>;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The memory LAYERS at the WRITE boundary (JOURNAL_NOTES_REDESIGN N1/N6/N7).
3
+ *
4
+ * `kind` replaced the eight-value `category` vocabulary: a fact is either a `journal` entry (what was
5
+ * done / talked about / thought about, anchored by an event date) or a `note` (a stable fact,
6
+ * preference, decision or pitfall). Three layers agree, each with a distinct job:
7
+ *
8
+ * 1. the contract's `z.enum(MEMORY_KINDS)` — the MODEL cannot even phrase an unknown value;
9
+ * 2. {@link kindOrThrow} — the ENGINE refuses one, so the CLI, the UI, an import and an embedder
10
+ * (none of which pass through the tool schema) get the same answer;
11
+ * 3. `facts.kind`'s CHECK constraint (the base DDL) — the DATABASE refuses one, which is what makes
12
+ * the rule hold for a writer this build does not control (an older process sharing the file).
13
+ *
14
+ * There is no lookup table any more (the old `categories` table is dropped by migration 13): two
15
+ * values do not need to be stored as data, and the schema test would have to keep them in step.
16
+ *
17
+ * @module store/kinds
18
+ */
19
+ import { type MemoryKind } from '../../mem-contract/index.js';
20
+ /** The EPISODIC layer: one row per episode, and it cannot exist without its event time. */
21
+ export declare const JOURNAL_KIND: MemoryKind;
22
+ /** The SEMANTIC layer — also what an omitted `kind` means on a new row (CLI / import / base). */
23
+ export declare const NOTE_KIND: MemoryKind;
24
+ /** Whether `value` is one of the two layers (the engine's own guard). */
25
+ export declare function isMemoryKind(value: string): value is MemoryKind;
26
+ /**
27
+ * Validate a caller's `kind`, or refuse with the allowed set.
28
+ *
29
+ * Whitespace/case are NOT "helpfully" accepted: `kind` is an identity (the column's CHECK compares it
30
+ * verbatim), and silently folding `Journal` into `journal` would make the stored value depend on which
31
+ * layer normalized it.
32
+ */
33
+ export declare function kindOrThrow(value: string): MemoryKind;
34
+ /**
35
+ * Map ANY historical value onto the two layers. Used where the value is not a fresh caller choice but
36
+ * data written by an older build (a snapshot restore, an old export, a pre-migration database), so
37
+ * refusing it would refuse the whole file over a label.
38
+ *
39
+ * `worklog` (the one episodic value in the published 16-value set, and in the eight-value experiment)
40
+ * is `journal`; every other value is `note` — the old vocabularies were topic taxonomies, and "the
41
+ * topic of a fact" says nothing about whether it is an episode.
42
+ */
43
+ export declare function mapLegacyKind(value: unknown): MemoryKind;
44
+ /** `true` when this layer cannot be written without an event date (N6). */
45
+ export declare function requiresEventDate(kind: string): boolean;
46
+ /** The refusal text for a `journal` write without `event_date` — one sentence, with the way out. */
47
+ export declare const JOURNAL_NEEDS_EVENT_DATE: string;
@@ -40,7 +40,7 @@
40
40
  */
41
41
  import { type FloorLeg } from './floors.js';
42
42
  import { type HybridContext, type HybridLeg } from './hybrid.js';
43
- import { type FtsTokenizer } from '../db/tokenizer.js';
43
+ import type { Awaitable } from '../interfaces/common.js';
44
44
  /**
45
45
  * The legs {@link LegRunner.scored} may wrap: those whose raw score has an absolute scale a floor
46
46
  * can cut. `semantic` is excluded on purpose — it shares `applyScoreFloor` but must not carry the
@@ -94,44 +94,39 @@ export declare function makeLegRunner(ctx: Pick<HybridContext, 'legCap'>): LegRu
94
94
  /** The per-store pieces of the FTS leg — see {@link ftsLeg}. `Scope` is opaque to the flow. */
95
95
  export interface FtsLegOptions<Scope> {
96
96
  query: string;
97
- /** The per-leg row cap (the store's `legCap`), handed to whichever DAO statement runs. */
97
+ /** The per-leg row cap (the store's `legCap`), handed to whichever backend statement runs. */
98
98
  cap: number;
99
- /** The store's scope shape (`{category, source}` / `{domain, source}`), passed straight through. */
99
+ /** The store's scope shape (`{kind, source}` / `{domain, source}`), passed straight through. */
100
100
  scope: Scope;
101
- /** The table's own tokenizer, for the `MATCH` build (`buildFtsQuery`). */
102
- tokenizer: FtsTokenizer;
103
- /** The FTS `MATCH` statement; its `rank` is bm25 (negative = better). */
104
- search: (ftsQuery: string, scope: Scope, limit: number) => readonly {
105
- id: number;
106
- rank: number;
107
- }[];
108
- /** The short-query LIKE statement; its `rank` counts the terms the row contains (higher = better). */
109
- substringSearch: (terms: readonly string[], scope: Scope, limit: number) => readonly {
101
+ /**
102
+ * The store's lexical search, handed the CALLER'S QUERY.
103
+ *
104
+ * The backend decides how to express it (a full-text `MATCH`, or the short-query substring
105
+ * fallback when the index cannot express the text at all) and normalizes its own rank convention
106
+ * so `rank` is always HIGHER IS BETTER. That is the DB-3 boundary: neither the tokenizer nor
107
+ * bm25's sign crosses into the store.
108
+ */
109
+ search: (query: string, scope: Scope, limit: number) => Awaitable<readonly {
110
110
  id: number;
111
111
  rank: number;
112
- }[];
112
+ }[]>;
113
113
  }
114
114
  /**
115
115
  * The FTS leg, shared by `MemoryStore.ftsPath` and `KnowledgeStore.ftsPath` (§4.6.1).
116
116
  *
117
- * The two were line-for-line the same flow — `buildFtsQuery` → the `MATCH` statement with NEGATED
118
- * bm25, else `substringTerms` → the LIKE statement with `rank` — differing only in the DAO and the
119
- * order its scope parameters bind. Both conventions are load-bearing and preserved verbatim:
117
+ * The two were line-for-line the same flow — build the index expression, run it, map hits to scores
118
+ * — differing only in the DAO and the order its scope parameters bind. What stays here is the shape
119
+ * both flows share: every hit becomes a score, and the leg is a `Map` in the backend's own order.
120
120
  *
121
- * - **bm25 is negative** (more negative = better match), so the `MATCH` branch negates it and higher
122
- * stays better everywhere downstream;
123
- * - **the LIKE `rank`** already counts contained terms, so it is used as-is, keeping the two branches
124
- * on the same "higher = better" scale;
125
- * - **an empty term list returns the empty map** (`substringTerms` yields none for a query with no
126
- * 3-char run) — the leg is empty by construction, not by a query that matched nothing.
121
+ * THE SHORT-QUERY FALLBACK moved DOWN into the backend in DB-3. `buildFtsQuery` returning `null`
122
+ * (a 2-char CJK query the trigram vocabulary cannot express) used to leave this leg empty by
123
+ * construction; the SQLite adapter now answers that shape with the bounded substring predicate
124
+ * itself, on the same "higher is better" scale, so the store has one call instead of a branch.
127
125
  *
128
- * THE SHORT-QUERY FALLBACK. `buildFtsQuery` returning `null` means the query carries no term the
129
- * trigram index can express (measured: every term in the FTS table is 3 characters), which used to
130
- * leave this leg empty by construction — the 2-char CJK shape. `substringTerms` is the one finer
131
- * predicate the FTS5 trigram TABLE still answers (`LIKE '%…%'`), bounded by `cap`; the accuracy guard
132
- * is the per-row term floor in `applyTermFloor`, which requires the row to CONTAIN the run.
126
+ * A backend that matches nothing returns an empty map — the leg is empty by construction, not by a
127
+ * query that matched nothing.
133
128
  */
134
- export declare function ftsLeg<Scope>(opts: FtsLegOptions<Scope>): Map<number, number>;
129
+ export declare function ftsLeg<Scope>(opts: FtsLegOptions<Scope>): Promise<Map<number, number>>;
135
130
  /** The per-store pieces of the semantic leg — see {@link semanticLeg}. */
136
131
  export interface SemanticLegOptions {
137
132
  query: string;
@@ -155,7 +150,7 @@ export interface SemanticLegOptions {
155
150
  * short-circuit (no lookup runs at all). A returned set is the allow-list; the flow still maps the
156
151
  * pool in ITS order, so ids/scores/insertion order are the vstore's.
157
152
  */
158
- filterTopk: (ids: readonly number[]) => ReadonlySet<number> | undefined;
153
+ filterTopk: (ids: readonly number[]) => Awaitable<ReadonlySet<number> | undefined>;
159
154
  }
160
155
  /**
161
156
  * The semantic leg, shared by `MemoryStore.semanticPath` and `KnowledgeStore.semanticPath` (§4.6.2).