@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.
- package/README.md +4 -0
- package/lib/client.js +548 -1195
- package/lib/dsh-build.json +1 -0
- package/lib/engine/graph-recall/in-memory.d.ts +93 -0
- package/lib/engine/graph-recall/index.d.ts +22 -0
- package/lib/engine/graph-recall/traverse.d.ts +119 -0
- package/lib/engine/graph-recall/types.d.ts +188 -0
- package/lib/engine/mem/config/config_files.d.ts +1 -1
- package/lib/engine/mem/config/loader.d.ts +2 -3
- package/lib/engine/mem/config/paths.d.ts +2 -13
- package/lib/engine/mem/db/dao/contradictions.d.ts +4 -22
- package/lib/engine/mem/db/dao/entities.d.ts +80 -7
- package/lib/engine/mem/db/dao/fact_sources.d.ts +3 -13
- package/lib/engine/mem/db/dao/facts.d.ts +311 -56
- package/lib/engine/mem/db/dao/triples.d.ts +1 -26
- package/lib/engine/mem/db/graph.d.ts +249 -0
- package/lib/engine/mem/db/journal_notes.d.ts +35 -0
- package/lib/engine/mem/db/migrations.d.ts +9 -0
- package/lib/engine/mem/db/port.d.ts +22 -3
- package/lib/engine/mem/db/schema.d.ts +119 -0
- package/lib/engine/mem/db/tokenizer.d.ts +9 -0
- package/lib/engine/mem/dbdrivers/fake/driver.d.ts +75 -0
- package/lib/engine/mem/dbdrivers/fake/index.d.ts +7 -0
- package/lib/engine/mem/dbdrivers/fake/surface.d.ts +34 -0
- package/lib/engine/mem/dbdrivers/fake/transport.d.ts +61 -0
- package/lib/engine/mem/dbdrivers/sqlite/connection.d.ts +14 -0
- package/lib/engine/mem/dbdrivers/sqlite/contradictions.d.ts +35 -0
- package/lib/engine/mem/dbdrivers/sqlite/dao/index.d.ts +16 -0
- package/lib/engine/mem/dbdrivers/sqlite/driver.d.ts +54 -0
- package/lib/engine/mem/dbdrivers/sqlite/facts.d.ts +117 -0
- package/lib/engine/mem/dbdrivers/sqlite/index.d.ts +6 -0
- package/lib/engine/mem/dbdrivers/sqlite/migrations.d.ts +15 -0
- package/lib/engine/mem/dbdrivers/sqlite/relations.d.ts +62 -0
- package/lib/engine/mem/dbdrivers/sqlite/search.d.ts +56 -0
- package/lib/engine/mem/dbdrivers/sqlite/sources.d.ts +37 -0
- package/lib/engine/mem/defaults.d.ts +3 -4
- package/lib/engine/mem/dispatch.d.ts +7 -32
- package/lib/engine/mem/entities/extract.d.ts +44 -47
- package/lib/engine/mem/eval/derived_runner.d.ts +6 -1
- package/lib/engine/mem/eval/relation_gold.d.ts +251 -0
- package/lib/engine/mem/eval/write_metrics.d.ts +1 -1
- package/lib/engine/mem/facts_io.d.ts +50 -10
- package/lib/engine/mem/index.d.ts +6 -2
- package/lib/engine/mem/interfaces/capabilities.d.ts +30 -0
- package/lib/engine/mem/interfaces/common.d.ts +11 -0
- package/lib/engine/mem/interfaces/errors.d.ts +65 -0
- package/lib/engine/mem/interfaces/index.d.ts +18 -0
- package/lib/engine/mem/interfaces/migrations.d.ts +21 -0
- package/lib/engine/mem/interfaces/repositories.d.ts +471 -0
- package/lib/engine/mem/interfaces/search_index.d.ts +113 -0
- package/lib/engine/mem/interfaces/storage_backend.d.ts +84 -0
- package/lib/engine/mem/interfaces/storage_driver.d.ts +43 -0
- package/lib/engine/mem/interfaces/version.d.ts +34 -0
- package/lib/engine/mem/lifecycle/contradiction.d.ts +2 -2
- package/lib/engine/mem/modelBootstrap.d.ts +1 -1
- package/lib/engine/mem/relations/confidence.d.ts +39 -0
- package/lib/engine/mem/relations/extract.d.ts +128 -0
- package/lib/engine/mem/relations/index.d.ts +32 -0
- package/lib/engine/mem/relations/name-quality.d.ts +78 -0
- package/lib/engine/mem/relations/normalize.d.ts +275 -0
- package/lib/engine/mem/relations/patterns.d.ts +67 -0
- package/lib/engine/mem/relations/statement.d.ts +88 -0
- package/lib/engine/mem/relations/types.d.ts +79 -0
- package/lib/engine/mem/runtime.d.ts +61 -88
- package/lib/engine/mem/storage/create.d.ts +42 -0
- package/lib/engine/mem/storage/errors.d.ts +65 -0
- package/lib/engine/mem/storage/index.d.ts +15 -0
- package/lib/engine/mem/storage/ports.d.ts +472 -0
- package/lib/engine/mem/storage/registry.d.ts +37 -0
- package/lib/engine/mem/storage/sqlite.d.ts +28 -0
- package/lib/engine/mem/store/common.d.ts +1 -1
- package/lib/engine/mem/store/entity_leg.d.ts +59 -2
- package/lib/engine/mem/store/entity_sweep.d.ts +4 -3
- package/lib/engine/mem/store/explicit_relations.d.ts +52 -0
- package/lib/engine/mem/store/facts_restore.d.ts +10 -2
- package/lib/engine/mem/store/floors.d.ts +4 -1
- package/lib/engine/mem/store/graph_reader.d.ts +146 -0
- package/lib/engine/mem/store/hybrid.d.ts +50 -13
- package/lib/engine/mem/store/kinds.d.ts +47 -0
- package/lib/engine/mem/store/legs.d.ts +24 -29
- package/lib/engine/mem/store/lexical.d.ts +36 -45
- package/lib/engine/mem/store/memory.d.ts +471 -83
- package/lib/engine/mem/store/recall_policy.d.ts +49 -0
- package/lib/engine/mem/store/relation_backfill.d.ts +210 -0
- package/lib/engine/mem/store/relation_planner.d.ts +191 -0
- package/lib/engine/mem/store/secret_guard.d.ts +0 -1
- package/lib/engine/mem/store/self_query.d.ts +4 -2
- package/lib/engine/mem/store/time_filter.d.ts +121 -0
- package/lib/engine/mem/store/titles.d.ts +52 -0
- package/lib/engine/mem/store/vector_repair.d.ts +44 -54
- package/lib/engine/mem-contract/config.d.ts +44 -38
- package/lib/engine/mem-contract/text.d.ts +1 -1
- package/lib/engine/mem-contract/tools.d.ts +145 -132
- package/lib/engine/mem-contract/types.d.ts +364 -288
- package/lib/index.js +12048 -11337
- package/lib/interface-version.json +1 -1
- package/lib/types/client/index.d.ts +2 -2
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/paging.d.ts +3 -12
- package/lib/types/client/paging.d.ts.map +1 -1
- package/lib/types/client/summarize.d.ts.map +1 -1
- package/lib/types/envinit-bootstrap.d.ts +1 -1
- package/lib/types/envinit.d.ts +2 -6
- package/lib/types/envinit.d.ts.map +1 -1
- package/lib/types/hints.d.ts +28 -24
- package/lib/types/hints.d.ts.map +1 -1
- package/lib/types/index.d.ts +21 -40
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/inject.d.ts +3 -2
- package/lib/types/inject.d.ts.map +1 -1
- package/lib/types/interface_gate.d.ts +1 -1
- package/lib/types/prompt.d.ts +17 -41
- package/lib/types/prompt.d.ts.map +1 -1
- package/lib/types/provision.d.ts +3 -0
- package/lib/types/provision.d.ts.map +1 -1
- package/lib/types/remote.d.ts +14 -7
- package/lib/types/remote.d.ts.map +1 -1
- package/lib/types/tool_schema.d.ts +4 -0
- package/lib/types/tool_schema.d.ts.map +1 -1
- package/lib/types/vectorMigration.d.ts +5 -7
- package/lib/types/vectorMigration.d.ts.map +1 -1
- package/package.json +15 -14
- package/lib/engine/mem/config/domains.d.ts +0 -20
- package/lib/engine/mem/db/dao/chunks.d.ts +0 -231
- package/lib/engine/mem/db/dao/documents.d.ts +0 -51
- package/lib/engine/mem/db/hash.d.ts +0 -2
- package/lib/engine/mem/db/knowledge.d.ts +0 -17
- package/lib/engine/mem/eval/index.d.ts +0 -4
- package/lib/engine/mem/git.d.ts +0 -73
- package/lib/engine/mem/router.d.ts +0 -51
- package/lib/engine/mem/store/doc_files.d.ts +0 -125
- package/lib/engine/mem/store/document_text.d.ts +0 -47
- package/lib/engine/mem/store/ingest_guard.d.ts +0 -25
- package/lib/engine/mem/store/knowledge.d.ts +0 -598
- package/lib/engine/mem/store/source_picker.d.ts +0 -43
- package/lib/engine/mem-retrieval/adapters/local_reranker.d.ts +0 -52
- package/lib/engine/mem-retrieval/adapters/none_reranker.d.ts +0 -10
- package/lib/engine/mem-retrieval/rerank.d.ts +0 -13
- package/lib/types/client/domains.d.ts +0 -53
- package/lib/types/client/domains.d.ts.map +0 -1
- package/lib/types/client/ingest.d.ts +0 -24
- package/lib/types/client/ingest.d.ts.map +0 -1
- package/lib/types/open.d.ts +0 -77
- package/lib/types/open.d.ts.map +0 -1
- package/lib/types/reconcile.d.ts +0 -64
- 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
|
|
13
|
-
*
|
|
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;
|