1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
//! Retrieval tuning and the agent-native surface vocabulary.
//!
//! Split out of the former single-file `constants.rs` in v1.2.5;
//! every item is re-exported by the parent module, so `crate::constants::X`
//! resolves exactly as before.
/// Jaccard threshold above which two memories are considered fuzzy duplicates.
pub const DEDUP_FUZZY_THRESHOLD: f64 = 0.8;
/// Cosine distance threshold below which two memories are semantic duplicates.
pub const DEDUP_SEMANTIC_THRESHOLD: f32 = 0.1;
/// Maximum number of hops allowed in graph traversals.
pub const MAX_GRAPH_HOPS: u32 = 2;
/// Minimum relationship weight required for traversal inclusion.
pub const MIN_RELATION_WEIGHT: f64 = 0.3;
/// Default traversal depth for `related` when `--hops` is omitted.
pub const DEFAULT_MAX_HOPS: u32 = 2;
/// Default minimum weight filter applied during graph traversal.
pub const DEFAULT_MIN_WEIGHT: f64 = 0.3;
/// Default weight assigned to newly created relationships.
pub const DEFAULT_RELATION_WEIGHT: f64 = 0.5;
/// Default `k` used by `recall` when the caller omits `--k`.
pub const DEFAULT_K_RECALL: usize = 10;
/// Default `k` for memory KNN searches when the caller omits `--k`.
pub const K_MEMORIES_DEFAULT: usize = 10;
/// Default `k` for entity KNN searches during graph expansion.
pub const K_ENTITIES_SEARCH: usize = 5;
/// Default `k` constant used by Reciprocal Rank Fusion in `hybrid-search`.
pub const RRF_K_DEFAULT: u32 = 60;
/// Maximum result count from the recursive graph CTE in `recall`.
pub const K_GRAPH_MATCHES_LIMIT: usize = 20;
/// Default `--limit` for `list` in a HUMAN format, when the caller omits it.
///
/// Bounds the text rendering only. Under `--format json` an omitted `--limit`
/// means the whole corpus, because a machine consumer that asked for no ceiling
/// must not silently receive a page — that asymmetry is the whole of GAP-SG-201.
///
/// Declared as 100 until v1.2.7 and referenced by nothing, while `list` carried
/// a bare `50` in its body: a constant that documented a default the code did
/// not use is worse than no constant, since it invites a reader to trust it.
pub const K_LIST_TEXT_DEFAULT_LIMIT: usize = 50;
/// Default `--limit` for `graph entities` when omitted.
pub const K_GRAPH_ENTITIES_DEFAULT_LIMIT: usize = 50;
/// Default `--limit` for `related` when omitted.
///
/// Same value as [`DEFAULT_K_RECALL`], which `related` used until v1.2.7 — a
/// borrowed name that tied this command's default to `recall`'s `-k` by accident
/// rather than by intent. Tuning one would silently have moved the other.
pub const K_RELATED_DEFAULT_LIMIT: usize = 10;
/// Default `--limit` for `history` when omitted.
pub const K_HISTORY_DEFAULT_LIMIT: usize = 20;
/// Maximum edges pulled when `deep-research` expands the graph around its hits.
pub const K_DEEP_RESEARCH_GRAPH_EDGES_LIMIT: usize = 50;
/// Default weight for the vector contribution in the `hybrid-search` RRF formula.
pub const WEIGHT_VEC_DEFAULT: f64 = 1.0;
/// Default weight for the BM25 text contribution in the `hybrid-search` RRF formula.
pub const WEIGHT_FTS_DEFAULT: f64 = 1.0;
/// GAP-SG-142: envelope members searched, in order, for the primary result
/// array reshaped by [`crate::agent_surface`].
///
/// The list is ordered from most to least specific so an envelope that carries
/// several arrays (for example `recall`, which exposes `direct_matches`,
/// `graph_matches` and the merged `results`) is reshaped on the member callers
/// actually consume. A payload matching none of these falls back to its first
/// array member.
///
/// `nodes` precedes `entities` because the `graph` envelope carries both and
/// `nodes` is the canonical one there; `entities` is its v1.0.66 alias and is
/// listed in [`AGENT_SURFACE_ALIAS_ARRAYS`]. Reshaping the alias while leaving
/// the canonical member untouched is precisely the failure that table closes.
/// `types` is last and is the only member here named after what it holds rather
/// than after its role. It belongs to `graph entity-types` (v1.2.8), whose whole
/// envelope is that one array; without the entry the fallback still elected it,
/// but as a guess, and `--select type` was then resolved against the top-level
/// members and refused with a suggestion identical to what the caller typed.
pub const AGENT_SURFACE_RESULT_KEYS: & = &;
/// GAP-SG-142: derived result arrays suppressed once the agent-native surface
/// reshapes their canonical source.
///
/// Each entry is `(subcommand, canonical member, members that merely restate
/// it)`: `list` clones `items` into `memories`, `graph export` clones `nodes`
/// into `entities`, `recall` publishes `results` as the concatenation of
/// `direct_matches` and `graph_matches`, and `related` clones `results` into
/// `related_memories`.
///
/// The subcommand is part of the key because "derived" is a property of one
/// command's envelope, not of a member name. `results` means a concatenation in
/// `recall` and a clone in `related`, and in `hybrid-search` it means neither:
/// there `graph_expansion` skips every id already present in `results`, so
/// `results` and `graph_matches` are DISJOINT and carry different types
/// (`HybridSearchItem` against `RecallItem`). Matching on the member name alone
/// deleted a set no other member restated — and one that
/// `docs/schemas/hybrid-search.schema.json` lists under `required`, so the
/// suppression produced an envelope invalid against this project's own schema.
/// `hybrid-search` is absent from this table by construction, which is what
/// keeps that from happening again.
///
/// Suppression only removes members that are actually present, so a declared
/// member the envelope never carried is a silent no-op and is never reported as
/// removed.
///
/// The surface reshapes exactly one array per envelope, so leaving a genuinely
/// derived member in place shipped the unfiltered, unsorted, unprojected copy
/// right next to the shaped one — the redundancy the projection exists to
/// remove, and a meta record (`sort`, `output_count`) that contradicted half the
/// payload. Those are therefore dropped whenever a knob is set.
///
/// Without any knob the surface is a no-op and nothing is removed, so the public
/// v1.0.66 alias contract stays intact byte for byte for every existing caller.
pub const AGENT_SURFACE_ALIAS_ARRAYS: & = &;
/// GAP-SG-230: field spellings the agent-native surface treats as ONE key.
///
/// Each entry pairs a SCOPE with a synonym group: every spelling in the group
/// names the same field, so a caller that asks for any member is asking for
/// whichever member the payload actually carries. Read the applicable groups
/// through [`agent_surface_field_synonym_groups`], never directly.
///
/// The first group is the entity type. `graph entities`,
/// `memory-entities`, `read --with-graph` and `deep-research.graph_context` emit
/// it as `entity_type`; `graph --format json` and `graph --format ndjson` emit it
/// as `type` (`NodeOut` and `NdjsonNode` both carry
/// `#[serde(rename = "type")] r#type`). A caller that learned the spelling on one
/// surface got `unresolved_keys: ["entity_type"]`, `vocabulary_partial: true` and
/// `exit 0` on its sibling — a silent miss, which is the failure class the whole
/// agent-native gate exists to remove. The INPUT side already closed this
/// asymmetry: `src/storage/entities/mod.rs` declares
/// `#[serde(alias = "type")] pub entity_type`, and
/// `docs/schemas/entities-input.schema.json` documents `type` as a synonym in
/// prose. This table is the output half of that same contract.
///
/// # GAP-SG-274: the scope column
///
/// The first member of each entry lists the
/// [`crate::cli::Commands::agent_surface_slug`] values the group applies to; an
/// EMPTY list means "every command", which is what the entity-type group needs
/// since `memory-entities`, `read` and `deep-research` report no slug at all.
///
/// The column exists because one group is true of a command in one output mode
/// and false of the same command in another. `kind` is that group. In `NodeOut`
/// (the json snapshot) `kind: String` is the deprecated alias of the entity
/// type — `src/commands/graph_export/tests.rs` asserts
/// `json["kind"] == json["type"]`. In `NdjsonNode` (the ndjson stream)
/// `kind: &'static str` is the LINE DISCRIMINATOR, valued `"node"`, `"edge"` or
/// `"summary"`. Declaring it a synonym for BOTH would make `--filter
/// kind=concept` reach edge and summary lines, and `--select type` answer
/// `"edge"` for an edge — the mistake [`AGENT_SURFACE_ALIAS_ARRAYS`] narrates for
/// member names, repeated one layer down on field names.
///
/// Until the slug distinguished the two modes there was no way to say "here and
/// not there", so `kind` was excluded from the table ENTIRELY and the caller who
/// spelled the entity type the way the json snapshot spells it got a silent miss
/// on every sibling surface. Now `agent_surface_slug` reports `graph-ndjson` for
/// the stream and `graph` for every other form of the command — the same
/// distinction [`crate::cli::Commands::streams`] computes — so the group is
/// declared exactly where it holds: under `graph`, never under `graph-ndjson`.
pub const AGENT_SURFACE_FIELD_SYNONYMS: & = &;
/// GAP-SG-274: the synonym groups that apply to `command`, in table order.
///
/// `command` is the slug [`crate::cli::Commands::agent_surface_slug`] reported
/// for this invocation, or `None` when the surface was never told which
/// subcommand emitted the envelope. `None` selects the unscoped groups alone,
/// which is the fail-safe reading: a command that does not identify itself gets
/// the synonyms that are true everywhere and none of the ones that are true only
/// somewhere.
+ '_
/// DEFAULT cap on `hybrid-search --with-graph` graph matches.
///
/// ACTIVE by default, unlike the `recall` flag of the same name, which defaults
/// to unbounded. `hybrid-search` had no cap at all: `graph_expansion` walks
/// outward from the fused results AND from the five entities nearest the query
/// embedding, then materialises every memory it reaches with a 300-character
/// snippet each. A `--k 3` query over a dense neighbourhood measured a 1 112 925
/// byte envelope — the caller asked for three results and got a megabyte.
///
/// A finite default is the only honest shape here: the flag caps a set the
/// caller never sized, so leaving it unbounded means the envelope is bounded by
/// the graph rather than by the request. 50 keeps a genuinely useful
/// neighbourhood while holding the envelope in the tens of kilobytes.
///
/// Read it through [`hybrid_search_max_graph_results`], never directly.
pub const DEFAULT_HYBRID_MAX_GRAPH_RESULTS: usize = 50;
/// Graph-match ceiling for `hybrid-search`: the `--max-graph-results` flag, then
/// XDG `search.hybrid.max_graph_results`, then
/// [`DEFAULT_HYBRID_MAX_GRAPH_RESULTS`].
///
/// `0` disables the cap at either layer, which is how a caller opts back into
/// the unbounded pre-v1.2.2 envelope. Returns `None` for that case so the
/// traversal loop can skip the check entirely.
/// Elements sampled when the surface builds the key vocabulary for a SUGGESTION.
///
/// GAP-SG-202: this bounds the suggestion only, never the resolution. Deciding
/// whether a requested key exists scans every element, because the scan is a
/// pointer walk per element with no allocation and a wrong `absent` verdict
/// would refuse a legitimate request. Listing the alternatives is the expensive
/// half — it collects names — and it only runs once a key has already failed,
/// so a sample is enough to name a near miss.
pub const K_VOCABULARY_SAMPLE_ELEMENTS: usize = 64;
/// Hard ceiling on distinct key names collected for a suggestion.
///
/// The envelope is caller-influenced, so the collector is a public parser: the
/// memory rules forbid sizing an allocation from untrusted input without a
/// ceiling. Reaching it costs a shorter suggestion list, never a refusal.
pub const K_VOCABULARY_MAX_KEYS: usize = 512;
/// Alternatives named in a refusal message.
///
/// Three is what a caller can act on at a glance; a longer list reads as a dump
/// of the schema rather than as a correction.
pub const K_VOCABULARY_MAX_SUGGESTIONS: usize = 3;
/// Jaro-Winkler similarity below which a candidate is not offered as a fix.
///
/// Jaro-Winkler rather than plain edit distance because it rewards a shared
/// prefix, and a mistyped key name almost always keeps its prefix — `body_length`
/// against `body`, `entity_type` against `entity`.
pub const VOCABULARY_SUGGESTION_MIN_SIMILARITY: f64 = 0.6;
/// Default cap on emitted result elements (`--max-items`). `0` means no cap,
/// preserving the pre-GAP-SG-142 envelope byte for byte.
pub const DEFAULT_AGENT_SURFACE_MAX_ITEMS: usize = 0;
/// Default cap on string length in characters (`--truncate-content`).
/// `0` disables content truncation.
pub const DEFAULT_AGENT_SURFACE_TRUNCATE_CONTENT: usize = 0;
/// Default cap on the serialized envelope in bytes (`--max-output-bytes`).
/// `0` disables the ceiling.
pub const DEFAULT_AGENT_SURFACE_MAX_OUTPUT_BYTES: usize = 0;
/// Inclusive upper bound for `-k`/`--k` on every retrieval command.
///
/// Kept at the historical `sqlite-vec` knn ceiling so the message an operator
/// gets does not change: values above it used to surface a leaky engine error
/// (`k value in knn query too large, provided 10000 and the limit is 4096`).
pub const K_QUERY_RANGE_MAX: usize = 4_096;
/// Inclusive upper bound for `--limit` on commands that page over stored rows.
///
/// Separate from [`K_QUERY_RANGE_MAX`] because `export --limit` ships a default
/// of 100_000, so the retrieval ceiling would be a breaking change there. These
/// limits reach SQLite as a `LIMIT` clause, where the row count bounds the work
/// no matter what the operator asks for; the ceiling exists to reject absurd
/// input at parse time rather than to protect memory.
pub const K_LIST_LIMIT_MAX: usize = 1_000_000;
/// Inclusive upper bound for `--max-hops` and `--depth` on graph traversal.
///
/// The breadth-first walks carry visited sets, so a huge value terminates at
/// the graph diameter rather than running away. The bound is here to keep the
/// surface honest, and because a request for more than sixty-four hops is a
/// typo in every real corpus.
pub const K_MAX_HOPS_CEILING: u32 = 64;
/// Inclusive upper bound for `deep-research --max-sub-queries`.
///
/// Unlike the other ceilings this one guards spend, not memory: each sub-query
/// is a separate REST round trip, so an unbounded value bills the operator for
/// an unbounded fan-out.
pub const K_MAX_SUB_QUERIES_CEILING: usize = 64;