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.
- brain/__init__.py +39 -0
- brain/__main__.py +14 -0
- brain/_assets/AGENTS.md +564 -0
- brain/_assets/overlay/template/brand/brand-guide.md +24 -0
- brain/_assets/overlay/template/keywords/glossary.md +15 -0
- brain/_assets/overlay/template/people/roster.md +14 -0
- brain/_assets/overlay/template/voice/voice-profile.md +34 -0
- brain/_assets/routines/manifest.json +257 -0
- brain/_assets/scripts/brain-brief-mac.plist +59 -0
- brain/_assets/scripts/brain-brief.sh +74 -0
- brain/_assets/scripts/brain-synthesis-mac.plist +48 -0
- brain/_assets/scripts/brain-synthesis.sh +214 -0
- brain/_assets/scripts/install-brief-mac.sh +152 -0
- brain/_assets/scripts/install-brief-windows.ps1 +97 -0
- brain/_assets/scripts/register_tasks.py +386 -0
- brain/_assets/templates/company.md +21 -0
- brain/_assets/templates/concept.md +27 -0
- brain/_assets/templates/daily.md +20 -0
- brain/_assets/templates/decision.md +42 -0
- brain/_assets/templates/meeting.md +33 -0
- brain/_assets/templates/person.md +20 -0
- brain/_assets/templates/project.md +23 -0
- brain/_assets/templates/state-moc.md +40 -0
- brain/_version.py +12 -0
- brain/anchor.py +121 -0
- brain/audit.py +422 -0
- brain/backup.py +210 -0
- brain/brief.py +417 -0
- brain/capture.py +117 -0
- brain/chunk.py +249 -0
- brain/classification.py +134 -0
- brain/cli.py +1906 -0
- brain/config.py +368 -0
- brain/connect.py +362 -0
- brain/context.py +108 -0
- brain/core.py +3018 -0
- brain/doctor.py +1161 -0
- brain/egress.py +148 -0
- brain/embed.py +857 -0
- brain/encryption.py +217 -0
- brain/frontmatter.py +102 -0
- brain/golden_probe.py +678 -0
- brain/graph.py +369 -0
- brain/graphify.py +352 -0
- brain/index.py +1576 -0
- brain/ingest/__init__.py +19 -0
- brain/ingest/handlers/__init__.py +43 -0
- brain/ingest/handlers/base.py +95 -0
- brain/ingest/handlers/docx.py +78 -0
- brain/ingest/handlers/email.py +228 -0
- brain/ingest/handlers/html.py +142 -0
- brain/ingest/handlers/image.py +91 -0
- brain/ingest/handlers/pdf.py +99 -0
- brain/ingest/handlers/pptx.py +69 -0
- brain/ingest/handlers/tables.py +41 -0
- brain/ingest/handlers/text.py +43 -0
- brain/ingest/handlers/xlsx.py +100 -0
- brain/ingest/handlers/zip.py +163 -0
- brain/ingest/pipeline.py +839 -0
- brain/ingest/transcript.py +158 -0
- brain/init.py +870 -0
- brain/maintenance.py +2266 -0
- brain/mcp_adapter.py +217 -0
- brain/multihop.py +232 -0
- brain/notes.py +195 -0
- brain/overlay.py +183 -0
- brain/projection.py +79 -0
- brain/rerank.py +425 -0
- brain/snapshot.py +231 -0
- brain/update.py +743 -0
- brain/vectors.py +225 -0
- brainiac_cli-0.16.0.dist-info/METADATA +306 -0
- brainiac_cli-0.16.0.dist-info/RECORD +77 -0
- brainiac_cli-0.16.0.dist-info/WHEEL +5 -0
- brainiac_cli-0.16.0.dist-info/entry_points.txt +4 -0
- brainiac_cli-0.16.0.dist-info/licenses/LICENSE +202 -0
- 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())
|