brainiac-cli 0.16.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 (77) hide show
  1. brain/__init__.py +39 -0
  2. brain/__main__.py +14 -0
  3. brain/_assets/AGENTS.md +564 -0
  4. brain/_assets/overlay/template/brand/brand-guide.md +24 -0
  5. brain/_assets/overlay/template/keywords/glossary.md +15 -0
  6. brain/_assets/overlay/template/people/roster.md +14 -0
  7. brain/_assets/overlay/template/voice/voice-profile.md +34 -0
  8. brain/_assets/routines/manifest.json +257 -0
  9. brain/_assets/scripts/brain-brief-mac.plist +59 -0
  10. brain/_assets/scripts/brain-brief.sh +74 -0
  11. brain/_assets/scripts/brain-synthesis-mac.plist +48 -0
  12. brain/_assets/scripts/brain-synthesis.sh +214 -0
  13. brain/_assets/scripts/install-brief-mac.sh +152 -0
  14. brain/_assets/scripts/install-brief-windows.ps1 +97 -0
  15. brain/_assets/scripts/register_tasks.py +386 -0
  16. brain/_assets/templates/company.md +21 -0
  17. brain/_assets/templates/concept.md +27 -0
  18. brain/_assets/templates/daily.md +20 -0
  19. brain/_assets/templates/decision.md +42 -0
  20. brain/_assets/templates/meeting.md +33 -0
  21. brain/_assets/templates/person.md +20 -0
  22. brain/_assets/templates/project.md +23 -0
  23. brain/_assets/templates/state-moc.md +40 -0
  24. brain/_version.py +12 -0
  25. brain/anchor.py +121 -0
  26. brain/audit.py +422 -0
  27. brain/backup.py +210 -0
  28. brain/brief.py +417 -0
  29. brain/capture.py +117 -0
  30. brain/chunk.py +249 -0
  31. brain/classification.py +134 -0
  32. brain/cli.py +1906 -0
  33. brain/config.py +368 -0
  34. brain/connect.py +362 -0
  35. brain/context.py +108 -0
  36. brain/core.py +3018 -0
  37. brain/doctor.py +1161 -0
  38. brain/egress.py +148 -0
  39. brain/embed.py +857 -0
  40. brain/encryption.py +217 -0
  41. brain/frontmatter.py +102 -0
  42. brain/golden_probe.py +678 -0
  43. brain/graph.py +369 -0
  44. brain/graphify.py +352 -0
  45. brain/index.py +1576 -0
  46. brain/ingest/__init__.py +19 -0
  47. brain/ingest/handlers/__init__.py +43 -0
  48. brain/ingest/handlers/base.py +95 -0
  49. brain/ingest/handlers/docx.py +78 -0
  50. brain/ingest/handlers/email.py +228 -0
  51. brain/ingest/handlers/html.py +142 -0
  52. brain/ingest/handlers/image.py +91 -0
  53. brain/ingest/handlers/pdf.py +99 -0
  54. brain/ingest/handlers/pptx.py +69 -0
  55. brain/ingest/handlers/tables.py +41 -0
  56. brain/ingest/handlers/text.py +43 -0
  57. brain/ingest/handlers/xlsx.py +100 -0
  58. brain/ingest/handlers/zip.py +163 -0
  59. brain/ingest/pipeline.py +839 -0
  60. brain/ingest/transcript.py +158 -0
  61. brain/init.py +870 -0
  62. brain/maintenance.py +2266 -0
  63. brain/mcp_adapter.py +217 -0
  64. brain/multihop.py +232 -0
  65. brain/notes.py +195 -0
  66. brain/overlay.py +183 -0
  67. brain/projection.py +79 -0
  68. brain/rerank.py +425 -0
  69. brain/snapshot.py +231 -0
  70. brain/update.py +743 -0
  71. brain/vectors.py +225 -0
  72. brainiac_cli-0.16.0.dist-info/METADATA +306 -0
  73. brainiac_cli-0.16.0.dist-info/RECORD +77 -0
  74. brainiac_cli-0.16.0.dist-info/WHEEL +5 -0
  75. brainiac_cli-0.16.0.dist-info/entry_points.txt +4 -0
  76. brainiac_cli-0.16.0.dist-info/licenses/LICENSE +202 -0
  77. brainiac_cli-0.16.0.dist-info/top_level.txt +1 -0
brain/golden_probe.py ADDED
@@ -0,0 +1,678 @@
1
+ """Golden-set regression probes over the `brain` CLI (WD-02).
2
+
3
+ GENERIC runner: the probes (queries + expected anchors) live in a per-vault
4
+ probes file OUTSIDE this repo — this module never embeds vault content and
5
+ takes the probes-file path as its one required argument. It shells out to
6
+ the installed `brain` CLI with ``--json`` and imports NOTHING from the
7
+ engine, so what it measures is exactly the gated surface an agent sees
8
+ (egress filter included).
9
+
10
+ Four probe classes, one per measured historical failure mode:
11
+
12
+ decision_state ``brain dossier`` must surface the decided claim in the
13
+ DECISION layer (catches egress starvation, decision-layer
14
+ crowding, uncaptured decisions).
15
+ currency the expected note's version-chain HEAD must be latest
16
+ (catches version-anchoring / recency poisoning).
17
+ freshness a note updated in the last N days must be reachable
18
+ through the gated read surface (catches sweep/ingest
19
+ death).
20
+ tension a decision with a known newer source must carry the
21
+ ``tensions`` flag (catches the proposal-promotion class).
22
+
23
+ Anchoring is by STABLE ANCHOR, not literal HEAD id: the self-organizing
24
+ folds (VER-01/PAR-01/supersede) legitimately retire ids, so when an anchor
25
+ id has been superseded the runner FOLLOWS the version chain to the current
26
+ HEAD instead of firing a false alarm; a decision can also be anchored on
27
+ its CLAIM text (``claim_any`` substrings) — when both are given and the
28
+ anchor id is gone, the runner falls through to the claim match before
29
+ declaring anything invalid. A MISSING anchor with no claim fallback is
30
+ probe-INVALID (loud) — never a silent pass — and deterministic invalidity
31
+ is distinguished from transient CLI failure so a scheduler retries only
32
+ the transient class.
33
+
34
+ Scoring: weighted mean over the VALID probes -> one 0-1 number; every
35
+ probe also emits pass/fail + a reason string. Weights are validated at
36
+ load (finite, non-negative) — a bad weight is config-invalid, never
37
+ scored; an all-zero weight total is unscoreable config, not a regression.
38
+
39
+ Exit codes (the s07 maintain-fold interface — disposition precedence
40
+ action_required > regression > transient > ok: a deterministic config
41
+ problem outranks everything; a real regression on the RESOLVED probes is
42
+ never masked by an unrelated transient sibling; transient backs off only
43
+ when nothing worse is known):
44
+
45
+ 0 ok score >= threshold, nothing invalid or transient
46
+ 1 regression resolved-probe score < threshold — a real retrieval
47
+ regression (fires even if a sibling probe is transient)
48
+ 2 action_required deterministic config problem (malformed probes file,
49
+ missing/renamed anchor id, non-numeric/negative
50
+ weight or field, zero weight total, supersession
51
+ cycle/runaway): fix the probes file / vault; do NOT
52
+ retry before the next scheduled run
53
+ 3 transient the brain CLI itself failed / emitted non-JSON —
54
+ bounded backoff is appropriate
55
+
56
+ The emitted JSON ALWAYS carries the same ``disposition``/``exit_code`` the
57
+ process exits with — no path crashes to a bare traceback/exit 1.
58
+
59
+ Usage:
60
+ brain-golden-probe <probes.json> [--vault DIR] [--brain-cmd CMD]
61
+ [--threshold F] [--max-tier TIER] [-k N] [--timeout S]
62
+ python -m brain.golden_probe <probes.json> ...
63
+ """
64
+ from __future__ import annotations
65
+
66
+ import argparse
67
+ import datetime as _dt
68
+ import json
69
+ import math
70
+ import shlex
71
+ import subprocess
72
+ import sys
73
+ from typing import Any, Callable, Optional
74
+
75
+ Call = Callable[[list], tuple] # (args) -> (returncode, stdout)
76
+
77
+ EXIT_OK, EXIT_REGRESSION, EXIT_ACTION_REQUIRED, EXIT_TRANSIENT = 0, 1, 2, 3
78
+ # The cycle check (`cur in seen`) is the real guard against a corrupt chain;
79
+ # this is only a safety net against a pathological NON-cyclic linear chain.
80
+ # 100 is well above any realistic supersession-family depth (a note the nightly
81
+ # folds revise a few times a year takes ~decades to approach it — finding [204]:
82
+ # a low ceiling of 10 false-alarmed a long, correctly-evolving family) while
83
+ # still bounding a runaway walk to ~100 sequential `get`s, not ~1000 (finding
84
+ # [228]: each hop is a subprocess round-trip).
85
+ MAX_CHAIN_HOPS = 100
86
+ # The fixed egress-tier vocabulary (hardcoded because this runner is
87
+ # deliberately stdlib-only / engine-decoupled). Kept in sync with
88
+ # classification.TIERS by hand — an unknown --max-tier is a config error, not a
89
+ # retryable transient (review finding [487]).
90
+ VALID_TIERS = ("Public", "Internal", "Confidential", "Restricted", "MNPI")
91
+
92
+ PROBE_CLASSES = ("decision_state", "currency", "freshness", "tension")
93
+ _REQUIRED_KEYS = {
94
+ "decision_state": ("query",),
95
+ "currency": ("anchor_id",),
96
+ "freshness": ("max_age_days",),
97
+ "tension": ("query", "anchor_id"),
98
+ }
99
+
100
+
101
+ class ProbeFail(Exception):
102
+ """The probe ran deterministically and the expectation did NOT hold."""
103
+
104
+
105
+ class ProbeInvalid(Exception):
106
+ """Deterministic CONFIG failure (missing anchor id, malformed probe) —
107
+ action_required, never a silent pass, never retried before next run.
108
+
109
+ ``kind`` distinguishes a RETIRED/renamed anchor id (``"missing_anchor"`` —
110
+ legitimate vault evolution the stable-anchor claim fallback recovers from)
111
+ from real chain CORRUPTION (``"chain_corrupt"`` — a cycle/runaway that must
112
+ NOT be papered over by the claim fallback; review finding [245])."""
113
+
114
+ def __init__(self, msg: str, *, kind: str = "config"):
115
+ super().__init__(msg)
116
+ self.kind = kind
117
+
118
+
119
+ class ProbeTransient(Exception):
120
+ """The brain CLI itself failed (crash / timeout / non-JSON output)."""
121
+
122
+
123
+ # ---------------------------------------------------------------------------
124
+ # CLI plumbing
125
+ # ---------------------------------------------------------------------------
126
+ def make_subprocess_call(brain_cmd: str, vault: Optional[str],
127
+ timeout: float) -> Call:
128
+ """Production ``call``: run the installed brain CLI, capture stdout only
129
+ (warnings go to stderr). Resolved from PATH / an explicit --brain-cmd —
130
+ never repo-relative, so a launchd run with no repo CWD works."""
131
+ base = shlex.split(brain_cmd)
132
+ if vault:
133
+ base += ["--vault", vault]
134
+
135
+ def call(args: list) -> tuple:
136
+ proc = subprocess.run( # noqa: S603 — operator-supplied command
137
+ base + list(args), capture_output=True, text=True, timeout=timeout)
138
+ return proc.returncode, proc.stdout
139
+
140
+ return call
141
+
142
+
143
+ def _cli_json(call: Call, args: list) -> tuple:
144
+ """One brain CLI invocation -> (rc, parsed-JSON). Any crash or
145
+ unparseable output is TRANSIENT (retryable), by definition: the probe
146
+ never got a deterministic answer."""
147
+ try:
148
+ rc, out = call(args)
149
+ except Exception as exc: # timeout, OSError, missing binary
150
+ raise ProbeTransient(f"brain CLI failed to run ({args[0]}): {exc}")
151
+ if not out.strip():
152
+ # A `--json` command ALWAYS emits a JSON object; empty stdout is a CLI
153
+ # malfunction, not a valid empty payload (review finding [152]: a
154
+ # rc=0 empty response was read as `{}` and treated as a real note).
155
+ raise ProbeTransient(f"empty output from `brain {args[0]}` (rc={rc})")
156
+ try:
157
+ payload = json.loads(out)
158
+ except ValueError:
159
+ raise ProbeTransient(
160
+ f"non-JSON output from `brain {args[0]}` (rc={rc})")
161
+ return rc, payload
162
+
163
+
164
+ def _coerce_num(value: Any, field: str, *, integer: bool = False,
165
+ minimum: Optional[float] = None, maximum: Optional[float] = None,
166
+ exclusive_min: bool = False) -> Any:
167
+ """Guarded numeric parse. A non-numeric / non-finite / out-of-range
168
+ value in the probes file is a deterministic CONFIG error (ProbeInvalid
169
+ -> action_required/exit 2) — never an uncaught crash, never scored."""
170
+ if isinstance(value, bool):
171
+ raise ProbeInvalid(f"{field} must be a number, got {value!r}")
172
+ try:
173
+ num = float(value)
174
+ except (TypeError, ValueError):
175
+ raise ProbeInvalid(f"{field} must be a number, got {value!r}")
176
+ if not math.isfinite(num):
177
+ raise ProbeInvalid(f"{field} must be finite, got {value!r}")
178
+ if minimum is not None and (num <= minimum if exclusive_min else num < minimum):
179
+ rel = ">" if exclusive_min else ">="
180
+ raise ProbeInvalid(f"{field} must be {rel} {minimum}, got {value!r}")
181
+ if maximum is not None and num > maximum:
182
+ raise ProbeInvalid(f"{field} must be <= {maximum}, got {value!r}")
183
+ return int(num) if integer else num
184
+
185
+
186
+ def _tier_args(max_tier: Optional[str]) -> list:
187
+ # Deliberately EMPTY by default: the probes must measure the surface an
188
+ # agent actually gets (default egress cap included) — pinning a tier
189
+ # would hide a regressed default cap (the round-1 starvation class).
190
+ return ["--max-tier", max_tier] if max_tier else []
191
+
192
+
193
+ def _clean_link(raw: Any) -> str:
194
+ """Normalize a supersession pointer: '[[id|display]]' / 'id.md' -> 'id'."""
195
+ s = str(raw or "").strip().strip('"').strip("'")
196
+ if s.startswith("[[") and s.endswith("]]"):
197
+ s = s[2:-2]
198
+ s = s.split("|", 1)[0].strip()
199
+ if s.endswith(".md"):
200
+ s = s[:-3]
201
+ return s
202
+
203
+
204
+ def _get_note(call: Call, note_id: str, max_tier: Optional[str]) -> tuple:
205
+ """-> (status, note) with status in ok|missing|withheld."""
206
+ rc, payload = _cli_json(
207
+ call, ["get", note_id, "--json"] + _tier_args(max_tier))
208
+ if rc == 0:
209
+ return "ok", payload
210
+ if rc == 1 and payload.get("error") == "not_found":
211
+ return "missing", None
212
+ if rc == 2 and payload.get("error") == "withheld_by_egress_filter":
213
+ return "withheld", None
214
+ raise ProbeTransient(f"`brain get {note_id}` returned rc={rc}")
215
+
216
+
217
+ def _chain_head(call: Call, anchor_id: str,
218
+ max_tier: Optional[str]) -> tuple:
219
+ """Follow the supersession chain from ``anchor_id`` to its HEAD.
220
+
221
+ A superseded anchor is CORRECT vault evolution (the folds retire ids
222
+ nightly) — follow it, don't alarm. A missing ANCHOR is config-invalid;
223
+ a broken link mid-chain or a withheld note is a real integrity FAIL; a
224
+ cycle / runaway chain is deterministic and unevaluable -> invalid
225
+ (action_required), never a retry.
226
+ -> (head_id, head_note, hops)
227
+ """
228
+ seen: list = []
229
+ cur = anchor_id
230
+ # Bound = MAX_CHAIN_HOPS *links* (MAX_CHAIN_HOPS + 1 notes): the HEAD
231
+ # of a legitimately MAX-hop family is always inspected; only a chain
232
+ # STILL carrying superseded_by past the bound is a runaway.
233
+ while len(seen) <= MAX_CHAIN_HOPS:
234
+ if cur in seen:
235
+ raise ProbeInvalid(
236
+ f"supersession CYCLE at {cur} (chain: {seen}) — fix the "
237
+ f"chain (or the probes file anchor) before re-running",
238
+ kind="chain_corrupt")
239
+ seen.append(cur)
240
+ status, note = _get_note(call, cur, max_tier)
241
+ if status == "missing":
242
+ if cur == anchor_id:
243
+ raise ProbeInvalid(
244
+ f"anchor id not in index: {anchor_id} — update the "
245
+ f"probes file (renamed/removed note?)",
246
+ kind="missing_anchor")
247
+ raise ProbeFail(
248
+ f"broken supersession chain: {seen[-2]} -> {cur} (missing)")
249
+ if status == "withheld":
250
+ raise ProbeFail(
251
+ f"note {cur} withheld by egress filter — starvation? "
252
+ f"(re-check the default --max-tier)")
253
+ nxt = _clean_link(note.get("superseded_by"))
254
+ if not nxt:
255
+ return cur, note, len(seen) - 1
256
+ cur = nxt
257
+ raise ProbeInvalid(
258
+ f"supersession chain from {anchor_id} still carries superseded_by "
259
+ f"after {MAX_CHAIN_HOPS} hops — cycle/runaway; fix the chain",
260
+ kind="chain_corrupt")
261
+
262
+
263
+ # ---------------------------------------------------------------------------
264
+ # probe classes
265
+ # ---------------------------------------------------------------------------
266
+ def _claims_of(probe: dict) -> list:
267
+ """Validated ``claim_any`` (lowercased). Must be a LIST of non-empty
268
+ strings — a bare string would iterate per-character and match on a single
269
+ common letter/space, blinding the gate (review finding [263]); an empty
270
+ string is a substring of everything. Absent ⇒ ``[]``."""
271
+ raw = probe.get("claim_any")
272
+ if raw is None:
273
+ return []
274
+ if not isinstance(raw, list) or any(
275
+ not isinstance(c, str) or not c.strip() for c in raw):
276
+ raise ProbeInvalid(
277
+ "claim_any must be a list of non-empty strings")
278
+ return [c.strip().lower() for c in raw]
279
+
280
+
281
+ def _probe_decision_state(call: Call, probe: dict, *, k: int,
282
+ max_tier: Optional[str]) -> str:
283
+ claims = _claims_of(probe)
284
+ anchor = probe.get("anchor_id")
285
+ if not claims and not anchor:
286
+ raise ProbeInvalid(
287
+ "decision_state probe needs anchor_id and/or claim_any")
288
+
289
+ # Resolve the anchor (if any) to its live HEAD. A RETIRED/renamed anchor
290
+ # (kind="missing_anchor") is legitimate vault evolution → drop to the
291
+ # claim path; real chain CORRUPTION (kind="chain_corrupt") must NOT be
292
+ # papered over → re-raise as action_required (review finding [245]).
293
+ head_id = None # set iff the anchor is LIVE (resolved to a HEAD)
294
+ if anchor:
295
+ try:
296
+ head_id, _, _ = _chain_head(call, anchor, max_tier)
297
+ except ProbeInvalid as exc:
298
+ if not claims or getattr(exc, "kind", "config") != "missing_anchor":
299
+ raise
300
+ head_id = None
301
+
302
+ rc, res = _cli_json(
303
+ call, ["dossier", probe["query"], "--json", "-k", str(k)]
304
+ + _tier_args(max_tier))
305
+ if rc != 0:
306
+ raise ProbeTransient(f"dossier returned rc={rc}")
307
+ decisions = res.get("decisions") or []
308
+ withheld = (res.get("egress") or {}).get("withheld", 0)
309
+ starve = (f"; {withheld} note(s) withheld by egress — starvation?"
310
+ if withheld else "")
311
+
312
+ # A LIVE anchor is the PRIMARY, most specific expectation: it MUST be in
313
+ # the decision layer. If it resolved but is NOT surfaced, the specifically
314
+ # anchored decision has been crowded out — that IS the decision-layer
315
+ # crowding regression this probe class exists to catch (review finding
316
+ # [293]); it must NOT be silently accepted just because a SIBLING decision
317
+ # happens to carry the claim substring. Claims are the fallback ONLY when
318
+ # there is no live anchor (retired, or none given).
319
+ if head_id is not None:
320
+ if any(d.get("id") == head_id for d in decisions):
321
+ return f"decision layer surfaced anchor {head_id}"
322
+ # KNOWN AUTHORING TRADEOFF (review finding [317]): "surfaced" means
323
+ # "within the top-k decision layer". A decision that is genuinely
324
+ # current but legitimately ranks BELOW -k for a broad query reads as a
325
+ # (mild) crowding regression here. That is intentional — the anchored
326
+ # decision SHOULD rank high for its own probe query — but it means a
327
+ # decision_state probe must be authored with a query specific enough
328
+ # (and a -k large enough) that its decision reliably sits in the top-k.
329
+ # A too-broad probe is a probes-file tuning issue, not a vault fault.
330
+ raise ProbeFail(
331
+ f"anchor {head_id} resolved but is NOT in the top-{k} decision "
332
+ f"layer ({len(decisions)} hit(s)) — crowded out?{starve}")
333
+
334
+ # No live anchor: match the claim in title+snippet, then full bodies.
335
+ for d in decisions:
336
+ hay = (str(d.get("title", "")) + " " + str(d.get("snippet", ""))).lower()
337
+ if any(c in hay for c in claims):
338
+ return f"decision layer carries the claim (in {d.get('id')})"
339
+ body_fetch_transient = False
340
+ for d in decisions: # fetch ALL surfaced decisions' bodies (the dossier
341
+ # already caps their count) — a fixed [:N] slice below the cap would
342
+ # skip a legitimate match at high -k and false-alarm (finding [301]).
343
+ try:
344
+ status, note = _get_note(call, d.get("id", ""), max_tier)
345
+ except ProbeTransient:
346
+ # A flaky secondary body fetch leaves the match UNDETERMINED — the
347
+ # claim may be hidden behind it. If nothing else matches, report
348
+ # transient (retry), never a false regression (finding [275]).
349
+ body_fetch_transient = True
350
+ continue
351
+ if status == "ok" and any(
352
+ c in str(note.get("body", "")).lower() for c in claims):
353
+ return f"decision layer carries the claim (in {d.get('id')})"
354
+ if body_fetch_transient:
355
+ raise ProbeTransient(
356
+ "a candidate decision's body fetch was transient; the claim "
357
+ "match is undetermined — retry rather than assert a regression")
358
+ raise ProbeFail(
359
+ f"decision layer ({len(decisions)} hit(s)) lacks the expected "
360
+ f"decision (claims={claims}){starve}")
361
+
362
+
363
+ def _probe_currency(call: Call, probe: dict, *, k: int,
364
+ max_tier: Optional[str]) -> str:
365
+ head_id, head, hops = _chain_head(call, probe["anchor_id"], max_tier)
366
+ latest = str(head.get("is_latest_version") or "").lower()
367
+ if latest == "false":
368
+ raise ProbeFail(
369
+ f"version-chain HEAD {head_id} is retired "
370
+ f"(is_latest_version: false) with no successor — stale HEAD")
371
+ via = f" (followed {hops} supersession hop(s))" if hops else ""
372
+ return f"chain HEAD {head_id} is current{via}"
373
+
374
+
375
+ def _probe_freshness(call: Call, probe: dict, *, k: int,
376
+ max_tier: Optional[str]) -> str:
377
+ max_age = _coerce_num(probe["max_age_days"], "max_age_days",
378
+ integer=True, minimum=0)
379
+ # -n 200 (not 20): `brain recent` limits BEFORE the egress gate, so at a
380
+ # narrow --max-tier a small window could be entirely withheld while older
381
+ # reachable notes exist, giving a false "sweep death" (review finding
382
+ # [305]). A wide window makes that vanishingly unlikely; the host default
383
+ # (full vault, no --max-tier) was never affected.
384
+ rc, res = _cli_json(
385
+ call, ["recent", "--json", "-n", "200"] + _tier_args(max_tier))
386
+ if rc != 0:
387
+ raise ProbeTransient(f"recent returned rc={rc}")
388
+ items = res.get("results") or []
389
+ if not items:
390
+ raise ProbeFail("`brain recent` surfaced nothing — empty index or "
391
+ "total egress starvation")
392
+ today_utc = _dt.datetime.now(_dt.timezone.utc).date()
393
+ dated = []
394
+ for it in items:
395
+ try:
396
+ d = _dt.date.fromisoformat(str(it.get("updated", ""))[:10])
397
+ except ValueError:
398
+ continue # 'unknown' etc.
399
+ # Ignore FUTURE-dated notes (review finding [388]): a clock-skew /
400
+ # template-placeholder / typo'd future `updated` would otherwise
401
+ # dominate max(), give a non-positive age, and always pass — masking a
402
+ # genuinely dead sweep/ingest.
403
+ if d <= today_utc:
404
+ dated.append((d, it))
405
+ if not dated:
406
+ raise ProbeFail("no non-future parseable `updated` date among recent "
407
+ "notes (empty index, or all newest notes are "
408
+ "future-dated — check the clock)")
409
+ newest_date, newest = max(dated, key=lambda t: t[0])
410
+ # UTC 'today' vs the UTC-stamped `updated` date (review finding [365]):
411
+ # local date.today() can be a day ahead just after local midnight in a
412
+ # timezone behind UTC, false-failing a note updated 'today' in UTC.
413
+ age = (today_utc - newest_date).days
414
+ if age > max_age:
415
+ raise ProbeFail(
416
+ f"newest indexed note is {age}d old (> {max_age}d) — "
417
+ f"sweep/ingest death? (newest: {newest.get('id')})")
418
+ # No re-fetch via `get`: `recent` already applied the egress gate, so
419
+ # the note surfacing at all IS the reachability evidence (per-subcommand
420
+ # gate parity is the engine's own test suite, not this probe's job).
421
+ return f"newest note {newest.get('id')} is {age}d old (within {max_age}d)"
422
+
423
+
424
+ def _probe_tension(call: Call, probe: dict, *, k: int,
425
+ max_tier: Optional[str]) -> str:
426
+ head_id, _, _ = _chain_head(call, probe["anchor_id"], max_tier)
427
+ rc, res = _cli_json(
428
+ call, ["dossier", probe["query"], "--json", "-k", str(k)]
429
+ + _tier_args(max_tier))
430
+ if rc != 0:
431
+ raise ProbeTransient(f"dossier returned rc={rc}")
432
+ match = next((d for d in res.get("decisions") or []
433
+ if d.get("id") == head_id), None)
434
+ if match is None:
435
+ raise ProbeFail(
436
+ f"decision {head_id} absent from the dossier decision layer — "
437
+ f"cannot evaluate tensions")
438
+ tensions = match.get("tensions") or []
439
+ if not tensions:
440
+ raise ProbeFail(
441
+ f"decision {head_id} carries NO tension flag despite an "
442
+ f"expected newer source — proposal-promotion guard is blind")
443
+ # Normalize the expected id the SAME way the tension ids are normalized
444
+ # (review finding [423]): a wikilink/`.md` form in the probes file
445
+ # ('[[widget-memo]]') must match the cleaned tension id ('widget-memo'),
446
+ # not false-fail a healthy vault that carries the tension.
447
+ want = _clean_link(probe.get("expect_source_id")) if probe.get("expect_source_id") else None
448
+ if want and all(_clean_link(t.get("id")) != want for t in tensions):
449
+ raise ProbeFail(
450
+ f"tension list on {head_id} lacks expected source {want} "
451
+ f"(has: {[t.get('id') for t in tensions]})")
452
+ return (f"decision {head_id} carries {len(tensions)} tension flag(s)")
453
+
454
+
455
+ _PROBE_FNS = {
456
+ "decision_state": _probe_decision_state,
457
+ "currency": _probe_currency,
458
+ "freshness": _probe_freshness,
459
+ "tension": _probe_tension,
460
+ }
461
+
462
+
463
+ # ---------------------------------------------------------------------------
464
+ # scoring + run
465
+ # ---------------------------------------------------------------------------
466
+ def _validate_probe(probe: Any) -> str:
467
+ """-> probe class, or raise ProbeInvalid (deterministic config error)."""
468
+ if not isinstance(probe, dict):
469
+ raise ProbeInvalid("probe entry is not an object")
470
+ cls = probe.get("class")
471
+ if cls not in PROBE_CLASSES:
472
+ raise ProbeInvalid(f"unknown probe class: {cls!r}")
473
+ # presence, not truthiness: `max_age_days: 0` is a VALID value
474
+ missing = [key for key in _REQUIRED_KEYS[cls]
475
+ if key not in probe or probe[key] is None]
476
+ if missing:
477
+ raise ProbeInvalid(f"{cls} probe missing required key(s): {missing}")
478
+ return cls
479
+
480
+
481
+ def run_probes(spec: dict, call: Call, *, threshold: Optional[float] = None,
482
+ k: int = 12, max_tier: Optional[str] = None) -> dict:
483
+ """Execute every probe in ``spec`` -> the result document (see module
484
+ docstring for the disposition/exit-code contract)."""
485
+ probes = spec.get("probes")
486
+ if not isinstance(probes, list) or not probes:
487
+ return {
488
+ "error": "invalid_probes_file",
489
+ "detail": "probes file has no `probes` list",
490
+ "disposition": "action_required", "score": None,
491
+ "probes": [], "exit_code": EXIT_ACTION_REQUIRED,
492
+ }
493
+ # Validate ALL run config HERE — run_probes is the s07 maintain-fold's
494
+ # direct-call seam, so it must guard the injected threshold/k/max_tier as
495
+ # tightly as main()'s CLI seam (findings [441]/[456]/[457]); otherwise a bad
496
+ # injected value blinds/false-alarms the gate. threshold in (0,1] ([400]);
497
+ # k >= 1; max_tier in VALID_TIERS ([487]).
498
+ def _config_error(detail: str) -> dict:
499
+ return {"error": "invalid_config", "detail": detail,
500
+ "disposition": "action_required", "score": None,
501
+ "probes": [], "exit_code": EXIT_ACTION_REQUIRED}
502
+ if max_tier is not None and max_tier not in VALID_TIERS:
503
+ return _config_error(f"max_tier {max_tier!r} is not one of {list(VALID_TIERS)}")
504
+ try:
505
+ threshold = _coerce_num(
506
+ spec.get("threshold", 1.0) if threshold is None else threshold,
507
+ "threshold", minimum=0.0, exclusive_min=True, maximum=1.0)
508
+ k = _coerce_num(k, "k", integer=True, minimum=1)
509
+ except ProbeInvalid as exc:
510
+ return _config_error(str(exc))
511
+
512
+ results = []
513
+ weight_total = 0.0
514
+ weight_passed = 0.0
515
+ counts = {"pass": 0, "fail": 0, "invalid": 0, "transient": 0}
516
+ for probe in probes:
517
+ pid = probe.get("id", "?") if isinstance(probe, dict) else "?"
518
+ weight = 1.0
519
+ try:
520
+ cls = _validate_probe(probe)
521
+ # weight must be > 0 (review finding [522]): a weight-0 probe is
522
+ # dropped from the score, so a weight-0 probe that FAILS would leave
523
+ # a real regression invisible (self-certify healthy). A probe to be
524
+ # de-emphasized should carry a small positive weight, or be removed
525
+ # — weight 0 is a config error, surfaced as invalid, never silent.
526
+ weight = _coerce_num(probe.get("weight", 1.0), "weight",
527
+ minimum=0.0, exclusive_min=True)
528
+ reason = _PROBE_FNS[cls](call, probe, k=k, max_tier=max_tier)
529
+ status = "pass"
530
+ except ProbeFail as exc:
531
+ status, reason = "fail", str(exc)
532
+ except ProbeInvalid as exc:
533
+ status, reason = "invalid", str(exc)
534
+ except ProbeTransient as exc:
535
+ status, reason = "transient", str(exc)
536
+ except Exception as exc: # noqa: BLE001 — review finding [425]/[480]: an
537
+ # unexpected error in ONE probe (a malformed CLI JSON shape raising
538
+ # AttributeError/KeyError, a corrupt note) must be CONTAINED to that
539
+ # probe, never escape and discard every other probe's verdict. It is
540
+ # classified INVALID (action_required), NOT transient: an unexpected
541
+ # shape is a DETERMINISTIC "fix the vault/probe" problem, and a
542
+ # transient classification would retry-loop it forever and silently
543
+ # blind the gate to that failure class ([480]). action_required
544
+ # surfaces it AND the next scheduled run still re-runs it fresh.
545
+ status, reason = "invalid", f"unexpected: {type(exc).__name__}: {exc}"
546
+ cls_name = probe.get("class", "?") if isinstance(probe, dict) else "?"
547
+ results.append({"id": pid, "class": cls_name, "status": status,
548
+ "weight": weight, "reason": reason})
549
+ counts[status] += 1
550
+ if status in ("pass", "fail"): # only VALID probes enter the score
551
+ weight_total += weight
552
+ if status == "pass":
553
+ weight_passed += weight
554
+
555
+ score = (round(weight_passed / weight_total, 4)
556
+ if weight_total > 0 else None)
557
+ # Precedence, refined for finding [531]: a genuine REGRESSION on the
558
+ # resolved probes is the most actionable signal for the maintain fold, so
559
+ # it must NOT be masked by a per-probe `invalid` (an unconfigured/errored
560
+ # sibling probe) — regression outranks a per-probe invalid. (A SPEC-level
561
+ # config error — bad threshold/k/max_tier, no probes — already returned
562
+ # action_required up front, before any probe ran.) Order:
563
+ # regression > per-probe invalid (action_required) > transient >
564
+ # nothing-resolved (action_required) > ok.
565
+ if score is not None and score < threshold - 1e-9:
566
+ disposition, exit_code = "regression", EXIT_REGRESSION
567
+ elif counts["invalid"]:
568
+ disposition, exit_code = "action_required", EXIT_ACTION_REQUIRED
569
+ elif counts["transient"]:
570
+ disposition, exit_code = "transient", EXIT_TRANSIENT
571
+ elif score is None: # nothing resolved to a pass/fail — nothing to score
572
+ disposition, exit_code = "action_required", EXIT_ACTION_REQUIRED
573
+ else:
574
+ disposition, exit_code = "ok", EXIT_OK
575
+
576
+ return {
577
+ "score": score,
578
+ "threshold": threshold,
579
+ "disposition": disposition,
580
+ "exit_code": exit_code,
581
+ "counts": counts,
582
+ "probes": results,
583
+ "captured": _dt.datetime.now(_dt.timezone.utc)
584
+ .strftime("%Y-%m-%dT%H:%M:%SZ"),
585
+ "baseline": spec.get("baseline"),
586
+ }
587
+
588
+
589
+ def main(argv: Optional[list] = None) -> int:
590
+ parser = argparse.ArgumentParser(
591
+ prog="brain-golden-probe",
592
+ description="Golden-set retrieval regression probes over the brain "
593
+ "CLI (see module docstring for the exit-code contract).")
594
+ parser.add_argument("probes_file", help="path to the per-vault probes "
595
+ "JSON (lives with the vault, NOT in this repo)")
596
+ parser.add_argument("--vault", default=None,
597
+ help="vault dir (passed to `brain --vault`)")
598
+ parser.add_argument("--brain-cmd", default="brain",
599
+ help="brain CLI command (default: `brain` from PATH; "
600
+ "e.g. `python -m brain.cli`)")
601
+ # Numeric flags are parsed as STRINGS then guarded-coerced below, NOT via
602
+ # argparse `type=float/int` — a bad value there raises argparse's own usage
603
+ # error to stderr with NO JSON on stdout, breaking the "JSON always carries
604
+ # the disposition" contract for a CLI config typo (review finding [542]).
605
+ parser.add_argument("--threshold", default=None,
606
+ help="override the probes-file threshold (0, 1]")
607
+ parser.add_argument("--max-tier", default=None,
608
+ help="pass an explicit egress cap through to every "
609
+ "read (default: none — measure the real default)")
610
+ parser.add_argument("-k", default="12", help="dossier depth")
611
+ parser.add_argument("--timeout", default="120", help="per-CLI-call timeout, seconds")
612
+ args = parser.parse_args(argv)
613
+
614
+ # Validate ALL CLI config to a JSON action_required (NOT argparse's own
615
+ # exit-2 usage error, which prints no JSON). An unknown --max-tier would
616
+ # make every `brain` call fail argparse and be misread as a retryable
617
+ # transient (finding [487]); the numeric flags are range/type-checked here
618
+ # via the same guarded coercion the probes file uses (findings [400]/[542]).
619
+ def _emit_cli_error(detail: str) -> int:
620
+ print(json.dumps({
621
+ "error": "invalid_config", "detail": detail,
622
+ "disposition": "action_required", "score": None,
623
+ "exit_code": EXIT_ACTION_REQUIRED, "probes_file": args.probes_file,
624
+ }, indent=2))
625
+ return EXIT_ACTION_REQUIRED
626
+
627
+ if args.max_tier is not None and args.max_tier not in VALID_TIERS:
628
+ return _emit_cli_error(
629
+ f"--max-tier {args.max_tier!r} is not one of {list(VALID_TIERS)}")
630
+ try:
631
+ threshold = (None if args.threshold is None else _coerce_num(
632
+ args.threshold, "--threshold", minimum=0.0, exclusive_min=True, maximum=1.0))
633
+ k = _coerce_num(args.k, "-k", integer=True, minimum=1)
634
+ timeout = _coerce_num(args.timeout, "--timeout", minimum=0.0, exclusive_min=True)
635
+ except ProbeInvalid as exc:
636
+ return _emit_cli_error(str(exc))
637
+
638
+ try:
639
+ with open(args.probes_file, "r", encoding="utf-8") as fh:
640
+ spec = json.load(fh)
641
+ if not isinstance(spec, dict):
642
+ raise ValueError("top level is not an object")
643
+ except Exception as exc: # missing file / bad JSON: deterministic config
644
+ print(json.dumps({
645
+ "error": "invalid_probes_file",
646
+ "detail": f"{args.probes_file}: {exc}",
647
+ "disposition": "action_required", "score": None,
648
+ "exit_code": EXIT_ACTION_REQUIRED,
649
+ }, indent=2))
650
+ return EXIT_ACTION_REQUIRED
651
+
652
+ call = make_subprocess_call(args.brain_cmd, args.vault, timeout)
653
+ try:
654
+ doc = run_probes(spec, call, threshold=threshold, k=k,
655
+ max_tier=args.max_tier)
656
+ except Exception as exc:
657
+ # Contract: NO path exits with a bare traceback/exit 1. Anything
658
+ # that escapes run_probes still emits a JSON disposition — a
659
+ # deterministic config error is action_required (2), everything
660
+ # unexpected is transient (3): the run never got a deterministic
661
+ # answer, so bounded backoff is the honest disposition.
662
+ if isinstance(exc, ProbeInvalid):
663
+ disposition, code = "action_required", EXIT_ACTION_REQUIRED
664
+ else:
665
+ disposition, code = "transient", EXIT_TRANSIENT
666
+ doc = {
667
+ "error": "unexpected_error",
668
+ "detail": f"{type(exc).__name__}: {exc}",
669
+ "disposition": disposition, "score": None,
670
+ "exit_code": code,
671
+ }
672
+ doc["probes_file"] = args.probes_file
673
+ print(json.dumps(doc, indent=2))
674
+ return int(doc["exit_code"])
675
+
676
+
677
+ if __name__ == "__main__": # python -m brain.golden_probe
678
+ sys.exit(main())