@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
@@ -0,0 +1,49 @@
1
+ /** A query needs this many CJK characters for a literal content match to mean anything. */
2
+ export declare const EXACT_MIN_CJK = 2;
3
+ /** …or this many Latin letters (case-folded for the comparison). */
4
+ export declare const EXACT_MIN_LATIN = 3;
5
+ /** The default page of an exact result, and its hard cap. */
6
+ export declare const EXACT_DEFAULT_LIMIT = 10;
7
+ export declare const EXACT_MAX_LIMIT = 50;
8
+ /**
9
+ * The widest literal match set that still counts as an ANSWER (P1-2, REVIEW-2026-10-24).
10
+ *
11
+ * A ten-character sentence that occurs verbatim in a few facts is a literal answer; a common word
12
+ * (`mem`, `插件`) that occurs in dozens is a corpus scan, and a page of it says nothing a fuzzy
13
+ * shortlist would not say better. So the literal stage refuses a `total` above this bound and lets
14
+ * the rest of the pipeline answer: the relation stage may still plan it, otherwise fuzzy runs.
15
+ *
16
+ * The number is the page cap's neighbourhood (10 default, 50 max): 20 literal matches is already
17
+ * "everything that mentions this word", which is not what "字面命中" promises.
18
+ */
19
+ export declare const EXACT_MAX_TOTAL = 20;
20
+ /** The fuzzy shortlist: how many rows, and the fused-score floor they must clear. */
21
+ export declare const FUZZY_TOP_K = 5;
22
+ export declare const FUZZY_MIN_SCORE = 0.5;
23
+ /** How many Han characters `text` contains. */
24
+ export declare function cjkCount(text: string): number;
25
+ /** How many Latin letters `text` contains. */
26
+ export declare function latinCount(text: string): number;
27
+ /**
28
+ * Is this (already normalized) query long enough to trigger the literal pass?
29
+ *
30
+ * A mixed query passes on EITHER bar: 「DSH 插件」 is 2 CJK (and 3 Latin) and is a real phrase, while
31
+ * 「a记忆」 is one Latin and one CJK and is not a phrase in either sense.
32
+ */
33
+ export declare function exactEligible(query: string): boolean;
34
+ /**
35
+ * The query a literal comparison uses: well-formed + NFC (`normalizeWrite`, the store's one write
36
+ * normalization) then trimmed. Trimming is what keeps a pasted `" 迁移 "` from missing its row.
37
+ */
38
+ export declare function normalizeExactQuery(query: string): string;
39
+ /**
40
+ * The page an exact result reports, from the caller's `offset`/`limit`.
41
+ *
42
+ * Both are clamped rather than refused: they are pagination, not a filter, and a caller asking for
43
+ * page 100 of a 3-row result should get an empty page with an honest `total`, not an error. The cap
44
+ * is the contract's own (max 50) — a page bigger than that is answered with 50.
45
+ */
46
+ export declare function exactPage(offset: number | undefined, limit: number | undefined): {
47
+ offset: number;
48
+ limit: number;
49
+ };
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Controlled relation backfill for the LEGACY corpus (docs/RELATION_BACKFILL.md §七, Phase 7).
3
+ *
4
+ * The problem this file solves is a REVIEW problem, not an extraction one. Phase 5b measured that
5
+ * live notes are mostly not statements, so a deterministic extractor cannot reach them without
6
+ * loosening the name/statement gates — which the plan refuses (§四). Instead a human/agent READS the
7
+ * notes, writes candidate relations with the exact text that says so, and an operator approves them.
8
+ * This module is the deterministic, auditable machinery around that loop:
9
+ *
10
+ * 1. `proposeRelations` — read-only selection of the notes that need review, as a portable JSON
11
+ * document. It never calls a model and never writes a row.
12
+ * 2. `parseRelationProposal` — the strict reader for a reviewed file. Strict because the file is
13
+ * HAND-EDITED: a silently ignored typo (`aproved: true`) would look like a reviewed entry that
14
+ * the tool refused for another reason.
15
+ * 3. `applyRelationProposal` — per-entry validation + write through the EXISTING `remember.update`
16
+ * (content passed back verbatim, so the row is not superseded and `fact_id` is preserved).
17
+ * One bad entry is reported, never fatal: a batch of 50 reviewed notes must not be lost to the
18
+ * one entry whose source text changed under the reviewer.
19
+ * 4. `relationCoverage` — how much of the active NOTE corpus carries caller-stated relations.
20
+ *
21
+ * WHAT THIS FILE DELIBERATELY DOES NOT DO:
22
+ *
23
+ * - **No LLM.** The proposal generator is external by contract (§7.2); this module cannot create a
24
+ * relation from fact text and would not be allowed to apply one unattended.
25
+ * - **No second write path.** `apply` calls `MemoryStore.update` with the UNCHANGED content. The
26
+ * store's duplicate-content branch lands on the same row, so `fact_id` survives and the explicit
27
+ * edge set is replaced in place (STRUCTURED_RELATIONS §5). Riding that path has one visible cost:
28
+ * the write counts as a duplicate ASSERTION, so `assert_count` moves by one per applied entry.
29
+ * - **No journal.** A journal is an episode, not a stable fact: `propose` never emits one, the
30
+ * document parser refuses a `kind: "journal"` entry, and `apply` re-checks the LIVE row's kind
31
+ * (a proposal can be stale, and a note can be edited into a journal in between).
32
+ * - **No forged evidence.** Every relation's `evidence` must be a contiguous substring of the live
33
+ * content, and the live content must still be the one the proposal recorded. This is the audit
34
+ * trail §一 asks for: "每一条回填边必须能回查 fact_id + 原文证据".
35
+ * - **No recall surface change.** Nothing here is reachable from `mem_recall`; the CLI `relations`
36
+ * command is the only caller today.
37
+ *
38
+ * ONE INHERITED WRINKLE, stated so it is not mistaken for a bug here: the edge writer is
39
+ * `INSERT OR IGNORE` on `(fact_id, subject, predicate, object)`. When the text extractor ALREADY
40
+ * derived the same relation from the note's content (`source: 'svo'` / `'pattern'`), the explicit
41
+ * write is the duplicate and the edge keeps its derived source — the relation is in the graph and
42
+ * answers questions, but it does not become `explicit`. `apply` notices the shortfall (it counts the
43
+ * fact's explicit edges after the write) and reports the entry as `failed` with that explanation
44
+ * rather than claiming a success it cannot see. `--dry-run` cannot detect it, because the conflict
45
+ * only exists once the write runs; that asymmetry is documented on {@link RelationApplyReport}.
46
+ *
47
+ * @module store/relation_backfill
48
+ */
49
+ import { type FactRelation } from '../../mem-contract/index.js';
50
+ import type { Db } from '../db/port.js';
51
+ import { NOTE_KIND } from './kinds.js';
52
+ import type { MemoryStore } from './memory.js';
53
+ /** The proposal document's version. Bump when the SHAPE changes, and keep the parser strict. */
54
+ export declare const RELATION_PROPOSAL_VERSION = 1;
55
+ /** Notes one `propose` run returns when the caller gives no `--limit` (§7.2's example page). */
56
+ export declare const DEFAULT_PROPOSAL_LIMIT = 20;
57
+ /** One relation a reviewer wants written, with the text that says it. */
58
+ export interface ProposedRelation {
59
+ subject: string;
60
+ predicate: string;
61
+ object: string;
62
+ /** A CONTIGUOUS fragment of the fact's content. Checked against the live row, never trusted. */
63
+ evidence: string;
64
+ /** Reviewer's own ordering hint; never a hidden gate (the write path owns every gate). */
65
+ confidence?: number;
66
+ }
67
+ /** One note in a proposal file: what the reviewer saw, what it proposes, and whether it is approved. */
68
+ export interface ProposedFact {
69
+ fact_id: number;
70
+ /** Notes only — the layer boundary is part of the document's shape, not a runtime check alone. */
71
+ kind: typeof NOTE_KIND;
72
+ /** The content the reviewer read. Compared with the live row before any write (staleness guard). */
73
+ content: string;
74
+ /**
75
+ * The explicit relations the reviewer saw on this note (`[]` for the untouched notes `propose`
76
+ * selects). `apply` requires it to EQUAL the live set: a proposal is a compare-and-swap, so two
77
+ * reviewers (or a reviewer and an `edit --relation`) cannot silently overwrite each other.
78
+ */
79
+ existing_relations: FactRelation[];
80
+ relations: ProposedRelation[];
81
+ /** Defaults to `false` — only an explicit `true` is written (§7.3). */
82
+ approved: boolean;
83
+ }
84
+ /** The portable proposal file (`version` 1). */
85
+ export interface RelationProposalDocument {
86
+ version: typeof RELATION_PROPOSAL_VERSION;
87
+ /** When the file was generated. Absent on a hand-written file; never interpreted as a TTL. */
88
+ generated_at?: string;
89
+ facts: ProposedFact[];
90
+ }
91
+ /** Options for {@link proposeRelations}. */
92
+ export interface RelationProposeOptions {
93
+ /** Page size; defaults to {@link DEFAULT_PROPOSAL_LIMIT}. */
94
+ limit?: number;
95
+ /** The clock the document records — injectable so a test can pin `generated_at`. */
96
+ generatedAt?: Date;
97
+ }
98
+ /** Read-only coverage of caller-stated relations over the ACTIVE note corpus. */
99
+ export interface RelationCoverageReport {
100
+ /** Active notes — the denominator, and the only layer a relation may live on. */
101
+ active_notes: number;
102
+ /** Active notes carrying at least one `source: 'explicit'` edge. */
103
+ notes_with_explicit_relations: number;
104
+ notes_without_explicit_relations: number;
105
+ /** Explicit edge rows on active notes (a note may carry several). */
106
+ explicit_relations: number;
107
+ /** `notes_with_explicit_relations / active_notes`, rounded to three decimals. */
108
+ coverage: number;
109
+ /**
110
+ * Explicit edges on a NON-note row. Must be `0`: a journal has no business in the graph (§四), so
111
+ * a non-zero value means some writer bypassed the layer rule and the graph now contains episodes.
112
+ */
113
+ journal_relations: number;
114
+ }
115
+ /** What one proposal entry came to. */
116
+ export type RelationApplyStatus =
117
+ /** Written; the fact's explicit set was replaced by the proposal's (and verified after the write). */
118
+ 'applied'
119
+ /** `--dry-run`: every check passed, nothing was written. */
120
+ | 'planned'
121
+ /** Not approved — the reviewer left it out, so it is not this run's business. */
122
+ | 'skipped'
123
+ /** Refused before any write: journal / archived / stale content / forged evidence / bad relation. */
124
+ | 'rejected'
125
+ /** The write itself failed, or the explicit set the store kept is not the one the proposal stated. */
126
+ | 'failed';
127
+ /** Per-entry outcome. `reason` is always populated for `skipped`/`rejected`/`failed`. */
128
+ export interface RelationApplyEntryResult {
129
+ fact_id: number;
130
+ status: RelationApplyStatus;
131
+ /** How many relations the entry proposed (what was, or would be, written). */
132
+ relations: number;
133
+ reason?: string;
134
+ /**
135
+ * The fact id the write landed on. Present only for a real write; it must equal `fact_id`
136
+ * (a changed content would open a new revision instead — which `apply` refuses, not performs).
137
+ */
138
+ fact_id_after?: number;
139
+ }
140
+ /**
141
+ * The batch report. `facts` is in DOCUMENT ORDER and `total` counts every entry, so "50 reviewed,
142
+ * 3 rejected" is readable without re-reading the file.
143
+ *
144
+ * `dry_run` is stated on the report rather than inferred from the statuses. The two modes agree on
145
+ * `skipped`/`rejected` (the same validation gate runs) and differ on `applied`/`planned`; the one
146
+ * asymmetry is the post-write edge check, which only a real write can perform (module header).
147
+ */
148
+ export interface RelationApplyReport {
149
+ dry_run: boolean;
150
+ total: number;
151
+ applied: number;
152
+ planned: number;
153
+ skipped: number;
154
+ rejected: number;
155
+ failed: number;
156
+ facts: RelationApplyEntryResult[];
157
+ }
158
+ /** The slice of the store `apply` needs. Narrow on purpose: `Pick<MemoryStore, …>` keeps it honest. */
159
+ export type RelationWriter = Pick<MemoryStore, 'get' | 'update'>;
160
+ /**
161
+ * The notes that need review: `status = 'active'`, `kind = 'note'`, and NO explicit relation yet.
162
+ *
163
+ * §2.1's selection, as SQL. "No explicit relation" is the `edges.source` of the fact — not the
164
+ * extracted triples — because the backfill's question is "has a CALLER ever stated a relation for
165
+ * this note", and an extracted edge is a guess about the text, which is exactly what Phase 5b found
166
+ * insufficient. Ordered by `fact_id` ASC: one stable order across runs, so a reviewed batch and a
167
+ * re-run page describe the same window.
168
+ *
169
+ * The emitted `relations` are empty by construction: filling them is the external reviewer's step
170
+ * (§7.2: "propose 不调用 LLM"), and an empty list is what makes an UNREVIEWED entry unapplicable
171
+ * (`apply` refuses an empty `relations`) rather than an accidental no-op.
172
+ */
173
+ export declare function proposeRelations(db: Db, options?: RelationProposeOptions): RelationProposalDocument;
174
+ /**
175
+ * How much of the active note corpus carries a CALLER-STATED relation (read-only, one report).
176
+ *
177
+ * `journal_relations` is in the report because this is the tool that would expose the violation: the
178
+ * layer rule lives in the write path, and a backfill that reports its own coverage should also say
179
+ * whether anything ELSE put an explicit edge on an episodic row.
180
+ */
181
+ export declare function relationCoverage(db: Db): RelationCoverageReport;
182
+ /**
183
+ * Parse a reviewed proposal file, or refuse the WHOLE file.
184
+ *
185
+ * Whole-file refusal is the rule because nothing here is derived state: a broken document is a
186
+ * caller mistake, and applying "the part that parsed" is exactly the silent partial write §7.4.5
187
+ * forbids. A per-ENTRY problem that a stale row creates is `apply`'s business instead — that one is
188
+ * reported entry by entry so the rest of a reviewed batch still lands.
189
+ *
190
+ * Required per entry: `fact_id` (positive integer), `kind: "note"`, `content` (non-empty). Optional:
191
+ * `existing_relations` (default `[]`), `relations` (default `[]` — an unreviewed entry, which
192
+ * `apply` refuses), `approved` (default `false`). Duplicate `fact_id` entries and duplicate
193
+ * relations inside one entry are refused: both would make "what this file says" ambiguous.
194
+ */
195
+ export declare function parseRelationProposal(text: string): RelationProposalDocument;
196
+ /**
197
+ * Apply a reviewed proposal document (docs/RELATION_BACKFILL.md §7.3).
198
+ *
199
+ * Per entry: `approved !== true` → `skipped`; {@link validateEntry} refuses → `rejected`; otherwise
200
+ * either `planned` (dry run, nothing written) or a real write through `remember.update`.
201
+ *
202
+ * NOT atomic across entries, on purpose (§7.3: "失败条目单独报告,不中断其他条目"). Each entry is one
203
+ * store transaction, so the batch can never be half-written WITHIN an entry; across entries the
204
+ * report is the record of what landed, and re-running the same file is safe: an applied entry's
205
+ * explicit set no longer matches its `existing_relations`, so the second run refuses it as stale
206
+ * rather than writing twice.
207
+ */
208
+ export declare function applyRelationProposal(db: Db, memory: RelationWriter, document: RelationProposalDocument, options?: {
209
+ dryRun?: boolean;
210
+ }): Promise<RelationApplyReport>;
@@ -0,0 +1,191 @@
1
+ /**
2
+ * The deterministic query planner for the relation stage of `mem_recall.search`
3
+ * (docs/RELATIONAL_RECALL.md §6–7, Phase 4).
4
+ *
5
+ * WHAT IT IS. A closed, pure string/position rule set over TWO vocabularies the store already owns —
6
+ * `entities` / `entity_aliases` and `predicates` / `predicate_aliases` — that decides whether a query
7
+ * is a relation QUESTION the graph can answer, and if so which walk answers it:
8
+ *
9
+ * ```text
10
+ * 谁 <pred> <entity> → reverse (谁管理李娜)
11
+ * <entity> <pred> 谁/什么 → one-hop (张伟管理谁)
12
+ * <entity> <pred> 的人 <pred2> 什么 → two-hop (张伟管理的人负责什么)
13
+ * <entity> <pred> → one-hop (张伟管理: bare SPO, no question word)
14
+ * <pred> <entity> → reverse (管理张伟: the same statement, object first)
15
+ * ```
16
+ *
17
+ * THE SURFACE-ONLY RULE (2026-10-24 review, P1-3/P2). The two bare-SPO shapes are only plans when
18
+ * the query is EXACTLY their surfaces: after removing the entity/predicate nothing meaningful may
19
+ * remain. Otherwise a normal content query that happens to mention a known entity would be answered
20
+ * from the graph instead of by retrieval — "张伟管理的缓存层" is a QUESTION ABOUT THE CACHE LAYER,
21
+ * not about the person. The interrogative shapes keep their documented behaviour: the question word
22
+ * already says what the answer slot is.
23
+ *
24
+ * WHAT IT IS NOT. No model, no jieba, no natural-language generation, no arbitrary depth. Every
25
+ * decision is `String#indexOf` on the caller's query plus the vocabulary rows, so the same query over
26
+ * the same database always yields the same plan — which is what lets `search` be predictable and the
27
+ * tests be exact.
28
+ *
29
+ * MULTIPLE ENTITIES ARE NOT A RELATION QUESTION (docs/GRAPH_RECALL_AND_SEMANTICS.md §7.9). This
30
+ * planner only answers a question about ONE entity plus a predicate; "张伟和李娜" asks which recorded
31
+ * facts mention both, which is a FACT-level question, and its edges (when a fact has any) are
32
+ * evidence of that fact rather than a path between the two entities. So the multi-entity shape is
33
+ * planned by {@link planFactQuery} over `fact_entities` — journal entries and notes without any
34
+ * relation edge included — and the graph walk is never asked to fake it with `andPaths`.
35
+ *
36
+ * THE FALLBACK IS THE DEFAULT. Anything short of a COMPLETE plan (an entity, a predicate, the
37
+ * interrogative that makes it a question, in the expected order) returns `null`, and the caller then
38
+ * runs the next stage. §6's "判定不足时不要猜关系" is implemented as `return null`, not as a
39
+ * best-effort guess: a wrong relation answer is worse than a fuzzy one, because it claims structure
40
+ * the corpus never stated.
41
+ *
42
+ * WHY THE VOCABULARY IS A PORT. The planner only needs "which entity/predicate surfaces occur where
43
+ * in this text"; `MemoryGraphReader` answers that from SQL (and applies the store's kind/source/time
44
+ * filters at the same time). Keeping it an interface means the rules can be tested with a fake
45
+ * vocabulary, without a database, and that the planner stays free of SQLite.
46
+ *
47
+ * @module store/relation_planner
48
+ */
49
+ import { type GraphReader, type GraphResult, type RelationPath as GraphRelationPath } from '../../graph-recall/index.js';
50
+ import type { RelationPath } from '../../mem-contract/index.js';
51
+ /** One surface form found in the query text, with the span it occupies. */
52
+ export interface SurfaceMatch {
53
+ /** The string as it occurred — an alias or the canonical name. What `resolveNode` gets. */
54
+ ref: string;
55
+ /** The canonical entity name / predicate this surface means. */
56
+ canonical: string;
57
+ /** Inclusive character offset of the match in the query. */
58
+ start: number;
59
+ /** Exclusive character offset. */
60
+ end: number;
61
+ }
62
+ /**
63
+ * The two lookups the planner needs. `MemoryGraphReader` is the production implementation; the
64
+ * planner never sees a table.
65
+ */
66
+ export interface RelationVocabulary {
67
+ /** Known entity surfaces (canonical names and explicit aliases) occurring in `text`, any order. */
68
+ entitiesIn(text: string): SurfaceMatch[];
69
+ /** Known predicate surfaces (canonical names and aliases) occurring in `text`, any order. */
70
+ predicatesIn(text: string): SurfaceMatch[];
71
+ }
72
+ /**
73
+ * A complete relation question.
74
+ *
75
+ * `start`/`object` are SURFACE refs (not ids): the executor resolves them through the reader, which
76
+ * is also what canonicalizes an alias to its entity. `predicate`/`secondPredicate` are already
77
+ * canonical names — the vocabulary resolved them — so an alias (`负责`) and its canonical (`管理`)
78
+ * produce the same walk.
79
+ */
80
+ export type RelationQueryPlan = {
81
+ intent: 'one-hop';
82
+ start: string;
83
+ predicate: string;
84
+ } | {
85
+ intent: 'reverse';
86
+ object: string;
87
+ predicate: string;
88
+ } | {
89
+ intent: 'two-hop';
90
+ start: string;
91
+ predicate: string;
92
+ secondPredicate: string;
93
+ };
94
+ /** The most paths one relation answer returns (the library's own default page size). */
95
+ export declare const RELATION_PATH_LIMIT = 10;
96
+ /**
97
+ * The most entities ONE fact-level AND question may name (§7.9); beyond it the query falls back to
98
+ * fuzzy rather than asking a storage layer for an `IN (…)` list nobody bounded.
99
+ */
100
+ export declare const MAX_FACT_ENTITIES = 50;
101
+ /**
102
+ * Every start offset of `surface` in `text`.
103
+ *
104
+ * Both vocabularies (the graph reader's and the fact-level one) need POSITIONS — which side of the
105
+ * interrogative a name sits on — and a query may mention the same name twice
106
+ * ("张伟管理的人张伟自己负责什么" is not a good question, but the vocabulary must report both
107
+ * occurrences so the planner can reject it rather than pick one). Shared here so the two
108
+ * implementations cannot disagree about what "occurs" means.
109
+ */
110
+ export declare function surfaceOccurrences(text: string, surface: string): number[];
111
+ /**
112
+ * Decide whether `query` is a relation question, and which walk answers it.
113
+ *
114
+ * Order of the rules is intentional: the two-predicate `two-hop` shape is checked BEFORE the
115
+ * single-predicate shapes, because a query that has both ("…管理的人负责什么") would otherwise be
116
+ * answered one hop short. A partially matching multi-predicate query is then `null` rather than
117
+ * silently reduced to the first hop. Within the single-predicate branch the interrogative forms win:
118
+ * they say which slot the caller left open, so the bare-SPO fallback (which GUESSES the direction
119
+ * from surface order) only sees queries that ask nothing.
120
+ *
121
+ * @param vocabulary - entity/predicate surfaces (production: `MemoryGraphReader`).
122
+ * @param query - the caller's query text, verbatim.
123
+ * @returns the plan, or `null` when the query must fall back to fuzzy.
124
+ */
125
+ export declare function planRelationQuery(vocabulary: RelationVocabulary, query: string): RelationQueryPlan | null;
126
+ /**
127
+ * A fact-level AND question: a page of recorded facts that mention EVERY one of these entities
128
+ * (docs/GRAPH_RECALL_AND_SEMANTICS.md §7.9).
129
+ *
130
+ * `entities` are SURFACE refs (an alias or the canonical name), exactly like a relation plan's
131
+ * `start`/`object`: the fact repository resolves them alias-first, so the planner, the graph reader
132
+ * and the fact reader cannot disagree about what a name means. The planner DEDUPLICATES by canonical
133
+ * name, so an alias and its canonical form are one requirement rather than two.
134
+ */
135
+ export interface FactQueryPlan {
136
+ /** The discriminant, named like every relation intent so a caller can switch on it. */
137
+ intent: 'facts';
138
+ entities: string[];
139
+ }
140
+ /**
141
+ * The one lookup the fact-level planner needs: known entity surfaces occurring in `text`.
142
+ *
143
+ * Production is `EntitiesDao.factEntitiesIn` (entities linked through `fact_entities`, so journal
144
+ * entries and notes without a relation edge count too), reached through `MemoryStore`; keeping it a
145
+ * callback means the rules can be tested without a database and stay independent of the storage
146
+ * layer. It may answer synchronously or asynchronously.
147
+ */
148
+ export type FactEntityLookup = (text: string) => readonly SurfaceMatch[] | Promise<readonly SurfaceMatch[]>;
149
+ /**
150
+ * Decide whether `query` is a PURE multi-entity question, and which entities it asks about.
151
+ *
152
+ * This is the fact-level AND stage: "张伟和李娜" is answered by the recorded facts that mention both,
153
+ * never by a graph walk between them. The shape is deliberately narrow, and each rule is a
154
+ * documented one:
155
+ *
156
+ * - at least ONE conjunction ("和/与/以及/跟/同/、") — the cheap proof that the query is about
157
+ * several entities, so a plain content search never pays for the vocabulary scan;
158
+ * - at least TWO DISTINCT canonical entities;
159
+ * - at most {@link MAX_FACT_ENTITIES} of them (§7.9; beyond that the query falls back to fuzzy);
160
+ * - no interrogative: "张伟和李娜谁负责什么" asks a question this stage does not answer;
161
+ * - after removing the entity and conjunction surfaces, NOTHING meaningful may remain:
162
+ * "张伟和李娜的项目" is a content query that happens to name two entities.
163
+ *
164
+ * @param entitiesIn - the fact-level entity vocabulary lookup.
165
+ * @param query - the caller's query text, verbatim.
166
+ * @returns the plan, or `null` when the next stage answers the query.
167
+ */
168
+ export declare function planFactQuery(entitiesIn: FactEntityLookup, query: string): Promise<FactQueryPlan | null>;
169
+ /**
170
+ * Run one plan over a reader.
171
+ *
172
+ * This is deliberately only dispatch: the walks themselves are `@avantf/graph-recall`'s operators,
173
+ * so memory and knowledge cannot drift on what "two hops" means. Every branch RESOLVES its surface
174
+ * refs through the reader (an unresolved entity means the plan cannot run) and returns an empty
175
+ * result rather than throwing — an empty result is what makes `search` fall back to fuzzy.
176
+ *
177
+ * @param reader - the filtered graph view (kind/source/time already applied by the adapter).
178
+ * @param plan - a plan from {@link planRelationQuery}.
179
+ * @param limit - maximum paths to return.
180
+ */
181
+ export declare function executeRelationPlan(reader: GraphReader, plan: RelationQueryPlan, limit?: number): GraphResult;
182
+ /**
183
+ * Map the library's paths onto the contract's shape (`RelationPath` in `@avantf/mem-contract`).
184
+ *
185
+ * Two deliberate translations:
186
+ * - the library's `GraphNode.id` is a string; memory's entity id is an integer;
187
+ * - an edge WITHOUT a `factId` is not evidence, and the relation contract promises every edge can
188
+ * be followed back to an openable fact — so such a path is dropped here rather than published as
189
+ * structure nobody can verify. (The memory adapter always sets one; this guards the invariant.)
190
+ */
191
+ export declare function relationPathViews(paths: readonly GraphRelationPath[]): RelationPath[];
@@ -10,7 +10,6 @@
10
10
  * What it does NOT do, by design:
11
11
  * - no rewriting (a silently redacted fact is a lie the caller cannot see);
12
12
  * - no scanning of what is already stored (writes only, never a retroactive sweep);
13
- * - no knowledge-base coverage (`kb_add` ingests user-provided documents and is out of scope);
14
13
  * - no config/override path (the refusal carries its own remedy: record WHERE the secret lives).
15
14
  *
16
15
  * The refusal message is the interface: it names the shape and tells the caller what to write
@@ -9,8 +9,10 @@
9
9
  * family of questions over the strict bar (0.685 / 0.677 / 0.626 vs 0.476 / 0.459 / 0.420).
10
10
  *
11
11
  * WHAT THIS MODULE IS, AND IS NOT. A pure string table: cues (substrings) → ONE canonical rewrite.
12
- * No model, no tokenizer, no await, no I/O — that is what lets the SAME table serve the synchronous
13
- * conditional hint (`runtime.relevance`, 方案 A②) and the async retrieval path (`store/hybrid.ts`).
12
+ * No model, no tokenizer, no await, no I/O — that is what lets the retrieval path
13
+ * (`store/hybrid.ts`) apply it as an AUGMENTATION, and the conditional hint inherit the exact same
14
+ * behaviour for free: the hint is `AvantfRuntime.wouldFind`, which runs that retrieval path
15
+ * (DESIGN §12), so there is no second copy of the table or of its rewrite logic.
14
16
  *
15
17
  * WHY THE TABLE IS CLOSED AND WHY ONLY ONE REWRITE. The first match wins and produces exactly one
16
18
  * canonical string. Chinese self-reference has unbounded variants ("我叫啥" / "本人是" / "我是干啥的"),
@@ -0,0 +1,121 @@
1
+ /**
2
+ * The EXPLICIT time filter behind `mem_recall.search` / `mem_recall.timeline`
3
+ * (docs/TIME_SEARCH_AND_CATEGORIES.md §3).
4
+ *
5
+ * WHAT IT IS. One pure normalizer from the contract's `TimeFilter` to a half-open instant range
6
+ * `[from, to)`, PLUS the SQL fragment that range becomes in each table. No clock of its own for the
7
+ * bounds themselves — every bound comes from the caller's own strings — and no third-party date
8
+ * library: only `Date` and `Intl`.
9
+ *
10
+ * WHY IT IS NOT `parseTimeWindow`. That module (P-11) reads a time expression out of a QUERY's text
11
+ * and feeds a default-OFF recall leg. This one is a caller-named HARD filter with a required axis:
12
+ * a fact outside the window is not a candidate at all. They stay separate because they answer
13
+ * different questions ("this text mentions 上个月" vs "the caller asked for these days"), and
14
+ * because the leg deliberately never filters while this one only filters.
15
+ *
16
+ * THE GRAMMAR (D3/D4):
17
+ *
18
+ * - `YYYY-MM-DD` (date only) → the whole local day `[00:00, next 00:00)`.
19
+ * - `YYYY-MM-DDTHH:MM` (date + time) → exactly that one minute `[HH:MM:00, HH:MM+1:00)`.
20
+ * - a value carrying seconds/milliseconds is ROUNDED to the minute in the direction that does not
21
+ * shrink the caller's window: `on`/`from` floor, `to` ceil (so `to=18:00:30` still contains the
22
+ * 18:00 minute).
23
+ * - `from` is INCLUSIVE, `to` is EXCLUSIVE. A date-only `to` expands to the NEXT day's 00:00, which
24
+ * is what makes "从 10-01 到 10-07" include the 7th.
25
+ * - a value WITHOUT an offset is read in `timezone` (default: the host's own zone — currently
26
+ * Asia/Shanghai); a value WITH `Z`/`±HH:MM` keeps its own offset and ignores `timezone`.
27
+ * - `on` XOR `from`/`to`; at least one of them; `from > to` is an error, not an empty answer.
28
+ *
29
+ * WHY THE RANGE BECOMES BOTH EPOCH MS AND UTC TEXT. `created_at`/`updated_at` are TEXT
30
+ * (`CURRENT_TIMESTAMP`, `YYYY-MM-DD HH:MM:SS`, UTC) on every existing database, so Phase 1 compares
31
+ * them lexicographically against the UTC form of the same bounds — no migration, no re-interpretation
32
+ * of stored clock values. `valid_from` is the caller's own string (date-only or offset ISO), which
33
+ * cannot be compared lexicographically, so the EVENT axis uses epoch-ms columns
34
+ * (`valid_from_ms`, migration 11) and this range supplies the numbers.
35
+ *
36
+ * @module store/time_filter
37
+ */
38
+ import type { TimeField, TimeFilter } from '../../mem-contract/index.js';
39
+ /** One normalized filter: the field it applies to and a half-open `[fromMs, toMs)` instant range. */
40
+ export interface TimeRange {
41
+ field: TimeField;
42
+ /**
43
+ * The zone the caller's OFFSET-LESS values were read in (undefined = the host's own zone).
44
+ *
45
+ * Kept on the range because the `event` axis's text prefix must be compared in the SAME zone the
46
+ * caller's bounds were interpreted in — reading it back in the host zone would shift a Shanghai
47
+ * evening by a day on a UTC host.
48
+ */
49
+ timezone?: string;
50
+ /** Inclusive lower bound, epoch ms. `null` = open (no `from`). */
51
+ fromMs: number | null;
52
+ /** Exclusive upper bound, epoch ms. `null` = open (no `to`). */
53
+ toMs: number | null;
54
+ /** Inclusive lower bound as a UTC `YYYY-MM-DD HH:MM:SS` string (`created_at`'s own form). */
55
+ fromUtc: string | null;
56
+ /** Exclusive upper bound as a UTC `YYYY-MM-DD HH:MM:SS` string. */
57
+ toUtc: string | null;
58
+ }
59
+ /**
60
+ * The SQL fragment a {@link TimeRange} becomes, for one table alias.
61
+ *
62
+ * `clause(alias)` returns only the predicate body (no leading `AND`) so a caller can append it to a
63
+ * statement that already has its own filters; `params` binds in the SAME order the caller's `?`
64
+ * placeholders appear, which is why the fragment is built once and reused verbatim.
65
+ */
66
+ export interface TimeSql {
67
+ clause(alias: string): string;
68
+ params: (string | number)[];
69
+ }
70
+ /** `YYYY-MM-DD HH:MM:SS` in UTC — the form `CURRENT_TIMESTAMP` writes. */
71
+ export declare function utcTimestamp(ms: number): string;
72
+ /** Local `YYYY-MM-DD` for an instant — the day an `event` bound falls on in the host's zone. */
73
+ export declare function localDay(ms: number): string;
74
+ /** A bound at its minute boundary, rounding `up` or down (see the module's rounding rule). */
75
+ /**
76
+ * Validate and normalize one `time` filter.
77
+ *
78
+ * Throws (never returns a "no rows" range) on a caller mistake: an unknown field, a missing/duplicate
79
+ * bound form, an unparseable value, an unknown timezone, or `from > to`. An empty ANSWER is a valid
80
+ * outcome; an empty-QUESTION is not, because it returns the same shape as "nothing was written that
81
+ * day" and the caller cannot tell the two apart.
82
+ */
83
+ export declare function normalizeTimeFilter(filter: TimeFilter): TimeRange;
84
+ /**
85
+ * The SQL predicate a normalized range becomes, or `undefined` when the range has no bounds.
86
+ *
87
+ * The fragment names the COLUMN for the field (never a `COALESCE` across axes — D5: an event filter
88
+ * that fell back to `created_at` would answer "上个月做了什么" with facts merely WRITTEN last month),
89
+ * and it is the one place that decision lives for both stores' legs and the timeline query.
90
+ */
91
+ export declare function timeSql(range: TimeRange): TimeSql | undefined;
92
+ /**
93
+ * The `AND <predicate>` suffix for a statement, or `''` when there is no filter.
94
+ *
95
+ * A helper rather than string concatenation at every call site: the leading `AND` and the alias are
96
+ * what a hand-written copy gets wrong, and an append with no predicate would produce a dangling
97
+ * `WHERE … AND` — a syntax error that only fires on the timed path.
98
+ */
99
+ export declare function timeAndClause(range: TimeRange | undefined, alias?: string): string;
100
+ /** The bind parameters behind {@link timeAndClause}, in the same order. */
101
+ export declare function timeParams(range: TimeRange | undefined): (string | number)[];
102
+ /**
103
+ * The calendar day `ms` falls on, in `timeZone` (undefined = the host's zone).
104
+ *
105
+ * The `Intl` path is the same formatter cache `zonedWallClock` uses: a caller naming a zone must get
106
+ * that zone's DATE, not the host's.
107
+ */
108
+ export declare function dayInZone(ms: number, timeZone?: string): string;
109
+ /**
110
+ * One stored `valid_from`/`valid_to` string → epoch ms, using the SAME grammar as the filters.
111
+ *
112
+ * This is what the write path stamps into `valid_from_ms` and what migration 12's backfill applies to
113
+ * existing rows, so "the event time a caller wrote" and "the event time a window compares" can never
114
+ * be produced by two different parsers. A `YYYY-MM-DD` value is that day's local midnight (the
115
+ * writer's own zone — the engine's host), exactly like a date-only bound in a filter; a value with an
116
+ * offset keeps its own offset.
117
+ *
118
+ * `undefined` means "not a value this engine can place on the axis" — the row then behaves like one
119
+ * with no event time at all (D5: excluded, never binned by guesswork).
120
+ */
121
+ export declare function eventTimeMs(value: string | null | undefined): number | undefined;
@@ -0,0 +1,52 @@
1
+ import { type FtsTokenizer } from '../db/tokenizer.js';
2
+ /** Why a title was refused. The messages name the way out, like every other write refusal. */
3
+ export declare const TITLE_EMPTY = "\u6807\u9898\u4E0D\u80FD\u4E3A\u7A7A\uFF1Ajournal \u7684 title \u662F\u4E00\u53E5\u8BDD\u7684\u4E8B\u52A1\u53E5\u67C4\u3002";
4
+ export declare const TITLE_TOO_LONG: string;
5
+ export declare const TITLE_HAS_CONTROL = "\u6807\u9898\u5FC5\u987B\u662F\u5355\u884C\uFF1A\u4E0D\u63A5\u53D7\u6362\u884C\u6216\u63A7\u5236\u5B57\u7B26\u3002";
6
+ /**
7
+ * The refusal a title CHANGE gets inside one revision chain (JOURNAL_TITLE_SEARCH §2.2).
8
+ *
9
+ * One sentence with the way out: the same transaction omits the title (it inherits), and a different
10
+ * transaction is a new journal. A rename is not offered IMPLICITLY — it would make the old handle
11
+ * unfindable for every agent that remembered the old name — but v17 added the explicit correction
12
+ * (`retitle: true`), which records the old title as an alias instead of losing it.
13
+ */
14
+ export declare const TITLE_IMMUTABLE = "\u6807\u9898\u5728\u4E00\u6761 revision \u94FE\u5185\u4E0D\u53EF\u53D8\uFF1A\u540C\u4E00\u4E8B\u52A1\u8BF7\u7701\u7565 title\uFF08\u81EA\u52A8\u7EE7\u627F\u88AB\u6539\u5199\u7684 revision\uFF09\uFF1B\u786E\u9700\u7EA0\u6B63\u5199\u9519\u7684\u6807\u9898\uFF0C\u8BF7\u5E26 `retitle: true` \u5E76\u7ED9\u51FA\u65B0 title\uFF08\u65E7\u6807\u9898\u4FDD\u7559\u4E3A\u522B\u540D\uFF0C\u65E7\u641C\u7D22\u4ECD\u80FD\u547D\u4E2D\uFF09\u3002";
15
+ /** `retitle: true` on a write that has no chain to correct (an `add`, or the raw write without an id). */
16
+ export declare const TITLE_RETITLE_NEEDS_UPDATE = "retitle \u53EA\u7528\u4E8E\u7EA0\u6B63\u5DF2\u6709\u8BB0\u5F55\u7684\u6807\u9898\uFF1Aadd \u4E0D\u9700\u8981\u5B83\uFF08\u65B0\u8BB0\u5F55\u76F4\u63A5\u7ED9 title \u5373\u53EF\uFF09\u3002";
17
+ /** `retitle: true` without the new title — there is nothing to change it TO. */
18
+ export declare const TITLE_RETITLE_NEEDS_TITLE = "retitle \u9700\u8981\u540C\u65F6\u7ED9\u51FA\u65B0\u7684 title\uFF1A\u5B83\u53EA\u7528\u6765\u6539\u6807\u9898\uFF0C\u4E0D\u7ED9\u65B0\u6807\u9898\u5C31\u6CA1\u6709\u53EF\u6539\u7684\u5185\u5BB9\u3002";
19
+ /** The model-facing "a journal needs a title" refusal (core/import/CLI keep allowing NULL). */
20
+ export declare const TITLE_REQUIRED = "journal\uFF08\u505A\u4E86\u4EC0\u4E48/\u804A\u4E86\u4EC0\u4E48/\u60F3\u4E86\u4EC0\u4E48\uFF09\u5FC5\u987B\u5E26 title\uFF1A\u77ED\u6807\u9898\u662F\u540C\u4E00\u4E8B\u52A1\u7684\u53E5\u67C4\uFF0C\u4E5F\u662F\u6807\u9898\u68C0\u7D22\u7684\u5165\u53E3\uFF1B\u53EA\u8981\u7A33\u5B9A\u4E8B\u5B9E\u8BF7\u8BB0 note\u3002";
21
+ /** A duplicate/merge hit an existing row with a DIFFERENT title — most likely another transaction. */
22
+ export declare const TITLE_CONFLICT = "\u540C\u6837\u7684\u5185\u5BB9\u5DF2\u6709\u4E00\u6761 title \u4E0D\u540C\u7684\u8BB0\u5F55\uFF1A\u5B83\u53EF\u80FD\u5C5E\u4E8E\u53E6\u4E00\u4E2A\u4E8B\u52A1\uFF1B\u540C\u4E00\u4E8B\u52A1\u8BF7 update \u90A3\u6761\u8BB0\u5F55\uFF08\u7701\u7565 title\uFF09\uFF0C\u4E0D\u8981\u65B0\u589E\u3002";
23
+ /**
24
+ * Normalize a title a WRITE carries. Throws rather than returning `''`: an empty title is a caller
25
+ * mistake for a journal, and the store decides separately what an empty NOTE title means (NULL).
26
+ */
27
+ export declare function normalizeTitle(value: string): string;
28
+ /**
29
+ * Normalize a title SEARCH text. Same rules, except empty is "no query" (`null`) rather than an
30
+ * error — a whitespace-only search is answered with no candidates, not with a refusal.
31
+ */
32
+ export declare function normalizeTitleQuery(value: string): string | null;
33
+ /** The keywords a title query is made of (the query is already normalized, so single spaces). */
34
+ export declare function titleTerms(query: string): string[];
35
+ /**
36
+ * The FTS5 MATCH expression for a title query, or `null` when the table's tokenizer cannot express it.
37
+ *
38
+ * Each keyword becomes ONE quoted phrase, OR-joined: in a trigram table a quoted phrase of three or
39
+ * more characters is a SUBSTRING match (not an n-gram bag), which is exactly the semantics a title
40
+ * search wants. A keyword shorter than three characters cannot be phrased at all — and a unicode61
41
+ * table would treat a CJK title as a single token, so a substring query could never match it — hence
42
+ * `null`, which sends the caller to the LIKE fallback rather than to a silent zero.
43
+ *
44
+ * The `null` is ALL-OR-NOTHING on purpose: dropping the inexpressible keywords would answer a
45
+ * different question than the caller asked (e.g. "dsh 网关" would become "dsh").
46
+ */
47
+ export declare function titleFtsQuery(query: string, tokenizer: FtsTokenizer): string | null;
48
+ /**
49
+ * One stored title → the value surfaces read. `null` and `''` both mean "no title" (a restored
50
+ * legacy row may carry the empty string), and a display never invents one.
51
+ */
52
+ export declare function storedTitle(value: unknown): string | null;