@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,275 @@
1
+ /**
2
+ * Entity / predicate normalization — the vocabulary half of Phase 2
3
+ * (docs/RELATIONAL_RECALL.md §4.1–4.2/R2), and the middle step of the relation pipeline:
4
+ *
5
+ * ```text
6
+ * relations/extract.ts → candidates (strings only, no database)
7
+ * relations/normalize.ts → alias/name + predicate vocabulary (THIS module; needs a `Db`)
8
+ * relations/confidence.ts → the name/statement/confidence gates (pure)
9
+ * db/graph.ts → ids, direction, rows
10
+ * ```
11
+ *
12
+ * It resolves BOTH halves of an edge — the two entity names and the predicate — because an edge is
13
+ * two names plus a predicate, and the alias tables are the vocabulary's, not the POS extractor's.
14
+ * `entities/` owns tagging and entity names; this module owns what those strings MEAN.
15
+ *
16
+ * What this module owns, and what it deliberately refuses to do:
17
+ *
18
+ * - **Predicate vocabulary.** §4.2 names the families (管理/负责/主管, 使用/依赖, 属于/归属) and
19
+ * requires that an unfittable predicate keep its own word. So there is a small CURATED table
20
+ * below: a canonical name per relation family, its surface aliases, a direction, and a
21
+ * description. It is code, not a data migration, because a relation vocabulary is a judgement
22
+ * about the language, not about one operator's corpus — and because a seeded row must never
23
+ * overwrite a row an operator already corrected (first write wins, the same rule
24
+ * `entities.entity_type` has).
25
+ * - **Entity aliases.** §4.1 forbids automatic merging of two entities and asks for explicit
26
+ * aliases only. `entity_aliases` is therefore filled from exactly one automatic source —
27
+ * an EXPLICIT alias statement in the text (`A(又名 B)`, `A 简称 B`, `A aka B`, …) — plus the
28
+ * explicit `registerEntityAlias` entry point. "Two names look similar" / "both are frequent" is
29
+ * not evidence and never merges anything.
30
+ * - **Resolution order: alias, then name, then (optionally) create.** An alias row is an explicit
31
+ * statement, so it outranks a raw extracted token; the token's own `entities` row is left alone
32
+ * (content recall still uses it), only the GRAPH uses the canonical name. Creating is the last
33
+ * resort and is the caller's choice (`create: false` is how a dry run learns what it would do
34
+ * without polluting the vocabulary).
35
+ *
36
+ * Why the alias tables are seeded from HERE and not from a migration: a migration runs once per
37
+ * database, but the vocabulary is allowed to grow with the code, and the seeding is idempotent
38
+ * (`INSERT OR IGNORE` on `predicates.canonical_name` / `predicate_aliases.alias`). The memo below
39
+ * makes it one pass per connection rather than one per write.
40
+ *
41
+ * @module relations/normalize
42
+ */
43
+ import type { Db } from '../db/port.js';
44
+ import { type RelationTripleInput } from './types.js';
45
+ /**
46
+ * Which way a predicate's surface order maps onto its stored edge.
47
+ *
48
+ * `forward` (almost every seeded family): the stored edge reads subject → object, i.e. the same
49
+ * order the sentence used. `inverse`: the surface sentence puts the END of the relation first
50
+ * ("A 的下属是 B" states "B 管理 A"), so the edge is written with the two entity slots swapped and
51
+ * every walker can read all edges in one direction (§4.2's "保留方向" — the direction is a property
52
+ * of the relation, recorded once on the predicate row, not re-guessed per edge).
53
+ *
54
+ * `symmetric`: the relation holds in BOTH directions (合作), so one stored edge — written in the
55
+ * sentence's own order — must answer from either end. Nothing is swapped at write time; the READER
56
+ * expands the walk (see `store/graph_reader.ts` and {@link isSymmetricPredicate} for why the code's
57
+ * vocabulary, not only the row, decides).
58
+ */
59
+ export type PredicateDirection = 'forward' | 'inverse' | 'symmetric';
60
+ /** One curated relation family. */
61
+ export interface PredicateSeed {
62
+ /** The stored `predicates.canonical_name`. */
63
+ canonical: string;
64
+ /** Surface forms that mean the same relation (never including the canonical name itself). */
65
+ aliases: readonly string[];
66
+ direction?: PredicateDirection;
67
+ description: string;
68
+ }
69
+ /**
70
+ * The curated predicate vocabulary (§4.2's three families plus the relations this store's own
71
+ * fixtures and docs already exercise).
72
+ *
73
+ * Rules a new entry must follow, because getting them wrong is what makes a graph lie:
74
+ * - only relations that assert something STABLE about two entities. Modal / hearsay / planning
75
+ * verbs (计划, 打算, 听说, 可能) are absent on purpose: §R3's "不把「可能/计划/听说」抽成确定关系".
76
+ * They stay unmatched, which is what makes the edge writer drop them (see `edgeConfidence`).
77
+ * - an alias must be a form that is interchangeable in this store's usage, not merely related.
78
+ * `修` is left out because it is below the extractor's 2-character floor anyway; `用` likewise.
79
+ */
80
+ export declare const PREDICATE_VOCABULARY: readonly PredicateSeed[];
81
+ /** What one seeding pass inserted (0/0 once the vocabulary is already there). */
82
+ export interface VocabularySeedReport {
83
+ predicatesCreated: number;
84
+ aliasesCreated: number;
85
+ }
86
+ /**
87
+ * Create the curated predicates and their aliases if they are missing — idempotent, first write wins.
88
+ *
89
+ * An existing `predicates` row keeps its `direction` / `description`: both may have been corrected by
90
+ * an operator, and re-asserting the code's version on every open would silently undo that. The one
91
+ * exception is a row that has NO description — that is the state every Phase 1 backfill row is in
92
+ * (`INSERT OR IGNORE INTO predicates (canonical_name)`), and "no description" means "nobody has said
93
+ * what relation this is", which is exactly the fact {@link ensureRelationalVocabulary} can fill in.
94
+ * Without that fill-in an upgraded database would keep every predicate uncurated and the write path's
95
+ * confidence policy would refuse all of its relations. The same holds for `predicate_aliases`.
96
+ *
97
+ * Requires the v15 graph tables; called from the store's write path, so a database that predates
98
+ * them would already have failed its migration.
99
+ */
100
+ export declare function ensureRelationalVocabulary(db: Db): VocabularySeedReport;
101
+ /** How a name was matched — the provenance a caller reports or asserts on. */
102
+ export type ResolveVia = 'alias' | 'name' | 'created';
103
+ /** One resolved entity. */
104
+ export interface EntityRef {
105
+ entity_id: number;
106
+ /** The CANONICAL name (the `entities.name` the graph uses), not necessarily the input. */
107
+ name: string;
108
+ via: ResolveVia;
109
+ }
110
+ /** One resolved predicate. */
111
+ export interface PredicateRef {
112
+ predicate_id: number;
113
+ canonical: string;
114
+ direction: PredicateDirection;
115
+ /**
116
+ * Whether someone has SAID what relation this is (`predicates.description` is set) — the seed does,
117
+ * an operator-registered relation is expected to, a row created by name-mapping does not. This is
118
+ * the bit the confidence policy uses: an undescribed verb is a guess, and a guess with a weak
119
+ * producer score stays out of the graph.
120
+ */
121
+ curated: boolean;
122
+ via: ResolveVia;
123
+ }
124
+ /** Options shared by the two resolvers. `create: false` is the dry-run / read-only mode. */
125
+ export interface ResolveOptions {
126
+ /** Create a vocabulary row for an unseen name (default true). */
127
+ create?: boolean;
128
+ /** `entities.extraction_method` for a row this call creates (default `'graph'`). */
129
+ method?: string;
130
+ }
131
+ /**
132
+ * Whether a canonical relation is SYMMETRIC — read from the curated vocabulary, not from the row.
133
+ *
134
+ * `predicates.direction` is written once per database (`INSERT OR IGNORE`), and a row seeded by an
135
+ * earlier build holds the column default `'forward'`: re-asserting the code's value on open would
136
+ * silently undo an operator's correction, which is why the seeding is first-write-wins. Symmetry is
137
+ * therefore a fact about the RELATION FAMILY as this build defines it, and the reader asks this
138
+ * function so an upgraded database answers `合作` from both ends without a data migration. A
139
+ * relation an operator registers is asymmetric unless it is one of these families — there is no
140
+ * operator-facing way to declare a new one yet, and pretending otherwise would be a promise the
141
+ * reader cannot keep.
142
+ */
143
+ export declare function isSymmetricPredicate(canonical: string): boolean;
144
+ /**
145
+ * Resolve one entity name to its canonical node: **alias first**, then exact name, then create.
146
+ *
147
+ * Alias first is the whole point of registration: a registered alias is an explicit statement
148
+ * ("老张 就是 张伟"), while the extracted token is a guess about a string. The token's own row — if
149
+ * one exists — is left untouched, so nothing that reads `fact_entities` changes behaviour; only the
150
+ * graph follows the canonical name.
151
+ *
152
+ * @returns `undefined` only when the name is unusable or `create: false` and it is unknown.
153
+ */
154
+ export declare function resolveEntity(db: Db, rawName: string, options?: ResolveOptions): EntityRef | undefined;
155
+ /**
156
+ * Resolve one predicate word to its canonical relation: alias, then canonical name, then create.
157
+ *
158
+ * The direction comes from the resolved ROW, so a predicate an operator registered as `inverse`
159
+ * keeps that meaning through every later write; a newly created row is `'forward'` (the only
160
+ * direction any seeded family uses, and the only claim an unknown word supports).
161
+ *
162
+ * `curated` is the one bit the confidence policy reads (see `confidence.ts#edgeConfidence`), and it is
163
+ * derived from the ROW rather than from the code's seed list so an operator-registered relation
164
+ * counts too: a predicate is curated when it carries a DESCRIPTION. The seed writes one for every
165
+ * family (and fills it into a row the Phase 1 backfill created without one); a row created by
166
+ * name-mapping or by this function's `create` path has none, which is the honest state of "a verb the
167
+ * extractor produced, which nobody has said is a relation".
168
+ *
169
+ * @returns `undefined` when the word is unusable or `create: false` and it is unknown — the caller
170
+ * treats that as "not a relation we can name", which is what the confidence policy acts on.
171
+ */
172
+ export declare function resolvePredicate(db: Db, rawPred: string, options?: ResolveOptions): PredicateRef | undefined;
173
+ /** One alias statement found in a text. */
174
+ export interface AliasPair {
175
+ /** The alternative name (`B` in "A(又名 B)"). */
176
+ alias: string;
177
+ /** The name it is stated to be another form of (`A`). */
178
+ canonical: string;
179
+ }
180
+ /**
181
+ * Explicit alias statements in `text`, in order of appearance.
182
+ *
183
+ * Both groups are narrow on purpose: no spaces, 2–20 characters, and the cue word is required. A
184
+ * false match would merge two nodes, which §4.1 ranks as the one thing not to automate.
185
+ *
186
+ * `knownNames` is the second half of that guard, and it is what makes the pattern usable at all:
187
+ * Chinese has no spaces, so `X` before the cue is a raw character run ("张伟管理缓存层(又名 …)"
188
+ * would otherwise become one alias target). Only a candidate the extractor ALSO recognised as an
189
+ * entity is accepted — the cue states the identity, the entity list states that the left side is a
190
+ * name rather than a sentence.
191
+ */
192
+ export declare function parseAliasPairs(text: string, knownNames?: readonly string[]): AliasPair[];
193
+ /**
194
+ * Strip an alias statement that the extractor merged into an entity slot.
195
+ *
196
+ * The SVO extractor (`relations/extract.ts`) merges everything noun-ish on one side of the predicate,
197
+ * so a sentence that
198
+ * states an alias ("张伟(又名 老张)负责李娜", "老张又名四哥,管理王五") yields a subject that contains
199
+ * the whole statement. Turning that into an entity would create a node whose name is a sentence, and
200
+ * would hide the fact from the canonical entity — so the graph resolves the slot to the name BEFORE
201
+ * the cue.
202
+ *
203
+ * Narrow on purpose: only a candidate that CONTAINS an alias cue is touched, a parenthesised group is
204
+ * only dropped when it carries a cue, and an empty result falls back to the input (nothing is
205
+ * invented to replace a name).
206
+ */
207
+ export declare function stripAliasClause(rawName: string): string;
208
+ /** Why an alias statement did not become a row (absent = it did). */
209
+ export type AliasSkipReason = 'blank' | 'self' | 'already-aliased';
210
+ /** What one alias registration did. */
211
+ export interface AliasRegistration {
212
+ alias: string;
213
+ canonical: string;
214
+ written: boolean;
215
+ reason?: AliasSkipReason;
216
+ }
217
+ /**
218
+ * Record `alias → canonical` in `entity_aliases`, creating the canonical entity if it is new.
219
+ *
220
+ * Two refusals, both conservative:
221
+ * - `alias === canonical` / either slot blank: not a statement about two names.
222
+ * - the alias is ALREADY registered to another entity: `alias` is UNIQUE, and re-pointing it would
223
+ * rewrite an earlier explicit statement. Reported, never silently overwritten.
224
+ *
225
+ * Registering an alias whose text is also an existing entity name is ALLOWED: the alias is an
226
+ * explicit statement about the string, the entity row is the extractor's guess, and resolution
227
+ * prefers the alias (see `resolveEntity`). Nothing is merged or deleted — the old row keeps its
228
+ * `fact_entities` links.
229
+ */
230
+ export declare function registerEntityAlias(db: Db, pair: AliasPair, options?: ResolveOptions): AliasRegistration;
231
+ /** {@link registerEntityAlias} over the statements found in one text (best-effort, in order). */
232
+ export declare function registerEntityAliasesFromText(db: Db, text: string, knownNames: readonly string[], options?: ResolveOptions): AliasRegistration[];
233
+ /**
234
+ * A producer triple after normalization — the input of `confidence.ts#evaluateRelation` and then of
235
+ * the storage step (`db/graph.ts`). Everything the policy needs is on this object, which is why the
236
+ * policy itself reads no database.
237
+ */
238
+ export interface NormalizedRelation {
239
+ /** The slots the graph stores: an alias STATEMENT is stripped, both sides trimmed. */
240
+ subj: string;
241
+ pred: string;
242
+ obj: string;
243
+ /**
244
+ * The slots as the producer wrote them (trimmed, alias statement INCLUDED). The statement test
245
+ * (`statement.ts#isStatedRelation`) reads the fact text against THESE — the strip above changed
246
+ * the slot, and the test has to ask about what is actually in the text.
247
+ */
248
+ raw: {
249
+ subj: string;
250
+ pred: string;
251
+ obj: string;
252
+ };
253
+ /** The producer's score, before the policy adjusts it. */
254
+ confidence: number | null | undefined;
255
+ /** The producer's provenance (`'svo'`, `'pattern'`, `'explicit'`, `'llm'`, …). */
256
+ source: string;
257
+ /** `source === 'explicit'`: a caller's statement, which the policy trusts outright. */
258
+ explicit: boolean;
259
+ /**
260
+ * The vocabulary row for `pred`, when it already exists. The probe is `create: false` on purpose:
261
+ * whether an unknown predicate earns a row of its own is the STORAGE step's decision, taken only
262
+ * after the policy accepted the relation (a refused triple must not leave a predicate behind).
263
+ */
264
+ predicate?: PredicateRef;
265
+ }
266
+ /**
267
+ * Normalize one producer triple onto the vocabulary: strip an alias STATEMENT that the extractor
268
+ * merged into an entity slot (`张伟(又名 老张)` → `张伟`), trim the predicate, and look its row up.
269
+ *
270
+ * Total: a blank slot is a relation the policy refuses (`confidence.ts#evaluateRelation` reports it
271
+ * as `blank`), not an error here — the slots are still returned so a drop sample can name them. An
272
+ * UNKNOWN predicate is not a refusal either: §4.2 keeps the producer's own word, and the
273
+ * `predicate` field is simply absent (the policy then scores it uncurated).
274
+ */
275
+ export declare function normalizeRelation(db: Db, triple: RelationTripleInput): NormalizedRelation;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Explicit relation patterns — the deterministic half of Phase 5b's extraction repair
3
+ * (docs/RELATIONAL_RECALL.md §4.3/R3, §十七; the 2026-10-25 review's §三/§七).
4
+ *
5
+ * WHY PATTERNS AT ALL. The token-based producers (`relations/extract.ts`) need a predicate WORD with
6
+ * a name on each side, and they take those names from proximity. That misses two whole classes of
7
+ * statement the live corpus does contain:
8
+ *
9
+ * - a relation expressed with a PREPOSITION rather than a verb: `冯飞在阿里巴巴工作`;
10
+ * - a relation whose surface order puts the object before the predicate: `张伟向李娜汇报`,
11
+ * `张伟与李娜合作`.
12
+ *
13
+ * Patterns are the right tool for exactly this: fixed cue words, fixed argument order, a small and
14
+ * closed set. They are NOT a general parser, and this module deliberately holds only patterns whose
15
+ * surface form cannot mean anything else. The bar for adding one is the same as for a curated
16
+ * predicate: an operator can read the cue and predict the edge.
17
+ *
18
+ * WHAT KEEPS A PATTERN HONEST — the "statement check" this module performs itself, because the
19
+ * canonical predicate a pattern writes (`工作于`) is not the word the text contains and
20
+ * `relations/statement.ts`'s gate therefore cannot be applied to it:
21
+ *
22
+ * - the cue is required, so a sentence without it produces nothing;
23
+ * - the cue must END at a clause boundary (`AFTER`, built from the statement gate's own
24
+ * `CLAUSE_BOUNDARY_CHARS`): this is the 2026-10-25 review's §三 false positive — `工作区`,
25
+ * `工作人员`, `工作报告` all used to match `工作` as a PREFIX and become `工作于` edges at
26
+ * confidence 0.95. A cue that continues into another word is not a cue;
27
+ * - the OBJECT must end at a clause boundary too, and must not itself contain a cue: a greedy name
28
+ * class otherwise swallows the rest of the clause (`张伟就职于某公司工作` → `某公司工作`);
29
+ * - both arguments must pass `graphNameIssue` (a bounded, punctuation-free name), and a trailing
30
+ * parenthetical is dropped (`阿里巴巴(阿里)` → `阿里巴巴`);
31
+ * - the relation name written is the CANONICAL predicate (`工作于`), which the curated vocabulary
32
+ * (`relations/normalize.ts`) must contain — an uncurated pattern name would be refused by the
33
+ * edge writer's confidence policy anyway, so a pattern cannot smuggle in a new relation family;
34
+ * - the triple is stamped `source: 'pattern'` and a confidence above the heuristic's, because the
35
+ * cue is evidence: it is not guessed from proximity.
36
+ *
37
+ * The patterns run on the RAW TEXT (not on POS tokens) on purpose: the tokenizer loses the
38
+ * preposition/verb distinction that makes the cue unambiguous, and a pattern needs no tagger, so it
39
+ * also works when nodejieba is unavailable.
40
+ *
41
+ * @module relations/patterns
42
+ */
43
+ import type { RelationCandidate } from './types.js';
44
+ /** Confidence a pattern triple carries: the cue is evidence, unlike an inferred proximity triple. */
45
+ export declare const PATTERN_TRIPLE_CONFIDENCE = 0.7;
46
+ /** One pattern's yield (used by the extraction report to separate "no cue" from "cue but refused"). */
47
+ export interface PatternYield {
48
+ id: string;
49
+ matches: number;
50
+ accepted: number;
51
+ refused: number;
52
+ }
53
+ /**
54
+ * Extract the relations stated by an EXPLICIT cue in `text`.
55
+ *
56
+ * Every module-level regex is `g` and therefore stateful, so `lastIndex` is reset before each
57
+ * `matchAll` — the same guard `normalize.parseAliasPairs` uses. A match is accepted only when both
58
+ * arguments are usable names AND the object is not a clause continuation; the rest are counted as
59
+ * refusals, not silently dropped, so a report can say which pattern matched and why it produced
60
+ * nothing.
61
+ */
62
+ export declare function patternTriples(text: string): RelationCandidate[];
63
+ /** {@link patternTriples} plus the per-pattern accounting (the eval script's half). */
64
+ export declare function patternTriplesWithReport(text: string): {
65
+ triples: RelationCandidate[];
66
+ yields: PatternYield[];
67
+ };
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Was a relation actually STATED, or did the extractor cut a fragment out of prose? — Phase 5b's
3
+ * precision gate (docs/RELATIONAL_RECALL.md §4.3/R3, §十七).
4
+ *
5
+ * THE PROBLEM. `relations/extract.ts` is an SVO proximity heuristic: for every verb it takes the
6
+ * noun-ish run on its left as the subject and the run on its right as the object. On a short
7
+ * declarative fact (`张伟管理李娜`) that is exactly a statement. On a long engineering note
8
+ * (`…命令:npm publish … 发布账号 ffeng86…`) it produces `命令 —创建→ 仓库README`: two fragments that
9
+ * happen to sit on either side of a verb, glued into a relation nobody asserted.
10
+ *
11
+ * Phase 5a measured the live graph at 48 active edges / 38 facts; this test finds that NONE of the
12
+ * 48 curated triples among the 809 stored active triples is a stated relation. So the 0.216 coverage
13
+ * Phase 5a reported was fragment coverage — the metric was measuring how much prose the tokenizer
14
+ * walked over, not how many relations the store knows. §R3 is explicit about the direction to fix
15
+ * it: a wrong edge is worse than a missing one, so an edge now requires the fact's TEXT to assert
16
+ * the triple as a clause.
17
+ *
18
+ * WHAT "STATED" MEANS HERE, exactly — the triple's three parts must appear as one contiguous run in
19
+ * the fact content, with:
20
+ *
21
+ * - a clause boundary before the subject and after the object (start/end of text, whitespace, any
22
+ * sentence punctuation, a quote or a bracket). A match that needs text on its outside is a
23
+ * fragment of a longer clause, so it is not a statement.
24
+ * - nothing but a closed set of REALIS adverbs between subject and predicate (`已经/已/正在/都/…`).
25
+ * A single comma is also allowed there: Chinese shares a subject across a comma-splice
26
+ * (`老张又名四哥,负责王五` states that 老张/四哥 manages 王五), and the live corpus has no case where
27
+ * that tolerance admits a fragment (measured: still 0/809).
28
+ * Modals and hedges are absent on purpose: `将要`, `需要`, `可以`, `可能`, `计划` mean the relation
29
+ * is not asserted, and §R3 forbids turning them into facts. A NEGATION (`不`, `没有`, `未`, `别`)
30
+ * is absent too — and because the adverb set is closed, `不再依赖` cannot match the positive
31
+ * `依赖` triple at all. That is how "the store must not assert the opposite of what was written"
32
+ * is enforced without a separate negation parser.
33
+ * - only an aspect particle (`了/着/过`) between predicate and object.
34
+ *
35
+ * The test is deliberately CONSERVATIVE: it is a substring test over the stored text, so a fact that
36
+ * paraphrases its own triple is dropped. That is the intended failure direction (§R3), and it is
37
+ * why the triple is still stored (`triples` remains "what the string extractor saw") even when the
38
+ * edge is refused.
39
+ *
40
+ * The gate applies to triples the TEXT EXTRACTOR produced (`source: 'svo'`), not to triples a
41
+ * caller asserts explicitly (a curated/LLM/fixture producer already knows the relation, and R2's
42
+ * write path lets it say so). See `confidence.ts#evaluateRelation`.
43
+ *
44
+ * TWO CALLERS, ONE DEFINITION. The extractor's triple-shape invariant shares the same regex parts:
45
+ * {@link isContiguousTriple} asks the weaker question "are the three slots one unbroken run of
46
+ * text?" and `relations/extract.ts` refuses a candidate that fails it before the triple is stored,
47
+ * while {@link isStatedRelation} is the edge gate above. Both read `CLAUSE_BOUNDARY_CHARS`, and
48
+ * `relations/patterns.ts` uses that same constant for a cue's right boundary — one definition of
49
+ * "clause", so the extractor, the patterns and the gate cannot disagree about where a clause ends.
50
+ *
51
+ * @module relations/statement
52
+ */
53
+ /**
54
+ * Clause characters that may sit immediately before a subject / after an object.
55
+ *
56
+ * EXPORTED because it is the definition, not a copy: `relations/extract.ts` (the triple-contiguity
57
+ * check) and `relations/patterns.ts` (a cue's right boundary) must agree with the statement gate on
58
+ * what ends a clause — otherwise a pattern can match text this gate would call one clause and a
59
+ * fragment, which is exactly how `工作区` became an edge.
60
+ */
61
+ export declare const CLAUSE_BOUNDARY_CHARS = "\\s\uFF0C\u3002\u3001\uFF1B\uFF1A\uFF01\uFF1F,;:!?\uFF08\uFF09()\u3010\u3011\u300A\u300B\u3008\u3009\u300C\u300D\u300E\u300F\u201C\u201D\u2018\u2019\"'`*#>\u2014\u2026";
62
+ /**
63
+ * Whether `content` states `subj pred obj` as one clause.
64
+ *
65
+ * Returns `false` for an empty part: a blank slot is not a statement (the edge writer refuses it
66
+ * for a different reason, and this function must not turn that into a match on `(?:^|…)`).
67
+ */
68
+ export declare function isStatedRelation(content: string, subj: string, pred: string, obj: string): boolean;
69
+ /**
70
+ * Whether `subj`, `pred` and `obj` appear as ONE contiguous run in `content` — the extractor's
71
+ * triple-shape invariant, WITHOUT the edge gate's clause-boundary requirement.
72
+ *
73
+ * WHY THIS IS NOT {@link isStatedRelation}. Two different questions:
74
+ *
75
+ * - the GATE (`isStatedRelation`) asks whether the fact ASSERTS the relation as a clause, so it
76
+ * insists on a boundary before the subject and after the object. It runs on the edge path and
77
+ * only for proximity triples (`relations/confidence.ts`);
78
+ * - this asks whether the three slots the tokenizer produced are one unbroken run of text at all.
79
+ * A triple that fails it was glued together across something the merge is not allowed to cross
80
+ * (the 2026-10-25 review measured `张伟在阿里巴巴工作报告` → `张伟阿里巴巴 — 工作 — 报告`), and it
81
+ * is refused BEFORE it reaches `triples`: the table feeds contradiction detection, and a
82
+ * fragment there is pollution even when the edge gate would refuse the edge anyway.
83
+ *
84
+ * Tolerances are the same as the gate's (realis adverbs, one comma splice, an aspect particle,
85
+ * edge padding), because those are exactly the tokens the merge is allowed to step over. A blank
86
+ * part is `false` — a slot-less triple is not a statement of anything.
87
+ */
88
+ export declare function isContiguousTriple(content: string, subj: string, pred: string, obj: string): boolean;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The relation module's shapes: what a producer hands over, and what the policy decides.
3
+ *
4
+ * WHY THIS MODULE EXISTS. Building an edge used to be spread over three places — `entities/extract`
5
+ * (candidate rules), `entities/normalize` (the vocabulary) and `db/graph.ts` (the gates AND the
6
+ * storage). That made "why did this relation not become an edge?" a question about one 900-line
7
+ * file, and it made the gates unreachable for a producer that is not the string extractor. The
8
+ * `relations/` module owns the pipeline now:
9
+ *
10
+ * ```text
11
+ * extractRelationCandidates(text, tokens?) ← SVO heuristic + explicit patterns (extract.ts)
12
+ * normalizeRelation(db, candidate) ← alias/name + predicate vocabulary (normalize.ts)
13
+ * evaluateRelation(normalized, content) ← name / statement / confidence gates (confidence.ts)
14
+ * writeEdgesForFact(db, id, candidates) ← ids, direction, rows, versions (db/graph.ts)
15
+ * ```
16
+ *
17
+ * `db/graph.ts` only materializes and stores; `entities/` only tags and extracts entity names. A
18
+ * future LLM or backfill producer implements {@link RelationExtractor} and gets the SAME gates,
19
+ * because it can only produce candidates.
20
+ *
21
+ * @module relations/types
22
+ */
23
+ /**
24
+ * The minimum a producer must supply: the three slots, the producer's score and its provenance.
25
+ *
26
+ * `confidence` and `source` are optional because a stored `triples` row may predate both — the
27
+ * column defaults describe a legacy heuristic triple, and the policy reads a missing score as the
28
+ * heuristic's `0.5` (see `confidence.ts#edgeConfidence`).
29
+ */
30
+ export interface RelationTripleInput {
31
+ subj: string;
32
+ pred: string;
33
+ obj: string;
34
+ confidence?: number | null;
35
+ source?: string | null;
36
+ }
37
+ /** One extracted relation — what `extractRelationCandidates` returns and the write path stores. */
38
+ export interface RelationCandidate extends RelationTripleInput {
39
+ confidence: number;
40
+ source: string;
41
+ }
42
+ /**
43
+ * `triples.source` of the POS/SVO proximity extractor (`relations/extract.ts`).
44
+ *
45
+ * Load-bearing, not cosmetic: it is the ONLY source the statement test (`statement.ts`) is applied
46
+ * to. A curated/LLM/fixture producer already knows the relation, and a caller-asserted one
47
+ * (`explicit`) outranks any test against the text.
48
+ */
49
+ export declare const SVO_SOURCE = "svo";
50
+ /** `triples.source` of an explicit pattern (`relations/patterns.ts`) — the cue is evidence. */
51
+ export declare const PATTERN_SOURCE = "pattern";
52
+ /**
53
+ * `edges.source` for a relation a CALLER stated (`mem_remember.relations`).
54
+ *
55
+ * It is the one producer whose triples are not extracted from the text, so it is also the one the
56
+ * confidence policy trusts outright: no floor, no statement test. `reextractEdges` preserves these
57
+ * rows instead of re-deriving them (they cannot be re-derived — the fact text does not contain
58
+ * them).
59
+ */
60
+ export declare const EXPLICIT_EDGE_SOURCE = "explicit";
61
+ /** Why an extracted relation never became an edge. */
62
+ export type RelationDropReason =
63
+ /** A slot is blank after trimming — not a relation at all. */
64
+ 'blank'
65
+ /** The confidence policy put it below `EDGE_MIN_CONFIDENCE` (`confidence.ts`). */
66
+ | 'low-confidence'
67
+ /** A slot is not a usable node name (`name-quality.ts`) — the Phase 5b fragment gate. */
68
+ | 'unusable-name'
69
+ /** A proximity triple (`SVO_SOURCE`) that the fact text never states as a clause. */
70
+ | 'not-a-statement';
71
+ /** What the policy decided about one normalized relation. */
72
+ export interface RelationEvaluation {
73
+ /** Whether the edge may be written. */
74
+ accepted: boolean;
75
+ /** The confidence the edge would carry — computed even when refused, so a report can show it. */
76
+ confidence: number;
77
+ /** Why it was refused; absent when accepted. */
78
+ reason?: RelationDropReason;
79
+ }