Skip to main content

sqlite_graphrag/i18n/validation/
messages_agent_surface.rs

1//! Localized messages for the agent-native output surface.
2//!
3//! Split out of `messages_cli` in v1.2.6: that catalogue already held 397 lines
4//! of unrelated CLI guards, and the refusals GAP-SG-201 through GAP-SG-204
5//! introduce eight more messages in one domain.
6//!
7//! Every message names the flag the caller typed and the way out, because the
8//! error-handling rules require a refusal to carry a corrective action rather
9//! than only a verdict. What is NEVER localized is the JSON around them: field
10//! names stay English so a `pt-BR` operator and an English one parse the same
11//! envelope.
12
13use crate::i18n::{current, Language};
14
15/// Joins alternatives for the "did you mean" tail, or an empty string when the
16/// vocabulary offered nothing close.
17fn did_you_mean(suggestions: &[String]) -> String {
18    if suggestions.is_empty() {
19        return String::new();
20    }
21    let list = suggestions.join(", ");
22    match current() {
23        Language::English => format!("; did you mean: {list}"),
24        Language::Portuguese => format!("; você quis dizer: {list}"),
25    }
26}
27
28/// GAP-SG-207: a subcommand that changes durable state reached path resolution
29/// without naming its target in the argv.
30///
31/// Names both ways out, because a refusal that only states a verdict leaves the
32/// operator guessing: designate the target, or accept the ambient one on
33/// purpose. The flag spellings stay English in both languages, since they are
34/// what the caller must type.
35///
36/// It no longer offers `config set db.path` as a remedy. That key is a HOST
37/// setting, so a write inheriting it is refused by the same fence — pointing the
38/// operator at it would send them one command further into the same refusal.
39pub fn target_not_designated() -> String {
40    match current() {
41        Language::English => String::from(
42            "this subcommand changes durable state and NOTHING named its target: \
43             no --db on the command line and no `db.path` in the configuration, \
44             so the write would land in the compiled default database. Name it \
45             with --db, or accept the default on purpose with --use-active",
46        ),
47        Language::Portuguese => String::from(
48            "este subcomando altera estado durável e NADA nomeou o alvo: nenhum \
49             --db na linha de comando e nenhum `db.path` na configuração, então \
50             a escrita cairia no banco padrão compilado. Nomeie com --db, ou \
51             aceite o padrão de propósito com --use-active",
52        ),
53    }
54}
55
56/// A subcommand that changes durable state inherited its target from the XDG
57/// key `db.path` instead of the argv.
58///
59/// Separate from [`target_not_designated`] because the operator's situation is
60/// different in the way that matters: something DID name a database, it just was
61/// not this invocation. The message therefore has to explain why a value that
62/// looks like a designation is not accepted as one, and the reason is SCOPE —
63/// `db.path` is a host setting, so it names one database for every directory on
64/// the machine rather than the one this command means.
65///
66/// The resolved path is deliberately absent from the text. It reaches the caller
67/// through `db_path_resolved` in the envelope, and repeating it here would
68/// suggest the refusal is about which value was found rather than about which
69/// layer supplied it.
70pub fn target_inherited_from_config() -> String {
71    match current() {
72        Language::English => String::from(
73            "this subcommand changes durable state and its target came from the \
74             `db.path` configuration key, not from this command line. That key is \
75             a HOST setting: it names one database for every directory on this \
76             machine, so it cannot designate the target of a single write. Name \
77             the database with --db, or accept the configured one on purpose with \
78             --use-active",
79        ),
80        Language::Portuguese => String::from(
81            "este subcomando altera estado durável e o alvo dele veio da chave de \
82             configuração `db.path`, não desta linha de comando. Essa chave é do \
83             HOST: ela nomeia um banco para todos os diretórios desta máquina, \
84             então não designa o alvo de uma escrita específica. Nomeie o banco \
85             com --db, ou aceite o configurado de propósito com --use-active",
86        ),
87    }
88}
89
90/// GAP-SG-202: a key given to `--filter`, `--sort` or `--dedupe-by` exists in no
91/// result element.
92pub fn key_absent(flag: &str, key: &str, suggestions: &[String]) -> String {
93    let tail = did_you_mean(suggestions);
94    match current() {
95        Language::English => format!(
96            "{flag} names '{key}', which no result element carries, so the \
97             predicate would reject every row and the empty answer would be \
98             indistinguishable from missing data{tail}. Pass \
99             --allow-unknown-keys to accept an unresolvable key"
100        ),
101        Language::Portuguese => format!(
102            "{flag} nomeia '{key}', que nenhum elemento de resultado carrega, \
103             então o predicado rejeitaria toda linha e a resposta vazia seria \
104             indistinguível de ausência de dado{tail}. Passe \
105             --allow-unknown-keys para aceitar uma chave irresolvível"
106        ),
107    }
108}
109
110/// GAP-SG-203: the key names a member of the envelope, not a field of the
111/// elements the predicate would be applied to.
112pub fn key_is_envelope_only(flag: &str, key: &str, array: &str) -> String {
113    match current() {
114        Language::English => format!(
115            "{flag} names '{key}', which is a member of the envelope and not a \
116             field of the '{array}' elements the predicate would run over. \
117             Applying it would empty '{array}' while '{key}' survived beside the \
118             result, contradicting the predicate. Filter on a field the elements \
119             carry, or read '{key}' from the unshaped envelope"
120        ),
121        Language::Portuguese => format!(
122            "{flag} nomeia '{key}', que é membro do envelope e não campo dos \
123             elementos de '{array}' sobre os quais o predicado rodaria. \
124             Aplicá-lo esvaziaria '{array}' enquanto '{key}' sobreviveria ao lado \
125             do resultado, contradizendo o predicado. Filtre por um campo que os \
126             elementos carreguem, ou leia '{key}' do envelope sem reshaping"
127        ),
128    }
129}
130
131/// GAP-SG-204: a knob was declared against an envelope that carries no result
132/// array, so it can have no effect at all.
133pub fn knob_without_target(flags: &[String]) -> String {
134    let list = flags.join(", ");
135    match current() {
136        Language::English => format!(
137            "{list} was given, but this envelope carries no result array, so the \
138             flag can have no effect. Returning success while silently ignoring \
139             an argument the caller typed is what this refusal exists to prevent"
140        ),
141        Language::Portuguese => format!(
142            "{list} foi passado, mas este envelope não carrega array de \
143             resultado, então a flag não pode ter efeito algum. Retornar sucesso \
144             ignorando em silêncio um argumento que o chamador digitou é \
145             exatamente o que esta recusa existe para impedir"
146        ),
147    }
148}
149
150/// GAP-SG-202: every key given to `--select` is unresolvable, so the projection
151/// would emit empty objects.
152pub fn select_fully_unresolved(keys: &[String], suggestions: &[String]) -> String {
153    let list = keys.join(", ");
154    let tail = did_you_mean(suggestions);
155    match current() {
156        Language::English => format!(
157            "--select names only keys this envelope does not carry ({list}), so \
158             the projection would emit empty objects{tail}. Pass \
159             --allow-unknown-keys to accept that"
160        ),
161        Language::Portuguese => format!(
162            "--select nomeia apenas chaves que este envelope não carrega \
163             ({list}), então a projeção emitiria objetos vazios{tail}. Passe \
164             --allow-unknown-keys para aceitar isso"
165        ),
166    }
167}
168
169/// GAP-SG-201: the predicate would observe only the page the query returned.
170pub fn filter_scope_is_a_page(observed: usize, total: usize, source: &str) -> String {
171    match current() {
172        Language::English => format!(
173            "the query returned {observed} of {total} rows, so --filter would \
174             judge only those {observed} and report an answer about a set it \
175             never saw (the ceiling came from the {source}). Raise the limit to \
176             cover the universe, or declare the narrower intent with \
177             --filter-scope page"
178        ),
179        Language::Portuguese => format!(
180            "a consulta devolveu {observed} de {total} linhas, então --filter \
181             julgaria apenas essas {observed} e reportaria uma resposta sobre um \
182             conjunto que nunca observou (o teto veio do {source}). Amplie o \
183             limite para cobrir o universo, ou declare a intenção mais estreita \
184             com --filter-scope page"
185        ),
186    }
187}
188
189/// GAP-SG-209: a knob that needs the whole set was aimed at a stream.
190///
191/// `export` emits one self-contained JSON record per line, and the surface is
192/// defined over a COMPLETE envelope. Applying a whole-set knob there ran it once
193/// per line: `--count-only export --limit 10` answered with eleven separate
194/// `{"count":1}` lines instead of one count. Naming the flags as data matters
195/// here as much as elsewhere, so `discarded_flags` carries them.
196///
197/// GAP-SG-215 added `--max-items` to the callers of this message and made the
198/// corrective action name `--limit`. "Narrow the query itself" was true and
199/// unactionable: a caller who reached for an output ceiling needs to be told
200/// which flag is the QUERY ceiling, not merely that one exists.
201pub fn knob_needs_a_whole_set(flags: &[String]) -> String {
202    let list = flags.join(", ");
203    match current() {
204        Language::English => format!(
205            "{list} needs a complete result set, but this subcommand emits one \
206             self-contained record per line. Applied here it would run once per \
207             record and answer about a single line instead of the stream. Narrow \
208             the query itself with --limit, or pipe the output to a tool that \
209             spans lines"
210        ),
211        Language::Portuguese => format!(
212            "{list} precisa de um conjunto de resultados completo, mas este \
213             subcomando emite um registro autocontido por linha. Aplicado aqui, \
214             rodaria uma vez por registro e responderia sobre uma linha isolada \
215             em vez do stream. Estreite a própria consulta com --limit, ou \
216             canalize a saída para uma ferramenta que atravesse linhas"
217        ),
218    }
219}
220
221/// GAP-SG-215: `--filter` on a stream would desynchronise the trailer's tally.
222///
223/// Separate from [`knob_needs_a_whole_set`] because the reason is different, and
224/// a refusal that gives the wrong reason teaches the caller the wrong fix. A
225/// predicate CAN be evaluated per record; what it cannot do is stay consistent
226/// with a count the command computed before the surface saw a single line. The
227/// corrective action is therefore the query's own narrowing flags, not a wider
228/// output ceiling.
229pub fn filter_would_desync_a_tally() -> String {
230    match current() {
231        Language::English => "--filter cannot narrow a stream: this subcommand emits one record \
232             per line and its summary line counts the records the QUERY \
233             returned, so a predicate applied here would leave that count \
234             describing rows you never received. Narrow the query instead — \
235             --type, --namespace or --limit"
236            .to_string(),
237        Language::Portuguese => "--filter não pode estreitar um stream: este subcomando emite um \
238             registro por linha e a linha de sumário conta os registros que a \
239             CONSULTA devolveu, então um predicado aplicado aqui deixaria essa \
240             contagem descrevendo linhas que você nunca recebeu. Estreite a \
241             consulta em vez disso — --type, --namespace ou --limit"
242            .to_string(),
243    }
244}
245
246/// GAP-SG-201: `--count-only` over an incomplete universe emits a bare number
247/// that reads as an inventory.
248pub fn count_only_over_a_page(observed: usize, total: usize) -> String {
249    match current() {
250        Language::English => format!(
251            "--count-only would emit a bare number counted over {observed} of \
252             {total} rows, which a caller reads as the inventory. Raise the \
253             limit, or declare --filter-scope page to accept a count of the page"
254        ),
255        Language::Portuguese => format!(
256            "--count-only emitiria um número isolado contado sobre {observed} de \
257             {total} linhas, que o chamador lê como o inventário. Amplie o \
258             limite, ou declare --filter-scope page para aceitar a contagem da \
259             página"
260        ),
261    }
262}