ai-code-engineer 0.1.0__py3-none-any.whl

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 (44) hide show
  1. ai_code_engineer/__init__.py +2 -0
  2. ai_code_engineer/catalog.py +143 -0
  3. ai_code_engineer/chat.py +181 -0
  4. ai_code_engineer/cli.py +384 -0
  5. ai_code_engineer/config.py +405 -0
  6. ai_code_engineer/engine.py +1282 -0
  7. ai_code_engineer/errors.py +27 -0
  8. ai_code_engineer/git_integration.py +443 -0
  9. ai_code_engineer/gui.py +2646 -0
  10. ai_code_engineer/host.py +81 -0
  11. ai_code_engineer/ignore.py +269 -0
  12. ai_code_engineer/intent.py +222 -0
  13. ai_code_engineer/labels.py +871 -0
  14. ai_code_engineer/memory.py +91 -0
  15. ai_code_engineer/modes.py +156 -0
  16. ai_code_engineer/overrides.py +540 -0
  17. ai_code_engineer/planbook.py +192 -0
  18. ai_code_engineer/providers.py +404 -0
  19. ai_code_engineer/redaction.py +54 -0
  20. ai_code_engineer/repair.py +564 -0
  21. ai_code_engineer/report.py +352 -0
  22. ai_code_engineer/runner.py +854 -0
  23. ai_code_engineer/setup.py +386 -0
  24. ai_code_engineer/symbols.py +1286 -0
  25. ai_code_engineer/verification.py +218 -0
  26. ai_code_engineer/webapp/__init__.py +1 -0
  27. ai_code_engineer/webapp/__main__.py +45 -0
  28. ai_code_engineer/webapp/contract.py +36 -0
  29. ai_code_engineer/webapp/controller.py +3556 -0
  30. ai_code_engineer/webapp/fake.py +1141 -0
  31. ai_code_engineer/webapp/launch.py +108 -0
  32. ai_code_engineer/webapp/server.py +349 -0
  33. ai_code_engineer/webapp/static/app.css +780 -0
  34. ai_code_engineer/webapp/static/app.js +2118 -0
  35. ai_code_engineer/webapp/static/boot.js +19 -0
  36. ai_code_engineer/webapp/static/index.html +89 -0
  37. ai_code_engineer/webapp/static/tokens.css +173 -0
  38. ai_code_engineer/workspace.py +385 -0
  39. ai_code_engineer-0.1.0.dist-info/METADATA +7 -0
  40. ai_code_engineer-0.1.0.dist-info/RECORD +44 -0
  41. ai_code_engineer-0.1.0.dist-info/WHEEL +5 -0
  42. ai_code_engineer-0.1.0.dist-info/entry_points.txt +2 -0
  43. ai_code_engineer-0.1.0.dist-info/licenses/LICENSE +21 -0
  44. ai_code_engineer-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,540 @@
1
+ """Configuration the program signs, so an edit made outside it is seen rather than followed.
2
+
3
+ The request was a file a person can change without touching a profile or a window's code — and
4
+ "encrypted, so nobody can edit it except from inside the program". Encryption is the part this tool
5
+ cannot deliver honestly: the standard library has no cipher, the key would have to sit on the same
6
+ account as the file it protects, and the contents are not secret — a credential *value* is refused on
7
+ the way in, so what remains is a model name, an address and four numbers.
8
+
9
+ What does answer the fear is integrity. Every row is signed with a per-install secret, and a file
10
+ whose rows were added, reordered, retyped or deleted by hand is **not obeyed**: the tool names the rows
11
+ it refused and carries on with the profile. Honest scope, in the words the file itself prints — this is
12
+ tamper evidence, not access control. The owner of this account can rewrite both files, and nothing on a
13
+ local filesystem stops them. What an edit from outside the program cannot do is change behaviour
14
+ quietly.
15
+
16
+ Nothing here decides what a value *means*: ``config.py`` owns the provider table, the ranges and the
17
+ one ``validate``. This module owns the file, the signature and the refusal.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import hashlib
22
+ import hmac
23
+ import json
24
+ import os
25
+ from pathlib import Path
26
+ import secrets
27
+ import time
28
+
29
+ from . import redaction
30
+ from .labels import say
31
+
32
+ FILE = ".agent-overrides.json"
33
+ KEY_FILE = ".agent-overrides.key"
34
+
35
+ # The target that means "for every provider". A row naming one provider beats it.
36
+ EVERY = "*"
37
+
38
+ # The keys a row may carry, and the JSON type each is stored as. `provider` is deliberately absent:
39
+ # which vendor answers a task is decided by the row on screen or the profile being read, and a side
40
+ # file able to swap it is the same failure ``providers.py`` refuses upstream fallbacks for — the model
41
+ # that answered would not be the one that was reviewed.
42
+ TYPES = {"model": str, "endpoint": str, "api_key_env": str, "max_turns": int,
43
+ "timeout_seconds": int, "context_chars": int, "output_tokens": int}
44
+
45
+ # A row with target `EVERY` states a preference that holds for every provider. An address and a key
46
+ # variable belong to one provider, so a wildcard row for either would silently aim a different vendor's
47
+ # traffic at it.
48
+ PER_PROVIDER = ("endpoint", "api_key_env")
49
+
50
+ # normcase(path) -> ((mtime_ns, size, key bytes), (accepted rows, refusals)). A snapshot goes out on
51
+ # every streamed log line, so a read must not re-HMAC the disk each time — the same idiom `modes` uses.
52
+ _cache: dict[str, tuple[tuple, tuple[list, list]]] = {}
53
+ # normcase(key path) -> (mtime_ns, size, bytes). The key is read on every snapshot's cache check, so it
54
+ # is cached too: a hot path that stats and reads a file per call is a hot path that shifts a race.
55
+ _keys: dict[str, tuple[int, int, bytes]] = {}
56
+
57
+
58
+ def path(app_dir) -> Path:
59
+ return Path(app_dir) / FILE
60
+
61
+
62
+ def key_path(app_dir) -> Path:
63
+ return Path(app_dir) / KEY_FILE
64
+
65
+
66
+ # ---------------------------------------------------------------- the signature ----------------------------------------------------------------
67
+ def _secret(app_dir, create: bool = False) -> bytes:
68
+ """This install's signing key, or ``b""`` when there is none and none was asked for.
69
+
70
+ Read from a file the program creates and never ships. A key written in the code would be a key in
71
+ the repository, which is the mistake the profiles already refuse to make about API keys.
72
+
73
+ Cached by the file's stamp because ``config`` consults this on the resolution order, and the
74
+ resolution order runs on every snapshot a window pushes — a per-push read of a file nobody writes
75
+ is a cost with no answer behind it.
76
+ """
77
+ file = key_path(app_dir)
78
+ stamp = os.path.normcase(str(file))
79
+ try:
80
+ info = file.stat()
81
+ except OSError:
82
+ _keys.pop(stamp, None)
83
+ return _create_secret(app_dir, stamp) if create else b""
84
+ held = _keys.get(stamp)
85
+ if held and held[0] == (info.st_mtime_ns, info.st_size):
86
+ return held[2]
87
+ try:
88
+ secret = bytes.fromhex(file.read_text(encoding="utf-8").strip())
89
+ except (OSError, ValueError):
90
+ _keys.pop(stamp, None)
91
+ return _create_secret(app_dir, stamp) if create else b""
92
+ _keys[stamp] = (info.st_mtime_ns, info.st_size, secret)
93
+ return secret
94
+
95
+
96
+ def _create_secret(app_dir, stamp: str) -> bytes:
97
+ secret = secrets.token_hex(32)
98
+ file = key_path(app_dir)
99
+ file.parent.mkdir(parents=True, exist_ok=True)
100
+ file.write_text(secret + "\n", encoding="utf-8")
101
+ try:
102
+ os.chmod(file, 0o600) # Windows has no per-user mode; the attempt is the whole promise there
103
+ except OSError:
104
+ pass
105
+ raw = bytes.fromhex(secret)
106
+ _keys[stamp] = (file.stat().st_mtime_ns, file.stat().st_size, raw)
107
+ return raw
108
+
109
+
110
+ def _canonical(rows: list) -> bytes:
111
+ """The bytes that get signed: the list as stored, one key order per row, no whitespace.
112
+
113
+ Not sorted here. ``_write`` already stores the rows in one order it computes itself, so signing
114
+ the stored list means a hand-edit that only *reordered* them changes the bytes and breaks the
115
+ signature — which is an edit, and the whole reason the file is signed.
116
+ """
117
+ return json.dumps(rows, sort_keys=True, separators=(",", ":")).encode("utf-8")
118
+
119
+
120
+ def _signature(secret: bytes, rows: list) -> str:
121
+ return hmac.new(secret, _canonical(rows), hashlib.sha256).hexdigest()
122
+
123
+
124
+ # ---------------------------------------------------------------- the document ----------------------------------------------------------------
125
+ def document() -> dict:
126
+ """The block the file carries beside its rows: what may be set, and to what.
127
+
128
+ Every limit here is *read* from ``config`` instead of written out again. A file that stated its own
129
+ ranges would be a second table, and the second table is what eventually disagrees with the
130
+ validator.
131
+ """
132
+ from . import config # `config` reads this module; one of the two directions has to wait
133
+
134
+ return {
135
+ "what": ("Configuration rows the program signs. Change them from inside the program - "
136
+ "Settings then Overrides, or `agent overrides` - never by editing this file. Rows the "
137
+ "program cannot verify are refused and named, and it runs on the profile instead."),
138
+ "why not encrypted": ("These contents are not secret and a cipher that this install can open "
139
+ "on its own cannot keep a person out of their own file, so the file is "
140
+ "signed instead: an edit made outside the program is seen and refused "
141
+ "rather than followed."),
142
+ "order": ["what you typed into the window for this run",
143
+ "a signed row in this file",
144
+ "the profile being read",
145
+ "the provider's environment variable",
146
+ "the provider's own row inside the program"],
147
+ "keys": {key: (f"a number, {config.LIMITS[key][0]} to {config.LIMITS[key][1]}"
148
+ if key in config.LIMITS else "text, and never a credential value")
149
+ for key in sorted(TYPES)},
150
+ "targets": {key: f"applies when the provider being used is {key}"
151
+ for key in sorted(config.BY_KEY)},
152
+ EVERY: "applies to every provider, and loses to a row that names one",
153
+ "not here": ["the provider - the choice on the screen or in the profile, not a preference",
154
+ "an API key - only the name of the variable holding one, exactly as a profile does",
155
+ "an address for a provider whose profile already spells one out: a row can supply "
156
+ "an address nobody wrote down, but it never redirects one somebody did"],
157
+ }
158
+
159
+
160
+ # ---------------------------------------------------------------- the rules ----------------------------------------------------------------
161
+ def _refusable(key: str, value, target: str) -> str:
162
+ """Why this row cannot be used, or "" when it can. Keys and types — ranges are `validate`'s."""
163
+ if key not in TYPES:
164
+ return "unknown"
165
+ if target == EVERY and key in PER_PROVIDER:
166
+ return "per-provider"
167
+ wanted = TYPES[key]
168
+ if wanted is int:
169
+ if isinstance(value, bool) or not isinstance(value, (int, str)):
170
+ return "not a number"
171
+ try:
172
+ int(str(value).strip())
173
+ except ValueError:
174
+ return "not a number"
175
+ return ""
176
+ if not isinstance(value, str):
177
+ return "not text"
178
+ return "empty" if not value.strip() else ""
179
+
180
+
181
+ # ---------------------------------------------------------------- reading ----------------------------------------------------------------
182
+ def _read(app_dir) -> tuple[list, list]:
183
+ """The rows this file signs, and the rows it refused with the reason for each.
184
+
185
+ A bad signature refuses the whole file rather than one row: the signature covers the row set, so
186
+ deleting or reordering a row is an edit too, and the safe reading of a file nobody can account for
187
+ is "this one is not mine".
188
+ """
189
+ file = path(app_dir)
190
+ stamp = os.path.normcase(str(file))
191
+ try:
192
+ info = file.stat()
193
+ except OSError:
194
+ _cache.pop(stamp, None)
195
+ return [], []
196
+ secret = _secret(app_dir)
197
+ now = ((info.st_mtime_ns, info.st_size), secret)
198
+ cached = _cache.get(stamp)
199
+ if cached and cached[0] == now:
200
+ return cached[1]
201
+ try:
202
+ text = file.read_text(encoding="utf-8")
203
+ except OSError:
204
+ # A file the operating system will not hand over is not a statement about the configuration,
205
+ # so nothing is refused and nothing is applied — the same direction `modes` reads an unreadable
206
+ # declaration in.
207
+ _cache[stamp] = (now, ([], []))
208
+ return [], []
209
+ accepted: list = []
210
+ refused: list = []
211
+ try:
212
+ raw = json.loads(text)
213
+ except ValueError:
214
+ raw = None
215
+ refused.append(_refused(EVERY, "*", "unreadable"))
216
+ if isinstance(raw, dict):
217
+ stored = raw.get("rows")
218
+ if not isinstance(stored, list):
219
+ refused.append(_refused(EVERY, "*", "no-rows"))
220
+ elif not secret or not hmac.compare_digest(str(raw.get("signature") or ""),
221
+ _signature(secret, stored)):
222
+ refused.append(_refused(EVERY, "*", "changed-outside"))
223
+ else:
224
+ for row in stored:
225
+ if not isinstance(row, dict):
226
+ continue
227
+ target = str(row.get("target") or EVERY).strip().casefold() or EVERY
228
+ key = str(row.get("key") or "").strip().casefold()
229
+ why = _refusable(key, row.get("value"), target)
230
+ if why:
231
+ refused.append(_refused(target, key, why))
232
+ else:
233
+ accepted.append({"target": target, "key": key,
234
+ "value": int(row["value"]) if TYPES[key] is int
235
+ else str(row["value"]).strip(),
236
+ "at": str(row.get("at") or "")[:32],
237
+ "by": str(row.get("by") or "")[:40]})
238
+ elif raw is not None:
239
+ refused.append(_refused(EVERY, "*", "wrong-shape"))
240
+ _cache[stamp] = (now, (accepted, refused))
241
+ return accepted, refused
242
+
243
+
244
+ def _refused(target: str, key: str, why: str) -> dict:
245
+ return {"target": target, "key": key, "why": why}
246
+
247
+
248
+ def rows(app_dir) -> list:
249
+ """Every row the file signs and the schema still knows."""
250
+ return [dict(row) for row in _read(app_dir)[0]]
251
+
252
+
253
+ def refusals(app_dir) -> list:
254
+ """What this file asked for that the program will not do — the named half of refuse-and-report."""
255
+ return [dict(row) for row in _read(app_dir)[1]]
256
+
257
+
258
+ def note(app_dir, key: str, why: str, target: str = EVERY) -> None:
259
+ """Record a refusal only the merge could find, so one list still tells the whole story.
260
+
261
+ The rows are individually valid, but a read can still refuse them as a set — a schema that shrank
262
+ between versions is the way. Dropping them silently would be the exact failure this file exists to
263
+ prevent, and raising would stop a task that has nothing to do with the row.
264
+ """
265
+ cached = _cache.get(os.path.normcase(str(path(app_dir))))
266
+ if cached:
267
+ cached[1][1].append(_refused(target, key, why))
268
+
269
+
270
+ def values(app_dir, provider: str) -> dict:
271
+ """The rows that speak for this provider: the wildcard first, so a row naming it wins."""
272
+ merged: dict = {}
273
+ for row in sorted(rows(app_dir), key=lambda item: (item["target"] != EVERY, item["at"])):
274
+ if row["target"] == EVERY or row["target"] == provider:
275
+ merged[row["key"]] = row["value"]
276
+ return merged
277
+
278
+
279
+ def listed(app_dir) -> list[dict]:
280
+ """Rows and refusals together, newest first, for a person asking what is in force."""
281
+ kept = [{**row, "state": "in force", "why": ""} for row in rows(app_dir)]
282
+ refused = [{**row, "state": "refused", "value": ""} for row in refusals(app_dir)]
283
+ return sorted(kept + refused, key=lambda row: row.get("at", ""), reverse=True)
284
+
285
+
286
+ # ---------------------------------------------------------------- writing ----------------------------------------------------------------
287
+ def _write(app_dir, kept: list) -> None:
288
+ from .engine import atomic_json # `engine` reads `config`, which reads this module
289
+
290
+ # One order, decided here and signed here: signing a list and then storing a re-ordered copy of it
291
+ # would leave the program unable to verify its own file.
292
+ ordered = sorted(kept, key=lambda row: (row["target"], row["key"]))
293
+ atomic_json(path(app_dir), {"read first": document(), "rows": ordered,
294
+ "signature": _signature(_secret(app_dir, create=True), ordered)})
295
+ _cache.pop(os.path.normcase(str(path(app_dir))), None)
296
+
297
+
298
+ def ensure(app_dir) -> list[str]:
299
+ """Create the key and the file on first run. An existing file is never rewritten.
300
+
301
+ Born as the schema rather than as an empty object: `{}` documents nothing and would be rewritten
302
+ every launch, while a file that lists its own keys, their ranges and the resolution order is the
303
+ documentation a person actually opens.
304
+ """
305
+ if path(app_dir).exists():
306
+ return []
307
+ key_exists = key_path(app_dir).exists()
308
+ _secret(app_dir, create=True)
309
+ _write(app_dir, [])
310
+ return [KEY_FILE, FILE] if not key_exists else [FILE]
311
+
312
+
313
+ def put(app_dir, target: str, key: str, value, by: str = "", *, arabic: bool = False) -> dict:
314
+ """Sign one row into the file, or raise with the reason this value was refused.
315
+
316
+ The value is judged by the program's own ``validate``, so a row cannot state a limit the tool would
317
+ then refuse at run time — and the sentence that comes back is the validator's, not a second copy of
318
+ the rules written here. The refusals this module writes itself are written in the asking window's
319
+ language; the validator's are English everywhere in this tool, and one more half-translation would
320
+ only make the two windows disagree about a range.
321
+ """
322
+ from . import config
323
+
324
+ target = str(target or EVERY).strip().casefold() or EVERY
325
+ key = str(key or "").strip().casefold()
326
+ if target != EVERY and config.kind_for(target) is None:
327
+ raise config.AgentError(unknown_target(target, arabic=arabic))
328
+ why = _refusable(key, value, target)
329
+ if why:
330
+ raise config.AgentError(bad_value(key, why, arabic=arabic))
331
+ cleaned = {"target": target, "key": key, "at": time.strftime("%Y-%m-%d %H:%M"),
332
+ "by": str(by or "")[:40],
333
+ "value": int(str(value).strip()) if TYPES[key] is int else str(value).strip()}
334
+ if TYPES[key] is str and redaction.redact(cleaned["value"]) != cleaned["value"]:
335
+ raise config.AgentError(credential_refused(key, arabic=arabic))
336
+ kind = config.kind_for(target) or config.DEFAULT_KIND
337
+ config.validate(_candidate(config, kind, key, cleaned["value"]))
338
+ kept = [row for row in rows(app_dir)
339
+ if not (row["target"] == target and row["key"] == key)]
340
+ kept.append(cleaned)
341
+ _write(app_dir, kept)
342
+ return cleaned
343
+
344
+
345
+ def _candidate(config, kind, key: str, value):
346
+ """A Settings for one provider row with this one value in it, for the validator to judge.
347
+
348
+ The provider comes from the row's own target, because that is the only rule set the address has to
349
+ satisfy: a row for Groq is judged by Groq's https rule, not by whichever row the window is on.
350
+ """
351
+ from dataclasses import replace
352
+
353
+ return replace(config.Settings(), provider=kind.key, **{key: value})
354
+
355
+
356
+ def delete(app_dir, target: str, key: str) -> bool:
357
+ """Take one row out of the file; whether it was there is the answer."""
358
+ target = str(target or EVERY).strip().casefold() or EVERY
359
+ key = str(key or "").strip().casefold()
360
+ before = rows(app_dir)
361
+ kept = [row for row in before if not (row["target"] == target and row["key"] == key)]
362
+ if len(kept) == len(before):
363
+ return False
364
+ _write(app_dir, kept)
365
+ return True
366
+
367
+
368
+ # ---------------------------------------------------------------- the sentences ----------------------------------------------------------------
369
+ def fields() -> list[dict]:
370
+ """What the row form offers: every key, whether it takes a number, and the range it is judged by.
371
+
372
+ The ranges are read out of ``config.LIMITS`` rather than repeated here, so a drawer cannot advertise
373
+ a number the validator then refuses — the failure the request timeout already had to be fixed for.
374
+ """
375
+ from . import config
376
+
377
+ out = []
378
+ for key in sorted(TYPES):
379
+ low, high = config.LIMITS.get(key, (None, None))
380
+ out.append({"key": key, "number": TYPES[key] is int, "low": low, "high": high,
381
+ "per_provider": key in PER_PROVIDER})
382
+ return out
383
+
384
+
385
+ def unknown_target(target: str, *, arabic: bool) -> str:
386
+ from . import config
387
+
388
+ return say(arabic,
389
+ en=f"A target is a provider — {', '.join(sorted(config.BY_KEY))} — or {EVERY} for "
390
+ f"all of them, not {target}.",
391
+ ar=f"الهدف يكون اسم بروفايدر — {'، '.join(sorted(config.BY_KEY))} — أو {EVERY} لكل "
392
+ f"البروفايدرات، مش {target}.")
393
+
394
+
395
+ def bad_value(key: str, why: str, *, arabic: bool) -> str:
396
+ from . import config
397
+
398
+ reasons = {"unknown": (f"There is no configuration field named {key} to override.",
399
+ f"مفيش خانة إعداد اسمها {key} تتغيّر."),
400
+ "per-provider": (f"{key} belongs to one provider, so it cannot be set for all of them "
401
+ "at once. Name the provider.",
402
+ f"{key} بتاع بروفايدر واحد، فمش ممكن يتظبط لكل البروفايدرات مع بعض. "
403
+ "اكتب اسم البروفايدر."),
404
+ "not a number": (f"{key} is a whole number.", f"{key} لازم يكون رقم كامل."),
405
+ "not text": (f"{key} is a piece of text.", f"{key} لازم يكون نص."),
406
+ "empty": (f"{key} cannot be blank. Remove the row instead of emptying it.",
407
+ f"{key} ما ينفعش فاضي. امسح السطر بدل ما تسيّبه فاضي.")}
408
+ en, ar = reasons.get(why, (f"{key} cannot be stored: {why}.", f"{key} ما يتخزنش: {why}."))
409
+ if why == "unknown":
410
+ en += " The file lists what may be set: " + ", ".join(sorted(TYPES)) + "."
411
+ ar += " الملف بيقارن اللي ممكن يتغيّر: " + "، ".join(sorted(TYPES)) + "."
412
+ if why == "not a number":
413
+ low, high = config.LIMITS.get(key, (None, None))
414
+ if low is not None:
415
+ en += f" Between {low} and {high}."
416
+ ar += f" بين {low} و {high}."
417
+ return say(arabic, en=en, ar=ar)
418
+
419
+
420
+ def credential_refused(key: str, *, arabic: bool) -> str:
421
+ return say(arabic,
422
+ en=f"{key} looks like a credential, and no key goes in this file. Name the environment "
423
+ f"variable that holds it, exactly as a profile does.",
424
+ ar=f"{key} بيشبه مفتاح، ومفيش مفتاح بيتكتب في الملف ده. اكتب اسم متغير البيئة اللي "
425
+ f"فيه، زي البروفايدر بالظبط.")
426
+
427
+
428
+ def written(row: dict, *, arabic: bool) -> str:
429
+ """What a save costs the operator to believe: the row, what it now overrides, and where it stops."""
430
+ return say(arabic,
431
+ en=f"{row['key']} = {row['value']} for "
432
+ + ("every provider" if row["target"] == EVERY else row["target"])
433
+ + ". Signed, and read by both windows and the terminal from now on.",
434
+ ar=f"{row['key']} = {row['value']} لـ "
435
+ + ("كل البروفايدرات" if row["target"] == EVERY else row["target"])
436
+ + ". اتوقع واتقرأ من النافذتين ومن التيرمنال من دلوقتي.")
437
+
438
+
439
+ def removed(key: str, *, arabic: bool) -> str:
440
+ return say(arabic,
441
+ en=f"{key} is no longer overridden. The profile, the environment variable and the "
442
+ f"provider's own row answer in its place, in that order.",
443
+ ar=f"{key} بقى مش متغيّر. البروفايدر، وبعده متغير البيئة، وبعده صف البروفايدر نفسه "
444
+ f"هي اللي ترد، بالترتيب ده.")
445
+
446
+
447
+ def absent(key: str, *, arabic: bool) -> str:
448
+ return say(arabic, en=f"There was no row for {key} to remove.",
449
+ ar=f"ما كانش في سطر لـ {key} يتشال.")
450
+
451
+
452
+ def nothing_yet(*, arabic: bool) -> str:
453
+ return say(arabic,
454
+ en="Nothing is overridden. Every value comes from its profile, its provider's "
455
+ "environment variable, or the provider's own row.",
456
+ ar="مفيش حاجة متغيّرة. كل قيمة جايّة من البروفايدر، أو من متغير البيئة تبعه، أو من صفه "
457
+ "نفسه.")
458
+
459
+
460
+ def spoken(rows: list, *, arabic: bool) -> list[dict]:
461
+ """One mapping from a row to what a surface may print — used by the file and by the preview.
462
+
463
+ A refusal code is internal; the sentence is ``why_text``'s, in the language the operator asked in,
464
+ and a drawer that drew the code would show an English identifier inside an Arabic window. A refused
465
+ row is not drawn with its value, because the program is not using that value.
466
+ """
467
+ out = []
468
+ for row in rows:
469
+ item = dict(row)
470
+ if item["state"] != "in force":
471
+ item["why"] = why_text(item.get("why", ""), arabic=arabic)
472
+ item["display"] = says(item, arabic=arabic)
473
+ else:
474
+ item["display"] = f"{row['key']} = {row['value']}"
475
+ out.append(item)
476
+ return out
477
+
478
+
479
+ def reported(app_dir, *, arabic: bool) -> list[dict]:
480
+ """The rows on disk, spoken."""
481
+ return spoken(listed(app_dir), arabic=arabic)
482
+
483
+
484
+ def says(row: dict, *, arabic: bool) -> str:
485
+ """What a refused row is called: a row-level one names its key, a file-level one names the file.
486
+
487
+ A wildcard target is spelled out because `*` alone in a sentence reads like a mistake rather than
488
+ like "this row cannot apply to every provider at once".
489
+ """
490
+ if row["key"] == EVERY:
491
+ return say(arabic, en="the whole file", ar="الملف كله")
492
+ target = (say(arabic, en="every provider", ar="كل البروفايدرات")
493
+ if row["target"] == EVERY else row["target"])
494
+ return f"{row['key']} for {target}"
495
+
496
+
497
+ def refused_line(refused: list, *, arabic: bool) -> str:
498
+ """One line naming every row the program would not obey — a refusal without a name reads as a bug."""
499
+ if not refused:
500
+ return ""
501
+ parts = [f"{says(row, arabic=arabic)}: {why_text(row['why'], arabic=arabic)}"
502
+ for row in refused]
503
+ head = say(arabic, en="Overrides not used:", ar="تعديلات ما اتطبقتش:")
504
+ return head + " " + ("؛ " if arabic else "; ").join(parts)
505
+
506
+
507
+ def why_text(why: str, *, arabic: bool) -> str:
508
+ from . import config
509
+
510
+ known = {"changed-outside": (f"the rows were changed outside the program, so none of them are "
511
+ f"trusted — re-save what you want from Settings or `agent overrides`",
512
+ f"السطور اتغيرت من برة البرنامج، فولا واحد فيهم محل ثقة — أعد الحفظ "
513
+ f"من Settings أو `agent overrides`"),
514
+ "unreadable": ("the file could not be read as the JSON the program writes",
515
+ "الملف ما اتقرش كـ JSON اللي البرنامج بيكتبه"),
516
+ "wrong-shape": ("the file is not the shape the program writes",
517
+ "الملف مش بالشكل اللي البرنامج بيكتبه"),
518
+ "no-rows": ("the file holds no row list", "الملف مفيهوش قائمة سطور"),
519
+ "no-longer-valid": ("the rows fit a version of the rules that is not this one, so none of "
520
+ "them were applied",
521
+ "السطور دي ماشية مع قواعد مش قواعد النسخة دي، فولا واحد اتطبق"),
522
+ "unknown": ("no configuration field by that name", "مفيش خانة إعداد بالاسم ده"),
523
+ "per-provider": ("it belongs to one provider", "هي بتاعة بروفايدر واحد"),
524
+ "not a number": ("not a whole number", "مش رقم كامل"),
525
+ "not text": ("not text", "مش نص"),
526
+ "empty": ("empty", "فاضي")}
527
+ en, ar = known.get(why, (why, why))
528
+ return say(arabic, en=en, ar=ar)
529
+
530
+
531
+ def scope(*, arabic: bool) -> str:
532
+ """What the file is, in the words the settings drawer and `agent overrides --list` both print."""
533
+ return say(arabic,
534
+ en="These rows are written only by the program and signed, so an edit made in another "
535
+ "editor is seen and refused rather than followed. It is tamper evidence, not a "
536
+ "password: the owner of this account can still rewrite the file by hand, and what "
537
+ "they lose by doing it is every override in it.",
538
+ ar="السطور دي البرنامج لوحده اللي بيكتبها وبيوقعها، فتعديل أي محرر تاني ليها بيظهر "
539
+ "ويترفض بدل ما ينفذ. دي علامة على التغيير مش كلمة سر: صاحب الحساب ده لسه يقدر "
540
+ "يعيد كتابة الملف بإيده، واللي بيخسره لما يعمل كده هو كل تعديل في الملف.")