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
@@ -0,0 +1,839 @@
1
+ """ING-01 orchestrator: dispatcher + quarantine + immutable archival + audited
2
+ promotion. Pure(ish) — the only I/O is the drop zone / vault filesystem and
3
+ (non-dry-run) ``BrainCore.write_note``. Never called with role=vm: BrainCore
4
+ refuses ``ingest_dropzone`` via ``_require_host`` BEFORE this module is even
5
+ imported, so a VM leg has zero side effects here (S06 hard guarantee, mirrors
6
+ ``write_note``/``drain_drafts``).
7
+
8
+ HARDENED (codex + grill):
9
+ - concurrency: each drop-zone file is CLAIMED via an atomic ``os.rename``
10
+ into ``inbox/_processing/`` before extraction, so a manual ``brain
11
+ ingest`` and the scheduled ``maintain`` drain can never double-process
12
+ the same file (the loser's rename raises ``OSError`` and it's skipped).
13
+ - immutability-safe writes: the archived original and the ``raw/`` source
14
+ are CREATE-EXCLUSIVE — same sha256 at the target = idempotent no-op,
15
+ different sha256 = quarantine as a collision (never silently overwritten).
16
+ - duplicate-content idempotency: a manifest keyed by the ORIGINAL file's
17
+ sha256 makes re-ingesting identical bytes a no-op (moved to
18
+ ``inbox/_duplicate/`` with a report line, never re-signed).
19
+ - quality gate: nothing reaches ``write_note`` unless the handler's
20
+ extraction passed its density/encryption/size gates (see
21
+ ``handlers.base.density_gate`` + each handler's own guards).
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import datetime as _dt
26
+ import errno
27
+ import hashlib
28
+ import json
29
+ import os
30
+ import re
31
+ import shutil
32
+ from pathlib import Path
33
+ from typing import Any
34
+
35
+ from . import handlers as H
36
+ from ..audit import AuditError
37
+ from ..notes import safe_slug
38
+
39
+ INBOX_DIRNAME = "inbox"
40
+ PROCESSING_DIRNAME = "_processing"
41
+ QUARANTINE_DIRNAME = "_quarantine"
42
+ DUPLICATE_DIRNAME = "_duplicate"
43
+ MANIFEST_RELPATH = ("ingest-manifest.json",) # under .brain/
44
+ FAILURES_RELPATH = ("ingest-failures.json",) # under .brain/ — per-file retry counter (C2)
45
+
46
+ # C2: a file that deterministically fails processing must not be retried
47
+ # forever (that would starve every later-alphabetical candidate's chance to
48
+ # ever be reported as "quarantined" and clutter the inbox root indefinitely
49
+ # with one file bouncing back and forth). After this many failed attempts
50
+ # (across separate run_ingest calls) it is quarantined instead of retried.
51
+ MAX_INGEST_FAILURES = 3
52
+
53
+ # B1: infra-wide outages (no signing key resolved; disk full/read-only fs)
54
+ # are NOT a per-file defect — they hit every remaining candidate in the batch
55
+ # identically. Counting one against the per-file poison counter (and
56
+ # eventually quarantining a perfectly good file) mistakes an outage for a
57
+ # poison file; see _is_systemic_error below.
58
+ _SYSTEMIC_OSERRNOS = frozenset({errno.ENOSPC, errno.EDQUOT, errno.EROFS})
59
+
60
+
61
+ def _is_systemic_error(exc: BaseException) -> bool:
62
+ """True for a batch-wide outage (signing-key unavailable, or an OSError
63
+ whose errno says disk-full/quota/read-only-fs) as opposed to a per-file
64
+ extraction/content defect. ``AuditError`` covers ``KeyUnavailable`` and
65
+ any sibling audit/signing failure (e.g. the 'cryptography' package
66
+ missing) — anything raised by ``core.write_note``'s signing step."""
67
+ if isinstance(exc, AuditError):
68
+ return True
69
+ if isinstance(exc, OSError) and exc.errno in _SYSTEMIC_OSERRNOS:
70
+ return True
71
+ return False
72
+
73
+
74
+ # HARDENED (code-review rework): a claim (rename into _processing/) followed
75
+ # by a crash before the file is unlinked/moved out would strand it forever —
76
+ # run_ingest only scans the inbox ROOT. Files older than this are swept back
77
+ # to the inbox root at the start of the NEXT run_ingest call (crash backstop).
78
+ # Files younger than this are left alone: they may belong to another process
79
+ # that is CURRENTLY (and legitimately) extracting them — sweeping those would
80
+ # break the atomic-claim guarantee. The per-file try/except below (moves the
81
+ # claim back to inbox on any in-process exception) is the primary mechanism;
82
+ # this sweep only catches a hard crash (killed process, power loss) that never
83
+ # got to run its except-clause.
84
+ STALE_PROCESSING_SECONDS = 15 * 60
85
+
86
+ # Cap checked via stat() BEFORE any read_bytes() of the claimed file, so a
87
+ # pathological multi-GB drop never loads fully into memory before rejection.
88
+ # Matches the largest per-handler cap (pdf.py); handlers still enforce their
89
+ # own (possibly smaller) cap on top of this.
90
+ MAX_INGEST_BYTES = 200 * 1024 * 1024
91
+
92
+ # S06 (ING-03): a handler (zip, eml) can return ``metadata["nested"]`` —
93
+ # member/attachment bytes that re-enter the SAME dispatcher as their own
94
+ # ingest candidates. Bounded on TWO axes so a nested-archive-of-archives
95
+ # ("zip bomb via nesting") can't blow past each handler's own per-level caps:
96
+ # - MAX_NESTED_DEPTH: how many levels of re-entry (zip-in-zip, eml
97
+ # attachment that is itself a zip, ...) before giving up.
98
+ # - a shared per-top-level-candidate BUDGET (bytes + item count) threaded
99
+ # through the WHOLE recursion tree, not just checked per level — each
100
+ # handler already caps its own single-level ``nested`` list, but nesting
101
+ # N archives each at their own cap multiplies past any single-level limit.
102
+ MAX_NESTED_DEPTH = 3
103
+ MAX_TOTAL_NESTED_BYTES = 500 * 1024 * 1024
104
+ MAX_TOTAL_NESTED_ITEMS = 1000
105
+
106
+
107
+ def inbox_dir(vault: Path) -> Path:
108
+ return vault / INBOX_DIRNAME
109
+
110
+
111
+ def _manifest_path(vault: Path) -> Path:
112
+ from .. import config
113
+
114
+ return config.brain_runtime_dir(vault) / MANIFEST_RELPATH[0]
115
+
116
+
117
+ def _load_manifest(vault: Path) -> dict[str, str]:
118
+ path = _manifest_path(vault)
119
+ if not path.exists():
120
+ return {}
121
+ try:
122
+ return json.loads(path.read_text(encoding="utf-8"))
123
+ except (OSError, ValueError):
124
+ return {}
125
+
126
+
127
+ def _save_manifest(vault: Path, manifest: dict[str, str]) -> None:
128
+ path = _manifest_path(vault)
129
+ path.parent.mkdir(parents=True, exist_ok=True)
130
+ tmp = path.with_suffix(".tmp")
131
+ tmp.write_text(json.dumps(manifest, indent=2, sort_keys=True), encoding="utf-8")
132
+ os.replace(tmp, path) # atomic on same filesystem
133
+
134
+
135
+ def _failures_path(vault: Path) -> Path:
136
+ from .. import config
137
+
138
+ return config.brain_runtime_dir(vault) / FAILURES_RELPATH[0]
139
+
140
+
141
+ def _load_failures(vault: Path) -> dict[str, int]:
142
+ path = _failures_path(vault)
143
+ if not path.exists():
144
+ return {}
145
+ try:
146
+ return json.loads(path.read_text(encoding="utf-8"))
147
+ except (OSError, ValueError):
148
+ return {}
149
+
150
+
151
+ def _save_failures(vault: Path, failures: dict[str, int]) -> None:
152
+ path = _failures_path(vault)
153
+ path.parent.mkdir(parents=True, exist_ok=True)
154
+ tmp = path.with_suffix(".tmp")
155
+ tmp.write_text(json.dumps(failures, indent=2, sort_keys=True), encoding="utf-8")
156
+ os.replace(tmp, path)
157
+
158
+
159
+ def _sha256_bytes(data: bytes) -> str:
160
+ return hashlib.sha256(data).hexdigest()
161
+
162
+
163
+ def _content_key(path: Path) -> str:
164
+ """E5: the stable key for the per-file retry/failure counter. Keyed on
165
+ the CONTENT hash rather than the filename — ``_claim`` disambiguates a
166
+ same-named collision in ``_processing/`` (e.g. ``poison.1.pdf``), and a
167
+ failed attempt is re-dropped into the inbox under THAT (possibly renamed)
168
+ name. A name-keyed counter loses its accumulated count the moment the
169
+ name changes; the sha256 of the bytes is stable across every rename."""
170
+ return _sha256_bytes(path.read_bytes())
171
+
172
+
173
+ _SLUG_SANITIZE = re.compile(r"[^A-Za-z0-9]+")
174
+
175
+
176
+ def _slugify_stem(stem: str) -> str:
177
+ cleaned = _SLUG_SANITIZE.sub("-", stem).strip("-").lower()
178
+ return cleaned or "file"
179
+
180
+
181
+ # C7/B2: chars that either break the hand-rolled double-quoted YAML wrapping
182
+ # in _build_frontmatter (`"`) or are unsafe/awkward across filesystems (`:`,
183
+ # `\`) — a hostile/careless original filename (e.g. `report:"final".pdf`)
184
+ # must never be able to bake malformed YAML into a signed, immutable raw/
185
+ # note. Also strip ALL control characters (0x00-0x1F incl. newline/tab, and
186
+ # DEL 0x7F) — an embedded literal newline flows into `origin:` as a bare
187
+ # unescaped scalar and corrupts the YAML just as surely as an unescaped quote.
188
+ _ARCHIVE_NAME_SANITIZE = re.compile(r'[\x00-\x1f\x7f:"\\]')
189
+
190
+
191
+ def _sanitize_archive_name(name: str) -> str:
192
+ """Sanitize the archived-original's filename component (used for BOTH the
193
+ on-disk archive path and the `origin:` frontmatter value it flows into)
194
+ so it can never carry a character that would corrupt the signed
195
+ frontmatter or misbehave on a legacy filesystem."""
196
+ cleaned = _ARCHIVE_NAME_SANITIZE.sub("_", name)
197
+ return cleaned or "file"
198
+
199
+
200
+ def _move(src: Path, dest: Path) -> None:
201
+ dest.parent.mkdir(parents=True, exist_ok=True)
202
+ try:
203
+ os.rename(src, dest)
204
+ except OSError:
205
+ shutil.move(str(src), str(dest)) # cross-device fallback
206
+
207
+
208
+ def _claim(path: Path, processing_dir: Path) -> Path | None:
209
+ """Atomically claim ``path`` for this process. Returns the claimed path,
210
+ or ``None`` if another process/thread claimed it first (or it vanished)."""
211
+ processing_dir.mkdir(parents=True, exist_ok=True)
212
+ dest = processing_dir / path.name
213
+ i = 0
214
+ while dest.exists():
215
+ i += 1
216
+ dest = processing_dir / f"{path.stem}.{i}{path.suffix}"
217
+ try:
218
+ os.rename(path, dest)
219
+ except OSError:
220
+ return None
221
+ # C1: os.rename PRESERVES the source's mtime, so a file that was old
222
+ # (e.g. downloaded/copied, keeping its original timestamp) looks
223
+ # instantly "stale" to _sweep_stale_processing the moment it's claimed —
224
+ # a concurrent drain would then sweep this LIVE claim back to the inbox.
225
+ # Touch to now so the processing-dir entry's mtime measures CLAIM time,
226
+ # not the source file's original timestamp.
227
+ try:
228
+ os.utime(dest, None)
229
+ except OSError:
230
+ pass # non-fatal: worst case the staleness backstop is slightly off
231
+ return dest
232
+
233
+
234
+ def _unique_dest(target_dir: Path, name: str) -> Path:
235
+ stem, suffix = Path(name).stem, Path(name).suffix
236
+ dest = target_dir / name
237
+ i = 0
238
+ while dest.exists():
239
+ i += 1
240
+ dest = target_dir / f"{stem}.{i}{suffix}"
241
+ return dest
242
+
243
+
244
+ def _sweep_stale_processing(
245
+ processing_dir: Path, inbox: Path, *,
246
+ vault: Path, quarantine_dir: Path, failures: dict[str, int],
247
+ ) -> None:
248
+ """Crash backstop: rescue files stranded in ``_processing/`` by a process
249
+ that claimed them and then died before finishing (see
250
+ ``STALE_PROCESSING_SECONDS``). Only sweeps files older than the staleness
251
+ threshold, so a concurrently-running claim by another live process is
252
+ never touched.
253
+
254
+ E3: a process death (OOM/segfault) mid-extraction is indistinguishable
255
+ from a deterministically-poison file after enough attempts — without this,
256
+ a file that reliably kills the process is swept back and reclaimed FOREVER
257
+ (never counted, never quarantined), and every nightly ``maintain`` dies
258
+ before ``index.sync``/publish ever runs. Count each sweep as a failed
259
+ attempt against the SAME persisted counter the in-process per-file
260
+ handler uses (keyed on content sha256, see E5/``_content_key``), so a
261
+ crash-looping file quarantines after ``MAX_INGEST_FAILURES`` just like an
262
+ in-process exception would."""
263
+ if not processing_dir.is_dir():
264
+ return
265
+ now = _dt.datetime.now().timestamp()
266
+ touched = False
267
+ for stuck in list(processing_dir.iterdir()):
268
+ if not stuck.is_file():
269
+ continue
270
+ try:
271
+ age = now - stuck.stat().st_mtime
272
+ except OSError:
273
+ continue
274
+ if age < STALE_PROCESSING_SECONDS:
275
+ continue
276
+ try:
277
+ key = _content_key(stuck)
278
+ except OSError:
279
+ key = stuck.name # unreadable; fall back to name-keying
280
+ count = failures.get(key, 0) + 1
281
+ failures[key] = count
282
+ touched = True
283
+ if count >= MAX_INGEST_FAILURES:
284
+ reason = "repeated_ingest_failure"
285
+ _quarantine(
286
+ stuck, quarantine_dir, reason,
287
+ [f"swept from stale {PROCESSING_DIRNAME}/ {count} time(s) — "
288
+ "process likely died mid-extraction (crash-death is "
289
+ "indistinguishable from poison after N attempts); giving up"],
290
+ )
291
+ failures.pop(key, None)
292
+ else:
293
+ _move(stuck, _unique_dest(inbox, stuck.name))
294
+ if touched:
295
+ _save_failures(vault, failures)
296
+
297
+
298
+ def _create_exclusive_or_collision(dest: Path, data: bytes, known_sha: str | None = None) -> str:
299
+ """Write ``data`` to ``dest`` create-exclusive. Returns "written",
300
+ "idempotent" (dest already holds identical bytes), or "collision" (dest
301
+ holds DIFFERENT bytes — never overwritten).
302
+
303
+ ``known_sha`` lets a caller that already hashed ``data`` (e.g. for the
304
+ manifest content-key) skip re-hashing a potentially large buffer on the
305
+ collision-check path."""
306
+ dest.parent.mkdir(parents=True, exist_ok=True)
307
+ try:
308
+ fd = os.open(dest, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
309
+ except FileExistsError:
310
+ existing = dest.read_bytes()
311
+ if existing == data:
312
+ return "idempotent"
313
+ data_sha = known_sha if known_sha is not None else _sha256_bytes(data)
314
+ return "idempotent" if _sha256_bytes(existing) == data_sha else "collision"
315
+ try:
316
+ with os.fdopen(fd, "wb") as f:
317
+ f.write(data)
318
+ except Exception:
319
+ dest.unlink(missing_ok=True)
320
+ raise
321
+ return "written"
322
+
323
+
324
+ def _yaml_dq_escape(s: str) -> str:
325
+ """Escape a string for embedding inside a YAML double-quoted scalar."""
326
+ return s.replace("\\", "\\\\").replace('"', '\\"')
327
+
328
+
329
+ def _build_frontmatter(meta: dict[str, Any], body: str) -> str:
330
+ lines = ["---"]
331
+ for k, v in meta.items():
332
+ if isinstance(v, bool):
333
+ lines.append(f"{k}: {'true' if v else 'false'}")
334
+ elif isinstance(v, str):
335
+ # S06 HARDENED (root-cause, not per-caller): strip raw control
336
+ # chars (embedded newline/tab/...) from EVERY string value here,
337
+ # in the ONE function every ingest caller routes through —
338
+ # previously a value with a control char but none of `:#"\\`
339
+ # skipped quoting entirely (see the C7 fix below) and could
340
+ # inject a bogus line into signed, immutable frontmatter. This
341
+ # covers both the drop-zone handlers' filenames AND
342
+ # transcript.py's caller-supplied `origin`/`language` without
343
+ # requiring each new caller to remember to pre-sanitize.
344
+ v = H.strip_control_chars(v)
345
+ if ":" in v or "#" in v or '"' in v or "\\" in v:
346
+ # C7: previously wrapped in double quotes WITHOUT escaping an
347
+ # embedded `"` — a value carrying one (e.g. origin embedding a
348
+ # hostile original filename) baked malformed YAML into a signed,
349
+ # immutable raw/ note that could never be fixed. Escape properly.
350
+ lines.append(f'{k}: "{_yaml_dq_escape(v)}"')
351
+ else:
352
+ lines.append(f"{k}: {v}")
353
+ else:
354
+ lines.append(f"{k}: {v}")
355
+ lines.append("---")
356
+ return "\n".join(lines) + "\n\n" + body.rstrip() + "\n"
357
+
358
+
359
+ def capability_report() -> dict[str, dict]:
360
+ return H.capability_report()
361
+
362
+
363
+ def run_ingest(core: Any, *, dry_run: bool = False) -> dict[str, Any]:
364
+ """Drain the drop zone. ``core`` is a HOST-role ``BrainCore`` — the caller
365
+ (``BrainCore.ingest_dropzone``) already enforced ``_require_host`` before
366
+ calling in, so this function assumes host privileges are available."""
367
+ vault = core.vault
368
+ inbox = inbox_dir(vault)
369
+ report: dict[str, Any] = {
370
+ "processed": [], "quarantined": [], "duplicates": [], "skipped": [],
371
+ "dry_run": dry_run,
372
+ }
373
+ if not inbox.is_dir():
374
+ report["reason"] = "no-inbox-dir"
375
+ return report
376
+
377
+ processing_dir = inbox / PROCESSING_DIRNAME
378
+ quarantine_dir = inbox / QUARANTINE_DIRNAME
379
+ duplicate_dir = inbox / DUPLICATE_DIRNAME
380
+ reserved = {PROCESSING_DIRNAME, QUARANTINE_DIRNAME, DUPLICATE_DIRNAME}
381
+
382
+ # E6: loaded once, up-front, and threaded through the sweep below too, so
383
+ # a single failures.json read/write pair covers both the crash-backstop
384
+ # sweep and the main per-file loop in this run.
385
+ failures = _load_failures(vault) if not dry_run else {}
386
+
387
+ # Crash backstop: rescue anything a PRIOR run left stranded in
388
+ # _processing/ (claimed, then the process died before finishing) so it
389
+ # re-enters this run's candidate scan instead of being lost forever.
390
+ if not dry_run:
391
+ _sweep_stale_processing(
392
+ processing_dir, inbox, vault=vault,
393
+ quarantine_dir=quarantine_dir, failures=failures,
394
+ )
395
+
396
+ candidates = sorted(
397
+ p for p in inbox.iterdir()
398
+ if p.is_file() and not p.name.startswith(".") and p.parent.name not in reserved
399
+ )
400
+ # Also skip anything whose top-level parent is a reserved dir (iterdir only
401
+ # lists the inbox root, so this is defense-in-depth, not load-bearing).
402
+ candidates = [p for p in candidates if p.name not in reserved]
403
+
404
+ if dry_run:
405
+ for path in candidates:
406
+ handler = H.handler_for(path)
407
+ if handler is None:
408
+ report["skipped"].append({"file": path.name, "reason": "no_handler_for_extension"})
409
+ continue
410
+ if not handler.available():
411
+ report["skipped"].append(
412
+ {"file": path.name, "reason": f"missing_dependency:{handler.dependency_name}"}
413
+ )
414
+ continue
415
+ # C6: the stat()-based size gate was only applied on the
416
+ # non-dry-run path — dry-run called handler.extract() directly,
417
+ # and a handler like TextHandler does an unconditional
418
+ # read_bytes(), so a pathological multi-GB drop got fully loaded
419
+ # into memory even for a mere preview. Gate BEFORE extract here too.
420
+ try:
421
+ size = path.stat().st_size
422
+ except OSError:
423
+ size = 0
424
+ if size > MAX_INGEST_BYTES:
425
+ report["quarantined"].append({"file": path.name, "reason": "file_too_large"})
426
+ continue
427
+ result = handler.extract(path)
428
+ if result.ok:
429
+ report["processed"].append({"file": path.name, "would_write": True})
430
+ else:
431
+ report["quarantined"].append({"file": path.name, "reason": result.quarantine_reason})
432
+ return report
433
+
434
+ manifest = _load_manifest(vault)
435
+ today = _dt.date.today().isoformat()
436
+
437
+ for path in candidates:
438
+ claimed = _claim(path, processing_dir)
439
+ if claimed is None:
440
+ report["skipped"].append({"file": path.name, "reason": "claimed_elsewhere"})
441
+ continue
442
+
443
+ # Size gate BEFORE any read_bytes() of the claimed file, so a
444
+ # pathological multi-GB drop never loads fully into memory before
445
+ # rejection — checked here (ahead of the content-key read below) so
446
+ # the gate still runs first even though _content_key now reads the
447
+ # file too.
448
+ try:
449
+ size = claimed.stat().st_size
450
+ except OSError:
451
+ size = 0
452
+ if size > MAX_INGEST_BYTES:
453
+ reason = "file_too_large"
454
+ _quarantine(claimed, quarantine_dir, reason,
455
+ [f"{size} bytes exceeds ingest cap {MAX_INGEST_BYTES}"])
456
+ report["quarantined"].append({"file": claimed.name, "reason": reason})
457
+ continue
458
+
459
+ # E5: key the per-file retry counter on the CONTENT hash, not the
460
+ # (possibly claim-collision-renamed / retry-renamed) filename.
461
+ original_bytes = claimed.read_bytes()
462
+ original_sha = _sha256_bytes(original_bytes)
463
+
464
+ try:
465
+ _process_claimed(
466
+ claimed, path.name, original_bytes=original_bytes,
467
+ original_sha=original_sha, core=core, manifest=manifest,
468
+ vault=vault, today=today, quarantine_dir=quarantine_dir,
469
+ duplicate_dir=duplicate_dir, processing_dir=processing_dir,
470
+ report=report,
471
+ )
472
+ except Exception as exc:
473
+ if _is_systemic_error(exc):
474
+ # B1: KeyUnavailable / disk-full / read-only-fs is an OUTAGE,
475
+ # not a per-file defect — it will hit every remaining
476
+ # candidate in this batch identically. Leave the file for the
477
+ # next drain untouched (no counter bump, no quarantine risk)
478
+ # and stop this run's batch rather than burn through every
479
+ # remaining candidate against the same wall, WITHOUT raising
480
+ # (a systemic outage must never abort the surrounding sync).
481
+ if claimed.exists():
482
+ _move(claimed, _unique_dest(inbox, claimed.name))
483
+ report["skipped"].append({
484
+ "file": path.name,
485
+ "reason": f"systemic_error:{type(exc).__name__}: {exc}",
486
+ })
487
+ break
488
+ # C2: a per-file exception used to be moved back to the inbox
489
+ # AND re-raised, aborting the whole run_ingest call — one
490
+ # deterministically-failing ("poison") file meant `brain sync`
491
+ # exited nonzero on EVERY invocation, so the index never
492
+ # reconciled and the snapshot never republished, forever. Now:
493
+ # retry a few times (moved back to the inbox root, NOT raised, so
494
+ # later candidates in THIS run still get processed), then
495
+ # quarantine it once it's clearly not transient.
496
+ count = failures.get(original_sha, 0) + 1
497
+ failures[original_sha] = count
498
+ _save_failures(vault, failures)
499
+ if not claimed.exists():
500
+ continue
501
+ if count >= MAX_INGEST_FAILURES:
502
+ reason = "repeated_ingest_failure"
503
+ _quarantine(
504
+ claimed, quarantine_dir, reason,
505
+ [f"{type(exc).__name__}: {exc}", f"failed {count} time(s), giving up"],
506
+ )
507
+ report["quarantined"].append({"file": path.name, "reason": reason})
508
+ failures.pop(original_sha, None)
509
+ _save_failures(vault, failures)
510
+ else:
511
+ _move(claimed, _unique_dest(inbox, claimed.name))
512
+ report["skipped"].append({
513
+ "file": path.name,
514
+ "reason": f"processing_error:{type(exc).__name__} (attempt {count}/{MAX_INGEST_FAILURES})",
515
+ })
516
+ continue
517
+ else:
518
+ # E6: clear the counter entry on success so a since-fixed/
519
+ # renamed file doesn't carry a stale count into a later,
520
+ # unrelated drop that happens to hash the same (vanishingly
521
+ # unlikely, but the entry should never outlive its file anyway).
522
+ if original_sha in failures:
523
+ failures.pop(original_sha, None)
524
+ _save_failures(vault, failures)
525
+
526
+ return report
527
+
528
+
529
+ def _existing_note_classification(vault: Path, existing_id: str) -> str | None:
530
+ """E4: the classification of an already-ingested ``raw/<id>.md`` note, so
531
+ a duplicate-report entry can be routed through the same egress gate as
532
+ ``processed`` (a duplicate's ``existing_id`` is a real note id — it must
533
+ not leak an above-max-tier note's identity just because the CONTENT was a
534
+ dedup hit rather than a fresh promotion)."""
535
+ note_path = vault / "raw" / f"{existing_id}.md"
536
+ if not note_path.is_file():
537
+ return None
538
+ from .. import frontmatter as fm
539
+
540
+ try:
541
+ meta, _ = fm.parse_text(note_path.read_text(encoding="utf-8"))
542
+ except OSError:
543
+ return None
544
+ val = meta.get("classification")
545
+ return str(val) if val else None
546
+
547
+
548
+ def _process_claimed(
549
+ claimed: Path, orig_name: str, *, original_bytes: bytes, original_sha: str,
550
+ core: Any, manifest: dict[str, str],
551
+ vault: Path, today: str, quarantine_dir: Path, duplicate_dir: Path,
552
+ processing_dir: Path, report: dict[str, Any],
553
+ depth: int = 0, budget: dict[str, int] | None = None, parent: str | None = None,
554
+ ) -> None:
555
+ """Process one already-claimed file (in ``inbox/_processing/``) to
556
+ completion: quarantine, duplicate, or promote. On success the claimed
557
+ copy is always consumed (moved or unlinked). On any exception the caller
558
+ (``run_ingest`` or, for a nested item, ``_process_nested``) moves the
559
+ (still-existing) claim back to the inbox root / quarantines it so the
560
+ next drain retries it — see the HARDENED note above.
561
+
562
+ ``original_bytes``/``original_sha`` are precomputed by the caller (which
563
+ needs the same content hash for its own retry-counter keying, E5) — read
564
+ once, not twice.
565
+
566
+ ``depth``/``budget``/``parent`` (S06, ING-03): non-default only when this
567
+ call is a zip member or eml attachment re-entering the dispatcher (see
568
+ ``_process_nested`` below) — all three are their defaults for every
569
+ TOP-LEVEL inbox candidate. ``parent`` (the container's own note id) is
570
+ stamped onto every report entry this call produces, so a nested item's
571
+ provenance is traceable in the ingest report."""
572
+ if budget is None:
573
+ budget = {"bytes": 0, "items": 0}
574
+
575
+ def _append(bucket: str, entry: dict[str, Any]) -> None:
576
+ if parent is not None:
577
+ entry["parent"] = parent
578
+ report[bucket].append(entry)
579
+
580
+ if original_sha in manifest:
581
+ existing_id = manifest[original_sha]
582
+ _move(claimed, duplicate_dir / claimed.name)
583
+ (duplicate_dir / f"{claimed.name}.duplicate-of.txt").write_text(
584
+ f"identical content already ingested as raw/{existing_id}.md\n",
585
+ encoding="utf-8",
586
+ )
587
+ _append("duplicates", {
588
+ "file": claimed.name, "existing_id": existing_id,
589
+ "classification": _existing_note_classification(vault, existing_id),
590
+ })
591
+ return
592
+
593
+ handler = H.handler_for(claimed)
594
+ if handler is None:
595
+ reason = "no_handler_for_extension"
596
+ _quarantine(claimed, quarantine_dir, reason, [])
597
+ _append("quarantined", {"file": claimed.name, "reason": reason})
598
+ return
599
+ if not handler.available():
600
+ reason = f"missing_dependency:{handler.dependency_name}"
601
+ _quarantine(claimed, quarantine_dir, reason, [])
602
+ _append("quarantined", {"file": claimed.name, "reason": reason})
603
+ return
604
+
605
+ result = handler.extract(claimed)
606
+ if not result.ok:
607
+ _quarantine(claimed, quarantine_dir, result.quarantine_reason, result.warnings)
608
+ _append("quarantined", {"file": claimed.name, "reason": result.quarantine_reason})
609
+ return
610
+
611
+ stem = _slugify_stem(claimed.stem)
612
+ slug = safe_slug(f"{today}-{stem}")
613
+ archive_subdir = vault / "raw" / "originals" / f"{today}-{stem}"
614
+ # C7: the archived filename flows verbatim into the signed `origin:`
615
+ # frontmatter value (and the filesystem path) — sanitize it so a hostile
616
+ # or careless original name can't carry a quote/colon/backslash into
617
+ # either.
618
+ archive_path = archive_subdir / _sanitize_archive_name(claimed.name)
619
+
620
+ arch_status = _create_exclusive_or_collision(archive_path, original_bytes, known_sha=original_sha)
621
+ if arch_status == "collision":
622
+ reason = "archive_collision"
623
+ _quarantine(claimed, quarantine_dir, reason,
624
+ [f"archived-original target already holds different content: {archive_path}"])
625
+ _append("quarantined", {"file": claimed.name, "reason": reason})
626
+ return
627
+
628
+ meta = _meta(slug, today, archive_path, vault, hashlib.sha256(result.markdown.encode("utf-8")).hexdigest())
629
+ body_sha = meta["sha256"]
630
+ classification = meta["classification"]
631
+ note_rel = f"raw/{slug}.md"
632
+ note_path = vault / note_rel
633
+ if note_path.exists():
634
+ # Manifest miss but the target id already exists (e.g. a
635
+ # hand-deleted/corrupted manifest) — defense in depth. Compare the
636
+ # frontmatter `sha256:` of the existing note against this body's
637
+ # hash rather than re-serialising: same body -> idempotent no-op,
638
+ # different -> collision, never overwritten.
639
+ from .. import frontmatter as fm
640
+
641
+ existing_meta, _ = fm.parse_text(note_path.read_text(encoding="utf-8"))
642
+ if str(existing_meta.get("sha256", "")) != body_sha:
643
+ reason = "note_id_collision"
644
+ _quarantine(claimed, quarantine_dir, reason,
645
+ [f"raw/{slug}.md already exists with different content"])
646
+ _append("quarantined", {"file": claimed.name, "reason": reason})
647
+ return
648
+ manifest[original_sha] = slug
649
+ _save_manifest(vault, manifest)
650
+ claimed.unlink(missing_ok=True)
651
+ existing_classification = existing_meta.get("classification")
652
+ _append("duplicates", {
653
+ "file": orig_name, "existing_id": slug,
654
+ "classification": str(existing_classification) if existing_classification else None,
655
+ })
656
+ return
657
+
658
+ content = _build_frontmatter(meta, result.markdown)
659
+ core.write_note(
660
+ note_rel, content,
661
+ reason=f"ingest {orig_name} -> raw/{slug}.md "
662
+ f"(original archived at {archive_path.relative_to(vault)})",
663
+ subtree="raw",
664
+ )
665
+ manifest[original_sha] = slug
666
+ _save_manifest(vault, manifest)
667
+ claimed.unlink(missing_ok=True) # promoted; the processing copy is spent
668
+ _append("processed", {
669
+ "file": orig_name, "id": slug, "note": note_rel,
670
+ "archived": str(archive_path.relative_to(vault)),
671
+ "classification": classification,
672
+ "warnings": result.warnings,
673
+ })
674
+
675
+ # S06 (ING-03): zip members / eml attachments re-enter the SAME
676
+ # dispatcher, one level deeper. Only on a FRESH promotion (never on a
677
+ # duplicate/id-collision return above) — a duplicate top-level archive's
678
+ # members were already fully expanded the first time it was ingested, so
679
+ # re-walking them here would just re-report already-known duplicates for
680
+ # no benefit (and risks re-running expensive recursion on every re-drop
681
+ # of the same file).
682
+ nested = result.metadata.get("nested") if isinstance(result.metadata, dict) else None
683
+ if nested:
684
+ _process_nested(
685
+ nested, parent_slug=slug, depth=depth, budget=budget,
686
+ core=core, manifest=manifest, vault=vault, today=today,
687
+ quarantine_dir=quarantine_dir, duplicate_dir=duplicate_dir,
688
+ processing_dir=processing_dir, report=report,
689
+ )
690
+
691
+
692
+ def _process_nested(
693
+ nested: list[dict], *, parent_slug: str, depth: int, budget: dict[str, int],
694
+ core: Any, manifest: dict[str, str], vault: Path, today: str,
695
+ quarantine_dir: Path, duplicate_dir: Path, processing_dir: Path,
696
+ report: dict[str, Any],
697
+ ) -> None:
698
+ """Re-enter the dispatcher for each nested (name, data) item a handler
699
+ returned (zip member / eml attachment). Bounded by ``MAX_NESTED_DEPTH``
700
+ and by the shared ``budget`` (bytes + item count) across the WHOLE
701
+ recursion tree for this one top-level candidate — see the module-level
702
+ constants' docstring. A poison nested item is quarantined on its own;
703
+ it never aborts its siblings or the parent's already-completed promotion."""
704
+ if not nested:
705
+ return
706
+ if depth >= MAX_NESTED_DEPTH:
707
+ for item in nested:
708
+ report["skipped"].append({
709
+ "file": H.strip_control_chars(item.get("name") or "?"),
710
+ "reason": "nested_depth_exceeded", "parent": parent_slug,
711
+ })
712
+ return
713
+
714
+ for idx, item in enumerate(nested):
715
+ name = H.strip_control_chars(item.get("name") or f"member-{idx}")
716
+ data = item.get("data", b"")
717
+ if budget["items"] >= MAX_TOTAL_NESTED_ITEMS or budget["bytes"] + len(data) > MAX_TOTAL_NESTED_BYTES:
718
+ report["quarantined"].append({
719
+ "file": name, "reason": "nested_budget_exceeded", "parent": parent_slug,
720
+ })
721
+ continue
722
+ budget["items"] += 1
723
+ budget["bytes"] += len(data)
724
+
725
+ # Member/attachment names NEVER become filesystem paths directly —
726
+ # only a slugified BASENAME feeds a wholly synthetic temp filename.
727
+ ext = Path(name).suffix.lower()
728
+ safe_stem = _slugify_stem(Path(name).stem)
729
+ synth_name = f"{parent_slug}-nested-{idx}-{safe_stem}{ext}"
730
+ temp_path = _unique_dest(processing_dir, synth_name)
731
+ try:
732
+ processing_dir.mkdir(parents=True, exist_ok=True)
733
+ temp_path.write_bytes(data)
734
+ except OSError as exc:
735
+ report["skipped"].append({
736
+ "file": name, "reason": f"nested_write_error:{type(exc).__name__}", "parent": parent_slug,
737
+ })
738
+ continue
739
+
740
+ try:
741
+ _process_claimed(
742
+ temp_path, name, original_bytes=data, original_sha=_sha256_bytes(data),
743
+ core=core, manifest=manifest, vault=vault, today=today,
744
+ quarantine_dir=quarantine_dir, duplicate_dir=duplicate_dir,
745
+ processing_dir=processing_dir, report=report,
746
+ depth=depth + 1, budget=budget, parent=parent_slug,
747
+ )
748
+ except Exception as exc:
749
+ if _is_systemic_error(exc):
750
+ # A batch-wide outage (signing key vanished, disk full) hits
751
+ # every remaining nested item (and every remaining top-level
752
+ # candidate) identically — bubble it up so run_ingest's own
753
+ # systemic handling takes over, rather than quarantining a
754
+ # perfectly good nested item as if it were poison.
755
+ raise
756
+ # A per-item defect. Unlike top-level candidates, a nested item
757
+ # has no stable identity across separate `brain sync` runs to
758
+ # retry against (it is re-derived from its parent archive every
759
+ # time) — quarantine it immediately, never abort its siblings.
760
+ reason = "nested_processing_error"
761
+ if temp_path.exists():
762
+ _quarantine(temp_path, quarantine_dir, reason, [f"{type(exc).__name__}: {exc}"])
763
+ report["quarantined"].append({"file": name, "reason": reason, "parent": parent_slug})
764
+
765
+
766
+ # Leading-date filename styles a dropped document commonly carries. Anchored
767
+ # at the start and followed by a non-digit so partial numbers (audit codes
768
+ # like "2024_011", MMYYYY stamps like "042022") never misparse into a date.
769
+ _DOC_DATE_RES = (
770
+ re.compile(r"^(\d{4})[-_. ](\d{1,2})[-_. ](\d{1,2})(?!\d)"), # 2026-03-25
771
+ re.compile(r"^(\d{4})(\d{2})(\d{2})(?!\d)"), # 20260325
772
+ re.compile(r"^(\d{2})(\d{2})(\d{2})(?!\d)"), # 260325 (YYMMDD)
773
+ # Embedded full-ISO fallback (workspace naming style:
774
+ # "_scenario_board_2026-06-15_v16.md"). Only the unambiguous hyphenated
775
+ # ISO form is accepted mid-name — never the digit-run styles, which would
776
+ # false-positive on version/id numbers.
777
+ re.compile(r"(?<!\d)(\d{4})-(\d{2})-(\d{2})(?!\d)"),
778
+ # Trailing YYYYMMDD before the extension ("b2c-gp-analysis-20260331.md")
779
+ # — end-anchored so a digit run mid-name never matches; the calendar +
780
+ # range checks below still reject non-dates.
781
+ re.compile(r"(?<!\d)(\d{4})(\d{2})(\d{2})(?=\.[A-Za-z0-9]+$|$)"),
782
+ )
783
+
784
+
785
+ def _derive_document_date(name: str, today: str) -> str | None:
786
+ """Best-effort ``document_date`` from a leading date in the ORIGINAL
787
+ filename. Without it, a bulk re-ingestion of old documents ranks as the
788
+ freshest content in the vault (recency keys on capture date) and "latest"
789
+ queries ground on months-old material. Conservative by design: anything
790
+ ambiguous, non-calendar, pre-1990 or in the future returns None — an
791
+ undated source is NEUTRAL in recency ranking, a misdated one poisons it."""
792
+ for rx in _DOC_DATE_RES:
793
+ # search(), not match(): the first three patterns are ^-anchored (so
794
+ # search behaves identically), the embedded-ISO fallback is not.
795
+ m = rx.search(name)
796
+ if not m:
797
+ continue
798
+ y, mo, d = (int(g) for g in m.groups())
799
+ if y < 100:
800
+ y += 2000
801
+ try:
802
+ dd = _dt.date(y, mo, d)
803
+ except ValueError:
804
+ continue
805
+ if y < 1990 or dd > _dt.date.fromisoformat(today):
806
+ continue
807
+ return dd.isoformat()
808
+ return None
809
+
810
+
811
+ def _meta(slug: str, today: str, archive_path: Path, vault: Path, body_sha: str) -> dict[str, Any]:
812
+ meta: dict[str, Any] = {
813
+ "id": slug,
814
+ "type": "source",
815
+ "classification": "Internal", # ADR-0003: unlabelled -> MNPI; ingest
816
+ # DECLARES Internal explicitly, matching
817
+ # capture.py's own missing-classification
818
+ # default (never left unlabelled).
819
+ "captured": today,
820
+ "origin": str(archive_path.relative_to(vault)),
821
+ "sha256": body_sha,
822
+ "immutable": True,
823
+ }
824
+ doc_date = _derive_document_date(archive_path.name, today)
825
+ if doc_date and doc_date != today:
826
+ meta["document_date"] = doc_date
827
+ return meta
828
+
829
+
830
+ def _quarantine(claimed: Path, quarantine_dir: Path, reason: str, warnings: list[str]) -> None:
831
+ dest_dir = quarantine_dir / reason
832
+ # C3: _move is an os.rename, which SILENTLY REPLACES a same-named file —
833
+ # a second same-named corrupt drop would clobber the first quarantined
834
+ # original (the only copy). Uniquify the destination like every other
835
+ # sink (_claim, _sweep_stale_processing) does.
836
+ dest = _unique_dest(dest_dir, claimed.name)
837
+ _move(claimed, dest)
838
+ report_lines = [f"quarantine_reason: {reason}"] + [f"- {w}" for w in warnings]
839
+ (dest_dir / f"{dest.name}.reason.txt").write_text("\n".join(report_lines) + "\n", encoding="utf-8")