Skip to content

feat: source pipeline integrity and operator tooling (v1.34.0)#143

Merged
solaitken merged 14 commits into
mainfrom
feat/source-pipeline-integrity
Jul 19, 2026
Merged

feat: source pipeline integrity and operator tooling (v1.34.0)#143
solaitken merged 14 commits into
mainfrom
feat/source-pipeline-integrity

Conversation

@solaitken

@solaitken solaitken commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

Vault intake becomes trustworthy end to end: ingest is scoped (--src-subpath / --exclude) and gated (extractable allowlist), batches are reconciled so silently-lost sources are named, a deterministic no-LLM pre-pass seeds code structure before agent ingest, prose citations become dated provenance events, search gains a configurable FTS tokenizer and graph-degree predicates, and the operator gets doctor --repair plus a unified o2b brain status snapshot with an exact next command on every problem line. 11 kanban tasks, one release (v1.34.0).

Why this wave pays off

flowchart LR
    SRC["Source tree"] --> SCOPE["Scope<br/>src-subpath + exclude<br/>nested gitignore"]
    SCOPE --> GATE["Gate<br/>extractable allowlist"]
    GATE --> PRE["Pre-extract<br/>deterministic code seeds"]
    PRE --> AGENT["Agent ingest"]
    AGENT --> REC["Reconcile<br/>dispatched vs ingested"]
    REC -->|"gap named, never hidden"| OP["Operator"]
    OP --> STATUS["brain status<br/>next-command hints"]
    STATUS --> REPAIR["doctor --repair<br/>guarded one-command fixes"]
    REPAIR --> SRC
Loading

Before this wave a monorepo ingested whole or not at all, a dead pack flag gated nothing, an agent could drop dispatched files without a trace, and health signals were scattered across six read-only surfaces. Now every stage of intake is explicit and every gap is reported, and the operator loop closes: status names the problem, the problem names its command, the command fixes it.

Concrete benefits

  • No silent losses: --reconcile diffs a plan's dispatched set against checkpoint completions and names every missing source; over-reports rather than hides.
  • Token economy: the stdlib pre-extract pass hands deterministic entity/edge seeds to the agent step, cutting extraction work on code sources.
  • Scoped intake: one shared gitignore-semantics engine (src/core/fs/ignore.ts) drives both the hygiene scan and ingest excludes; one home, no drift.
  • Provenance from prose: hand-typed [Source: name, YYYY-MM-DD] citations land on the temporal timeline, deduplicated and dated at the citation.
  • Query wins: non-English vaults tune the FTS tokenizer (typed-error validated, reindex-explicit); orphans and hubs are one --degree predicate away.
  • Closed operator loop: hints travel with issue definitions in a shared diagnostics-signal registry, so brain status and doctor --repair can never disagree.
  • Byte-identical opt-out: every unit regression-tests that unset config and absent flags change nothing.

What ships

Unit Surface
Nested gitignore hygiene scan src/core/fs/ignore.ts engine + scan-repo.ts
Ingest scoping o2b brain batch-plan --src-subpath/--exclude, MCP params
Extractable gate discovery-time allowlist skip with reasons
Code pre-extraction o2b brain pre-extract, pre_extract on brain_ingest_source
Reconciliation o2b brain batch-plan --reconcile, MCP reconcile
Citation promotion o2b brain scan-citations
FTS tokenizer config search_fts_diacritics, search_fts_stemmer
Degree predicates o2b search --degree, degree on brain_search
Doctor repair o2b brain doctor --repair [--apply], MCP params
Operator snapshot o2b brain status, new MCP tool brain_status (103 total)
Clean pipe exit EPIPE exits 0; other stdout errors now fail loudly

Test plan

  • bun test - 6347 pass / 0 fail (793 files)
  • bun run typecheck - clean
  • bun run lint - exactly 134 warnings / 0 errors (baseline)
  • bun run fmt:check - clean
  • bun run sync-version:check - 1.34.0 across all 7 manifests
  • Phase-4 QA smoke in an isolated vault: all 11 scenarios PASSED end to end through the real CLI, MCP tools/list returns 103 with the new params present
  • Self-review vs main: no correctness, security, or reachability findings; ignore-engine composition verified against git check-ignore ground truth

Summary by CodeRabbit

  • New Features
    • Added scoped ingestion with nested ignore rules, subtree discovery/exclusions, extractability gating, deterministic batch reconciliation, and surfaced skipped-non-extractable reporting.
    • Added deterministic code pre-extraction, improved inline citation promotion, and new citation scanning.
    • Added configurable search FTS tokenizer and graph-degree filtering (backlinks/outlinks).
    • Added operator status snapshots and guarded doctor repair workflows (preview/repair).
    • Expanded MCP tooling parity (including new status tool).
  • Bug Fixes
    • Improved CLI behavior when stdout is closed by downstream commands (clean exit).
  • Documentation
    • Updated changelog, README “What’s new,” CLI reference, and MCP documentation for v1.34.0.

solaitken and others added 13 commits July 18, 2026 20:14
An early-closed stdout pipe (`o2b <listing> | head -1`) surfaces as an
EPIPE write failure. That is the normal end of a shell pipeline, not a
program error, so the CLI must exit 0 with no diagnostic. A non-EPIPE
stdout write failure (ENOSPC, EIO, ...) is a real I/O fault and was
being swallowed silently; it now fails loudly with its message and a
nonzero exit.

The new guard installs a `process.stdout` error listener (Bun surfaces a
closed-pipe write as an asynchronous error event) and the entry point's
promise catch covers the synchronous-throw case. `vault-log` shares the
same `main.ts` entry, so both CLIs are covered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The hygiene repo scan only skipped a static list of directory names, so
paths ignored by the repo's own .gitignore files were still read and
scanned. A new path-scope engine (src/core/fs/ignore.ts) implements the
documented gitignore subset: basename vs anchored matching, directory-only
patterns, `*`/`?`/doubled-star wildcards, nested composition where a deeper
file scopes its subtree, a nearer `!` re-include winning over an outer
ignore, and `.git/info/exclude` layered at the lowest precedence.

scan-repo composes these per directory during its walk. With no ignore
files present the walk is byte-identical to the static skip-dir baseline.
Malformed patterns are reported on stderr and produce no rule, so a bad
pattern never silently drops a path. The engine sits below both hygiene
and ingest so P2 can reuse it without a duplicate matcher.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Folder-ingest planning could only walk a whole source directory. It now
accepts two optional scoping controls, plumbed through the batch-plan CLI
verb and the brain_ingest_batch_plan MCP tool:

- --src-subpath restricts discovery to a subtree of the source dir; a
  value escaping the source root rejects with a typed error.
- --exclude takes gitignore-style patterns matched by the shared
  src/core/fs/ignore.ts engine (no duplicate matcher); an excluded
  directory is pruned so its subtree is never walked. A malformed pattern
  is a typed error, never a silent skip.

With neither flag set the plan (paths, planId, sourceDir) is
byte-identical to before. No new MCP tools are added.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The schema pack stored an `extractable` allowlist of schema tokens (set
via the set_extractable mutation) but nothing consulted it. Folder-ingest
page discovery now gates on it: when the allowlist is non-empty, a
discovered page whose `schema_type` is not in it is skipped before
extraction, reported on the plan as skipped-with-reason, and logged to
stderr rather than silently ingested. A page with no declared type stays
ungated, and an empty allowlist gates nothing so the plan (paths, planId,
and serialized output) is byte-identical to before.

The gate is a pure, read-only helper that reads frontmatter and returns a
partition; it touches no schema mutation surface. The batch-plan CLI verb
and brain_ingest_batch_plan MCP tool surface the skipped set only when
non-empty.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a no-LLM, stdlib-only pre-ingest pass (P4, t_ef786747) that turns a
code source into JSON entity/edge seeds: classes and functions as entity
seeds, imports and inheritance as edge seeds. Structural parsing per
language family (TypeScript/JavaScript and Python) via a line grammar,
deduped and sorted so the same input always yields byte-identical output.
Unsupported extensions are reported as unextracted with a reason, never a
fake empty success.

Wire the pass into ingestSource behind an opt-in preExtract option that
surfaces the seeds on the result for the agent step; with the pass off the
ingest and its summary page are byte-identical to before. Expose the pass
via a new `o2b brain pre-extract` CLI verb and an optional pre_extract
flag on brain_ingest_source (no new MCP tool; tool count stays 102).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add reconcilePlan (P5, t_d067a153): after a batch plan drains, diff the
plan's dispatched set (batch files plus manifest-unchanged skips) against
the checkpoint's completed entries and report the gap - each source that
was dispatched but never recorded as ingested. It is a warning surface,
not a retry: read-only over checkpoint state, idempotent, and it
re-dispatches nothing. A plan that dispatched nothing, or whose sources
all completed, reports an empty gap explicitly.

Surface the report via a `reconcile` flag on brain_ingest_batch_plan and
`--reconcile` on `o2b brain batch-plan`; the report rides on the output
only when requested, so the default output stays byte-identical. The MCP
reconcile runs before any resume-drain checkpoint clear so it reads a live
checkpoint. No new MCP tool; tool count stays 102.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…vents

Add a structural parser for inline `[Source: <name>, YYYY-MM-DD]` prose
markers and a scan surface that promotes each well-formed citation into
the temporal timeline as a dated `source-citation` provenance event,
stamped at the citation date.

- New `src/core/brain/temporal/citations.ts`: pure `parseCitations`
  (fixed marker grammar, ISO-shaped date validated as a real calendar
  date, no natural-language date parsing) plus `scanCitations` which
  walks the configured note folders and appends one dated event per
  unique citation.
- Dedup on (normalized name, date) against already-logged
  source-citation events, so a re-scan of an unchanged vault promotes
  nothing. Malformed markers are reported explicitly and skipped.
- New `source-citation` log event kind; a new `o2b brain scan-citations`
  CLI verb (--dry-run, --strict, --path, --exclude, --json).

Kept CLI-only for the agent surface: scanning is an operator maintenance
action like scan-inline (also CLI-only), and adding an MCP tool would
break the frozen 102-tool parity count with no existing tool to extend.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Assemble the `chunk_fts` FTS5 tokenizer clause from validated config
keys instead of a hardcoded literal, so operators can tune diacritic
folding and layer Porter stemming without patching the schema.

- New `buildFtsTokenize` in schema.ts composes the clause from an
  allow-list (`search_fts_diacritics` 0|1|2, `search_fts_stemmer`
  none|porter); an out-of-range value throws a typed
  SearchError("INVALID_INPUT") listing the allowed values before it can
  reach the DDL.
- Migrations that create `chunk_fts` (v1, v2, v5) take the clause via a
  MigrationContext threaded through applyMigrations; unset config yields
  the historical `unicode61 remove_diacritics 2` byte-identically. The
  CJK `chunk_trigram` shadow index keeps its `trigram` tokenizer.
- resolveSearchConfig resolves and validates the keys once
  (ResolvedSearchConfig.ftsTokenize); Store.open passes the clause to
  applyMigrations. No implicit reindex: the clause only takes effect on a
  fresh build, documented in the CLI reference.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add count predicates over a note's backlinks/outlinks to the search
filter DSL, so operators and agents can select orphans (`backlinks=0`),
hubs (`outlinks>=N`), and any `= != > >= < <=` comparison.

- graph-index.ts gains directed in/out degree maps on the memoized
  snapshot plus a `degreeForPath` accessor; a path with no resolved
  edges (or an unknown path) reads as an orphan.
- property-filter.ts gains `parseDegreePredicate` (typed
  SearchError("INVALID_INPUT") on bad syntax, listing allowed fields and
  operators) and a pure `applyDegreeFilters` (dependency-injected degree
  lookup; empty list is a byte-identical pass-through).
- search.ts applies the predicates as a post-rank phase backed by the
  link-graph snapshot; result-filters.ts wires the store. CLI `--degree`
  and the MCP brain_search `degree` param feed the same predicate list.
- Queries without degree predicates are byte-identical; the MCP tool
  count stays 102 (a new param on the existing tool, not a new tool).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Introduce the wave's diagnostics-signal model (issue class + detector +
optional fixer + next-command hint) co-located with the doctor, and a
guarded `o2b brain doctor --repair` mode built on it.

- `--repair` previews planned fixes without writing (dry-run default);
  `--repair --apply` performs them and appends one typed `doctor-repair`
  event per fix. Re-running after apply is an idempotent no-op.
- Fixers exist only for issue classes the doctor already detects:
  `wal-gap` closes a dangling dream workrun (an append-only write-ahead
  checkpoint log with no terminal phase) by appending the missing
  `interrupted` marker; `orphaned-reference` prunes a dead
  `evidenced_by` Brain link from a preference/retired record and its
  `## Origin` bullet. Broken structural links (supersedes, retired_by,
  superseded_by) are reported needs-review, never auto-removed.
- Detected classes with no fixer are aggregated as unfixable, each with
  its next command from the signal registry (hints travel with the
  issue definition, never hardcoded in a formatter).
- Plain doctor and `--strict` stay read-only and byte-identical;
  `--strict` cannot be combined with an applying repair.
- MCP: optional `repair`/`apply` params on the existing brain_doctor
  tool (tool count stays 102).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add `o2b brain status` (and the `brain_status` MCP counterpart): one
read-only verb composing the operator signal sources that previously
required polling six commands - doctor, semantic health, hygiene, stale
scan, review candidates, active profile, and state-file health - into a
single readable snapshot.

- Every problem line carries the exact next command to run, looked up
  from the O2 diagnostics-signal registry (resolveSignal), so hints
  travel with the issue definition and are never hardcoded in the
  formatter.
- A healthy vault prints a compact all-clear; each source is wrapped
  fail-soft so one broken surface degrades its line, not the snapshot.
- Reads only.
- MCP tool count goes 102 -> 103; frozen parity/count tests updated
  deliberately (mcp.test, removed-tools, brain-tools-parity), and
  brain_status is added to the preview-budget exempt list (bounded
  snapshot).

Also point the review-queue signal hint at `o2b brain dream --dry-run`
(the real surface for review candidates).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@solaitken
solaitken enabled auto-merge (squash) July 18, 2026 23:06
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a20cb635-bf9c-409b-8bce-936bf3a7d508

📥 Commits

Reviewing files that changed from the base of the PR and between 5c298e7 and 893e7e5.

📒 Files selected for processing (13)
  • src/cli/brain/verbs/doctor.ts
  • src/cli/command-manifest.ts
  • src/core/brain/diagnostics.ts
  • src/core/brain/ingest/batch-plan.ts
  • src/core/brain/ingest/ingest.ts
  • src/core/brain/ingest/reconcile.ts
  • src/core/brain/operator-snapshot.ts
  • src/core/fs/ignore.ts
  • src/core/hygiene/scan-repo.ts
  • src/mcp/brain/health-tools.ts
  • tests/core/brain/ingest/reconcile.test.ts
  • tests/core/fs/ignore.test.ts
  • tests/core/hygiene/scan-repo-ignore.test.ts
🚧 Files skipped from review as they are similar to previous changes (11)
  • tests/core/fs/ignore.test.ts
  • src/cli/brain/verbs/doctor.ts
  • src/mcp/brain/health-tools.ts
  • src/core/hygiene/scan-repo.ts
  • tests/core/hygiene/scan-repo-ignore.test.ts
  • src/cli/command-manifest.ts
  • src/core/brain/ingest/batch-plan.ts
  • src/core/brain/ingest/ingest.ts
  • src/core/brain/operator-snapshot.ts
  • src/core/brain/diagnostics.ts
  • src/core/fs/ignore.ts

📝 Walkthrough

Walkthrough

Changes

The v1.34.0 release adds scoped discovery, ignore rules, extractability gating, deterministic code extraction, reconciliation, citation promotion, configurable search, graph-degree predicates, repair/status tooling, MCP updates, and clean stdout-pipe handling. Documentation, tests, and version metadata are updated.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: itechmeat

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.97% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: source pipeline integrity and operator tooling for v1.34.0.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/source-pipeline-integrity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/core/hygiene/scan-repo.ts (1)

205-220: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Apply the ignore scope to root scan files too.

The recursive targets use baseScope, but SCAN_ROOT_FILES are appended unconditionally. Consequently, an ignored root file such as README.md is still scanned, violating the new discovery contract.

Proposed fix
   for (const name of SCAN_ROOT_FILES) {
     const p = join(root, name);
     try {
-      if (statSync(p).isFile()) abs.push(p);
+      if (statSync(p).isFile() && !baseScope.isIgnored(toPosixRel(root, p), false)) {
+        abs.push(p);
+      }
     } catch {

Add a regression test covering a root file ignored by .gitignore.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/hygiene/scan-repo.ts` around lines 205 - 220, Update listScanTargets
so each SCAN_ROOT_FILES entry is checked against baseScope before being
appended, matching the filtering applied by collectFiles; preserve absent-file
handling and reportIgnoreWarnings behavior. Add a regression test demonstrating
that a root file ignored by .gitignore is excluded from the returned scan
targets.
🧹 Nitpick comments (3)
src/cli/command-manifest.ts (2)

198-198: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add missing flags to the command manifest.

The scan-citations verb accepts flags (e.g. --dry-run, --strict, --path, --exclude) in its handler (cmdBrainScanCitations), but they are missing from the command manifest. Declaring them here ensures shell completions and manifest-driven tooling can surface these options.

🛠️ Proposed fix
-        command("scan-citations", "Promote inline [Source: ...] citations to dated events"),
+        command(
+          "scan-citations",
+          "Promote inline [Source: ...] citations to dated events",
+          [
+            flag("vault", "string"),
+            flag("dry-run", "boolean"),
+            flag("strict", "boolean"),
+            flag("path", "string-array"),
+            flag("exclude", "string-array"),
+            flag("agent", "string"),
+            flag("json", "boolean"),
+          ]
+        ),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/command-manifest.ts` at line 198, Add the handler-supported flags
--dry-run, --strict, --path, and --exclude to the scan-citations command
manifest entry, reusing the existing manifest flag declaration conventions so
completions and manifest-driven tooling expose them. Keep the command
description and cmdBrainScanCitations behavior unchanged.

134-134: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add missing flags to the command manifest.

The status verb accepts flags (e.g. --vault, --json) in its handler (cmdBrainStatus), but they are missing from the command manifest. Declaring them here ensures shell completions and manifest-driven tooling can surface these options.

🛠️ Proposed fix
-        command("status", "Unified operator status snapshot with next-command hints"),
+        command(
+          "status",
+          "Unified operator status snapshot with next-command hints",
+          [
+            flag("vault", "string"),
+            flag("json", "boolean"),
+          ]
+        ),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/command-manifest.ts` at line 134, Update the status command entry in
the command manifest to declare the flags accepted by cmdBrainStatus, including
--vault and --json, using the manifest’s existing flag-definition format so
completions and manifest-driven tooling expose them.
src/core/brain/ingest/batch-plan.ts (1)

315-316: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Use case-insensitive extension matching.

DEFAULT_INGESTIBLE_EXTENSIONS normalizes extensions to lowercase, but extname(entry.name) preserves the file's actual case. This will silently ignore valid files with uppercase extensions like README.MD. Consider lowercasing the extension before the set lookup.

♻️ Proposed fix
-      } else if (entry.isFile() && extensions.has(extname(entry.name))) {
+      } else if (entry.isFile() && extensions.has(extname(entry.name).toLowerCase())) {
         if (exclude.isIgnored(rel, false)) continue;
         out.push(abs);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/brain/ingest/batch-plan.ts` around lines 315 - 316, Update the
extension lookup in the directory-entry handling branch around entry.isFile() to
lowercase extname(entry.name) before checking extensions.has(...), preserving
the existing ignore filtering and ingestion flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.codex-plugin/plugin.json:
- Line 3: The version in .codex-plugin/plugin.json was edited directly; revert
that change, update the single source-of-truth version in package.json, then run
scripts/sync-version.ts via Bun to regenerate all mirrored manifest versions.

In `@src/cli/brain/verbs/doctor.ts`:
- Around line 19-20: Require repair mode whenever apply is supplied: update
src/cli/brain/verbs/doctor.ts lines 19-20 and the doctor command validation at
lines 30-34 to reject apply without repair via usageError before normal
execution, using plain stderr and exit code 2 rather than a JSON envelope;
update src/mcp/brain/health-tools.ts lines 25-37 to reject apply: true unless
repair: true, and align the schema contract at lines 160-169 with this
requirement.

In `@src/cli/brain/verbs/scan-citations.ts`:
- Around line 35-59: Replace every use of the nonexistent result.malformed
property in the scanCitations output and strict-exit logic with
result.malformedMarkers.length. Preserve the existing JSON field, text output,
and --strict behavior while deriving the malformed count from the
malformedMarkers collection.

In `@src/core/brain/diagnostics.ts`:
- Around line 322-337: The apply method must acquire the existing workrun file
lock before its idempotency recheck, then hold that lock through
scanDanglingWorkruns and appendFileSync. Update the apply flow around path and
the second scan to use the established locking mechanism, releasing it reliably
afterward while preserving the current null-return behavior and append logic.
- Around line 447-454: Update removeOriginBullet so it only filters matching
wikilink bullets while inside the ## Origin section. Track section boundaries
during line processing, preserve bullets in all other sections, and retain the
existing normaliseWikilinkTarget comparison within Origin.
- Around line 561-571: Update the repair flow around FIXER_BY_CODE and
appendLogEvent so disk mutations and their doctorRepair audit events are
failure-atomic: ensure a failed appendLogEvent rolls back the fixer’s persisted
changes, or otherwise defer/commit the repair only after logging succeeds.
Preserve idempotent no-op handling and only add the repair to applied after both
mutation and audit logging complete.
- Around line 470-489: The COVERED_DOCTOR_CODES logic in planRepair must not
discard every broken-wikilink finding based solely on its code. Restrict
coverage to findings actually enumerated by the relevant fixer, or retain
unmatched broken-wikilink instances in the unfixable results, while preserving
aggregation of other uncovered doctor findings.

In `@src/core/brain/ingest/batch-plan.ts`:
- Around line 195-197: Remove the process.stderr.write loop over
skippedNonExtractable from the batch-plan core logic, leaving
skippedNonExtractable available on the returned plan for callers such as
cmdBrainBatchPlan to present. Do not add replacement logging or other side
effects in this function.

In `@src/core/brain/ingest/extractable-gate.ts`:
- Around line 65-87: The partitionExtractable flow currently performs
synchronous pageType/frontmatter I/O for every path when the allowlist is
active. Refactor the planning flow around classifyPaths in batch-plan.ts to
apply the extractability gate only to new and modified files, preserving
unchanged files as unchanged; avoid calling partitionExtractable for the full
discovered-path set.

In `@src/core/brain/ingest/ingest.ts`:
- Around line 197-210: Update runPreExtract to treat any source that cannot be
read as unextracted, not only paths rejected by existsSync. Guard readFileSync
for directories, permission errors, and deletion races, returning the existing
extracted: false result with the canonicalSource reason instead of propagating
the read failure; preserve successful preExtractCodeStructure processing for
readable files.

In `@src/core/brain/ingest/pre-extract.ts`:
- Around line 140-219: Update parseTypeScript and parsePython to tokenize or
otherwise track multiline comment and string-literal state before applying
declaration/import regexes, so matches inside block comments, triple-quoted
Python strings, and TS/JS string literals are ignored. Preserve extraction of
real declarations and imports outside comments and literals, including correct
state across lines.

In `@src/core/brain/ingest/reconcile.ts`:
- Around line 48-57: Update reconcilePlan to build the complete source set from
batch files, plan.skipped, and checkpoint completions matching the plan, then
classify manifest skips and checkpointed paths as ingested rather than missing.
Preserve missing classification only for dispatched batch paths without either
confirmation, and add regressions covering an all-unchanged plan and a fully
resumed plan.

In `@src/core/brain/operator-snapshot.ts`:
- Around line 121-123: Update each catch block in the snapshot collection flow
to record an explicit degraded problem for the failed source instead of leaving
its clean default unchanged. Apply this to the hygiene, stale, review, and
doctor probes, ensuring the added problem causes healthy to become false while
preserving snapshot resilience and continued collection.
- Around line 95-109: Update the runDoctor invocation in the health-check flow
to pass the already resolved search database path from resolveSearchConfig,
using the same vault and configPath inputs as the preceding existsSync check.
Preserve the existing doctor options, including now, so DB-backed findings such
as tier-drift are included.

In `@src/core/brain/temporal/citations.ts`:
- Around line 303-332: Make the citation check-and-persist sequence in the
marker promotion flow atomic by acquiring the shared log lock before checking
DedupIndex and keeping it through appendLogEvent and dedup updates. On
appendLogEvent failure, reconcile DedupIndex from the authoritative JSONL state
before deciding whether to count the citation as promoted or allow a retry, so
partial Markdown failures cannot leave committed events untracked or create
duplicates.

In `@src/core/fs/ignore.ts`:
- Around line 118-132: Update stripTrailingWhitespace so an odd preceding
backslash still preserves the trailing whitespace but removes the backslash that
escapes it from the returned rule. Keep existing trimming behavior for unescaped
whitespace and add a regression test confirming foo\  compiles to match the
filename foo .
- Around line 65-115: Update translateGlob to avoid emitting backtracking-prone
regexes for repeated **/ and wildcard chains, using a linear-time matching
strategy or enforcing a strict wildcard complexity budget while preserving glob
semantics. Also unescape escaped trailing spaces before translation so patterns
such as foo\  match foo  rather than a backslash followed by a space.

---

Outside diff comments:
In `@src/core/hygiene/scan-repo.ts`:
- Around line 205-220: Update listScanTargets so each SCAN_ROOT_FILES entry is
checked against baseScope before being appended, matching the filtering applied
by collectFiles; preserve absent-file handling and reportIgnoreWarnings
behavior. Add a regression test demonstrating that a root file ignored by
.gitignore is excluded from the returned scan targets.

---

Nitpick comments:
In `@src/cli/command-manifest.ts`:
- Line 198: Add the handler-supported flags --dry-run, --strict, --path, and
--exclude to the scan-citations command manifest entry, reusing the existing
manifest flag declaration conventions so completions and manifest-driven tooling
expose them. Keep the command description and cmdBrainScanCitations behavior
unchanged.
- Line 134: Update the status command entry in the command manifest to declare
the flags accepted by cmdBrainStatus, including --vault and --json, using the
manifest’s existing flag-definition format so completions and manifest-driven
tooling expose them.

In `@src/core/brain/ingest/batch-plan.ts`:
- Around line 315-316: Update the extension lookup in the directory-entry
handling branch around entry.isFile() to lowercase extname(entry.name) before
checking extensions.has(...), preserving the existing ignore filtering and
ingestion flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 779d4c38-1f5a-41fc-aabb-3db8b50d4e54

📥 Commits

Reviewing files that changed from the base of the PR and between 77513f2 and 5c298e7.

📒 Files selected for processing (83)
  • .claude-plugin/plugin.json
  • .codex-plugin/plugin.json
  • CHANGELOG.md
  • README.md
  • docs/brainstorm/source-pipeline-integrity/cli-output/claude.md
  • docs/brainstorm/source-pipeline-integrity/cli-output/prompt.md
  • docs/brainstorm/source-pipeline-integrity/design.md
  • docs/brainstorm/source-pipeline-integrity/plan.md
  • docs/brainstorm/source-pipeline-integrity/variants.md
  • docs/cli-reference.md
  • docs/mcp.md
  • openclaw.plugin.json
  • package.json
  • plugin.yaml
  • plugins/codex/.codex-plugin/plugin.json
  • plugins/hermes/plugin.yaml
  • pyproject.toml
  • src/cli/brain.ts
  • src/cli/brain/help-text.ts
  • src/cli/brain/verbs/batch-plan.ts
  • src/cli/brain/verbs/doctor.ts
  • src/cli/brain/verbs/index.ts
  • src/cli/brain/verbs/pre-extract.ts
  • src/cli/brain/verbs/scan-citations.ts
  • src/cli/brain/verbs/status.ts
  • src/cli/command-manifest.ts
  • src/cli/main.ts
  • src/cli/search.ts
  • src/cli/stdout-guard.ts
  • src/core/brain/diagnostics.ts
  • src/core/brain/doctor.ts
  • src/core/brain/ingest/batch-plan.ts
  • src/core/brain/ingest/extractable-gate.ts
  • src/core/brain/ingest/ingest.ts
  • src/core/brain/ingest/pre-extract.ts
  • src/core/brain/ingest/reconcile.ts
  • src/core/brain/link-graph/graph-index.ts
  • src/core/brain/operator-snapshot.ts
  • src/core/brain/temporal/citations.ts
  • src/core/brain/types.ts
  • src/core/fs/ignore.ts
  • src/core/hygiene/scan-repo.ts
  • src/core/search/index.ts
  • src/core/search/property-filter.ts
  • src/core/search/result-filters.ts
  • src/core/search/schema.ts
  • src/core/search/search.ts
  • src/core/search/store.ts
  • src/core/search/types.ts
  • src/mcp/brain/health-tools.ts
  • src/mcp/brain/ingest-tools.ts
  • src/mcp/registry-guard.ts
  • src/mcp/search-tools.ts
  • tests/cli/brain-batch-plan-reconcile.test.ts
  • tests/cli/brain-batch-plan-scoping.test.ts
  • tests/cli/brain-pre-extract.test.ts
  • tests/cli/brain.test.ts
  • tests/cli/stdout-epipe-guard.test.ts
  • tests/core/brain.diagnostics.test.ts
  • tests/core/brain.operator-snapshot.test.ts
  • tests/core/brain.types.test.ts
  • tests/core/brain/graph-index.test.ts
  • tests/core/brain/ingest/batch-plan-extractable.test.ts
  • tests/core/brain/ingest/batch-plan-scoping.test.ts
  • tests/core/brain/ingest/extractable-gate.test.ts
  • tests/core/brain/ingest/ingest.test.ts
  • tests/core/brain/ingest/pre-extract.test.ts
  • tests/core/brain/ingest/reconcile.test.ts
  • tests/core/brain/temporal/citations.test.ts
  • tests/core/fs/ignore.test.ts
  • tests/core/hygiene/scan-repo-ignore.test.ts
  • tests/core/search/degree-filter-search.test.ts
  • tests/core/search/degree-filter.test.ts
  • tests/core/search/fts-tokenizer.test.ts
  • tests/core/search/store.test.ts
  • tests/core/search/store.vec.test.ts
  • tests/helpers/epipe-stream-harness.ts
  • tests/helpers/search-fixtures.ts
  • tests/mcp/brain-tools-parity.test.ts
  • tests/mcp/brain.test.ts
  • tests/mcp/ingest-tool.test.ts
  • tests/mcp/mcp.test.ts
  • tests/mcp/removed-tools.test.ts

Comment thread .codex-plugin/plugin.json
{
"name": "open-second-brain",
"version": "1.33.0",
"version": "1.34.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔴 Critical | ⚡ Quick win

Revert this hand-edited version bump and use the sync script.

As per coding guidelines, the version in package.json is the single source of truth and mirrored files like .codex-plugin/plugin.json must never be hand-edited. Please revert this change, bump the version in package.json, and run bun run scripts/sync-version.ts to propagate the change to all mirrored manifest files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.codex-plugin/plugin.json at line 3, The version in
.codex-plugin/plugin.json was edited directly; revert that change, update the
single source-of-truth version in package.json, then run scripts/sync-version.ts
via Bun to regenerate all mirrored manifest versions.

Source: Coding guidelines

Comment thread src/cli/brain/verbs/doctor.ts
Comment on lines +35 to +59
malformed: result.malformed,
malformed_markers: result.malformedMarkers.map((m) => ({
path: m.path,
line: m.line,
raw: m.raw,
reason: m.reason,
})),
errors: result.errors.map((e) => ({ path: e.path, message: e.message })),
files_with_citations: result.filesWithCitations.map((f) => ({
path: f.path,
citations: f.citations,
})),
});
} else {
ok(`scanned: ${result.scanned}`);
ok(`found: ${result.found}`);
ok(`promoted: ${result.promoted}`);
ok(`deduped: ${result.deduped}`);
if (result.malformed > 0) ok(`malformed: ${result.malformed}`);
for (const m of result.malformedMarkers) {
info(` malformed: ${m.path}:${m.line}: ${m.reason} (${m.raw})`);
}
for (const e of result.errors) info(` error: ${e.path}: ${e.message}`);
}
if (flags["strict"] && result.malformed > 0) return 2;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Use malformedMarkers.length instead of malformed.

The CitationScanResult returned by scanCitations does not have a malformed property; it contains malformedMarkers. As a result, result.malformed evaluates to undefined, which silently breaks the --strict exit-code logic (undefined > 0 is false) and omits the count from both the JSON and text outputs.

🐛 Proposed fix
-      malformed: result.malformed,
+      malformed: result.malformedMarkers.length,
       malformed_markers: result.malformedMarkers.map((m) => ({
         path: m.path,
         line: m.line,
         raw: m.raw,
         reason: m.reason,
       })),
       errors: result.errors.map((e) => ({ path: e.path, message: e.message })),
       files_with_citations: result.filesWithCitations.map((f) => ({
         path: f.path,
         citations: f.citations,
       })),
     });
   } else {
     ok(`scanned: ${result.scanned}`);
     ok(`found: ${result.found}`);
     ok(`promoted: ${result.promoted}`);
     ok(`deduped: ${result.deduped}`);
-    if (result.malformed > 0) ok(`malformed: ${result.malformed}`);
+    if (result.malformedMarkers.length > 0) ok(`malformed: ${result.malformedMarkers.length}`);
     for (const m of result.malformedMarkers) {
       info(`  malformed: ${m.path}:${m.line}: ${m.reason} (${m.raw})`);
     }
     for (const e of result.errors) info(`  error: ${e.path}: ${e.message}`);
   }
-  if (flags["strict"] && result.malformed > 0) return 2;
+  if (flags["strict"] && result.malformedMarkers.length > 0) return 2;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
malformed: result.malformed,
malformed_markers: result.malformedMarkers.map((m) => ({
path: m.path,
line: m.line,
raw: m.raw,
reason: m.reason,
})),
errors: result.errors.map((e) => ({ path: e.path, message: e.message })),
files_with_citations: result.filesWithCitations.map((f) => ({
path: f.path,
citations: f.citations,
})),
});
} else {
ok(`scanned: ${result.scanned}`);
ok(`found: ${result.found}`);
ok(`promoted: ${result.promoted}`);
ok(`deduped: ${result.deduped}`);
if (result.malformed > 0) ok(`malformed: ${result.malformed}`);
for (const m of result.malformedMarkers) {
info(` malformed: ${m.path}:${m.line}: ${m.reason} (${m.raw})`);
}
for (const e of result.errors) info(` error: ${e.path}: ${e.message}`);
}
if (flags["strict"] && result.malformed > 0) return 2;
malformed: result.malformedMarkers.length,
malformed_markers: result.malformedMarkers.map((m) => ({
path: m.path,
line: m.line,
raw: m.raw,
reason: m.reason,
})),
errors: result.errors.map((e) => ({ path: e.path, message: e.message })),
files_with_citations: result.filesWithCitations.map((f) => ({
path: f.path,
citations: f.citations,
})),
});
} else {
ok(`scanned: ${result.scanned}`);
ok(`found: ${result.found}`);
ok(`promoted: ${result.promoted}`);
ok(`deduped: ${result.deduped}`);
if (result.malformedMarkers.length > 0) ok(`malformed: ${result.malformedMarkers.length}`);
for (const m of result.malformedMarkers) {
info(` malformed: ${m.path}:${m.line}: ${m.reason} (${m.raw})`);
}
for (const e of result.errors) info(` error: ${e.path}: ${e.message}`);
}
if (flags["strict"] && result.malformedMarkers.length > 0) return 2;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/brain/verbs/scan-citations.ts` around lines 35 - 59, Replace every
use of the nonexistent result.malformed property in the scanCitations output and
strict-exit logic with result.malformedMarkers.length. Preserve the existing
JSON field, text output, and --strict behavior while deriving the malformed
count from the malformedMarkers collection.

Comment thread src/core/brain/diagnostics.ts Outdated
Comment thread src/core/brain/diagnostics.ts
Comment thread src/core/brain/operator-snapshot.ts Outdated
Comment thread src/core/brain/operator-snapshot.ts
Comment on lines +303 to +332
if (dedup.has(marker.name, marker.date)) {
deduped++;
continue;
}
if (opts.dryRun) {
// Count the promotion the run WOULD make, and record it in the
// in-memory index so repeated markers within one dry run still
// dedup against the first.
promoted++;
dedup.add(marker.name, marker.date);
continue;
}
try {
appendLogEvent(vault, {
timestamp: `${marker.date}T00:00:00Z`,
eventType: BRAIN_LOG_EVENT_KIND.sourceCitation,
body: {
agent: opts.agent,
name: marker.name,
date: marker.date,
source: `[[${vaultRelSource}]]`,
},
});
dedup.add(marker.name, marker.date);
promoted++;
} catch (err) {
errors.push({
path: file.absPath,
message: `promote failed: ${(err as Error).message ?? String(err)}`,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make citation deduplication atomic with event persistence.

The check and append are separate operations, so concurrent scans can both emit the same citation. Additionally, appendLogEvent can persist JSONL before its Markdown write fails; the catch then leaves DedupIndex stale, allowing another identical marker in this scan to append a duplicate and reporting the committed event as unpromoted.

Perform check-and-append under one shared log lock, and reconcile the authoritative JSONL state after partial failures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/brain/temporal/citations.ts` around lines 303 - 332, Make the
citation check-and-persist sequence in the marker promotion flow atomic by
acquiring the shared log lock before checking DedupIndex and keeping it through
appendLogEvent and dedup updates. On appendLogEvent failure, reconcile
DedupIndex from the authoritative JSONL state before deciding whether to count
the citation as promoted or allow a retry, so partial Markdown failures cannot
leave committed events untracked or create duplicates.

Comment thread src/core/fs/ignore.ts
Comment on lines +65 to +115
function translateGlob(body: string): string {
let re = "";
const n = body.length;
let i = 0;
while (i < n) {
const ch = body[i]!;
if (ch === "*") {
const isDouble = body[i + 1] === "*";
if (isDouble) {
const prevIsBoundary = i === 0 || body[i - 1] === "/";
let j = i;
while (body[j] === "*") j++;
const nextIsBoundary = j >= n || body[j] === "/";
if (prevIsBoundary && nextIsBoundary) {
if (body[j] === "/") {
// `**/` - zero or more leading path segments.
re += "(?:[^/]+/)*";
i = j + 1;
} else {
// trailing `**` - everything below (and the node itself).
re += ".*";
i = j;
}
} else {
// `**` glued to a segment behaves like a single `*`.
re += "[^/]*";
i = j;
}
} else {
re += "[^/]*";
i++;
}
} else if (ch === "?") {
re += "[^/]";
i++;
} else if (ch === "[") {
let j = i + 1;
if (body[j] === "!" || body[j] === "^") j++;
if (body[j] === "]") j++; // a leading `]` is a literal member
while (j < n && body[j] !== "]") j++;
if (j >= n) throw new MalformedPatternError("unterminated character class");
let cls = body.slice(i, j + 1);
if (cls.startsWith("[!")) cls = `[^${cls.slice(2)}`;
re += cls;
i = j + 1;
} else {
re += escapeLiteral(ch);
i++;
}
}
return re;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the dynamic-regex construction and all parser call sites.
rg -n -C4 'new RegExp|translateGlob|parseIgnore(?:Layer|Patterns)\s*\(' \
  src tests

Repository: itechmeat/open-second-brain

Length of output: 24117


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the ignore parser file and inspect the whitespace / matching logic around the claimed issue.
wc -l src/core/fs/ignore.ts tests/core/fs/ignore.test.ts
sed -n '1,240p' src/core/fs/ignore.ts
printf '\n--- tests/core/fs/ignore.test.ts ---\n'
sed -n '1,220p' tests/core/fs/ignore.test.ts

Repository: itechmeat/open-second-brain

Length of output: 14380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Build a small read-only probe that exercises the generated regexes on crafted glob bodies.
python3 - <<'PY'
import re, time

def escape_literal(ch: str) -> str:
    return re.sub(r'([\\.^$|?*+()\[\]{}])', r'\\\1', ch)

def translate_glob(body: str) -> str:
    re_src = ""
    n = len(body)
    i = 0
    while i < n:
        ch = body[i]
        if ch == "*":
            is_double = i + 1 < n and body[i + 1] == "*"
            if is_double:
                prev_is_boundary = i == 0 or body[i - 1] == "/"
                j = i
                while j < n and body[j] == "*":
                    j += 1
                next_is_boundary = j >= n or body[j] == "/"
                if prev_is_boundary and next_is_boundary:
                    if j < n and body[j] == "/":
                        re_src += "(?:[^/]+/)*"
                        i = j + 1
                    else:
                        re_src += ".*"
                        i = j
                else:
                    re_src += "[^/]*"
                    i = j
            else:
                re_src += "[^/]*"
                i += 1
        elif ch == "?":
            re_src += "[^/]"
            i += 1
        elif ch == "[":
            j = i + 1
            if j < n and body[j] in "!^":
                j += 1
            if j < n and body[j] == "]":
                j += 1
            while j < n and body[j] != "]":
                j += 1
            if j >= n:
                raise ValueError("unterminated character class")
            cls = body[i:j+1]
            if cls.startswith("[!"):
                cls = "[^" + cls[2:]
            re_src += cls
            i = j + 1
        else:
            re_src += escape_literal(ch)
            i += 1
    return re_src

def compile_pat(body: str, anchored=False):
    src = translate_glob(body)
    if anchored:
        return re.compile("^" + src + "$")
    return re.compile("^(?:.*/)?"+ src + "$")

cases = [
    "*a"*20,
    ("**/"*10) + "a",
    ("a*"*20) + "b",
    ("*/"*20) + "a",
]

for body in cases:
    r = compile_pat(body)
    path = "/".join(["x"*20]*8) + "/z"
    t0 = time.perf_counter()
    for _ in range(1000):
        r.fullmatch(path)
    dt = time.perf_counter() - t0
    print(body[:40], len(body), dt)
PY

Repository: itechmeat/open-second-brain

Length of output: 416


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the parser implementation and probe the two claims:
# 1) whether escaped trailing spaces are preserved correctly,
# 2) whether generated regexes show pathological growth on crafted inputs.

node - <<'JS'
const fs = require('fs');

const text = fs.readFileSync('src/core/fs/ignore.ts', 'utf8');
const lines = text.split('\n');
const slice = lines.slice(0, 220).map((l, i) => String(i + 1).padStart(4) + ': ' + l).join('\n');
console.log(slice.split('\n').slice(58, 156).join('\n'));

function escapeLiteral(ch) {
  return ch.replace(/[\\.^$|?*+()\[\]{}]/g, '\\$&');
}
function translateGlob(body) {
  let re = '';
  const n = body.length;
  let i = 0;
  while (i < n) {
    const ch = body[i];
    if (ch === '*') {
      const isDouble = body[i + 1] === '*';
      if (isDouble) {
        const prevIsBoundary = i === 0 || body[i - 1] === '/';
        let j = i;
        while (body[j] === '*') j++;
        const nextIsBoundary = j >= n || body[j] === '/';
        if (prevIsBoundary && nextIsBoundary) {
          if (body[j] === '/') {
            re += '(?:[^/]+/)*';
            i = j + 1;
          } else {
            re += '.*';
            i = j;
          }
        } else {
          re += '[^/]*';
          i = j;
        }
      } else {
        re += '[^/]*';
        i++;
      }
    } else if (ch === '?') {
      re += '[^/]';
      i++;
    } else if (ch === '[') {
      let j = i + 1;
      if (body[j] === '!' || body[j] === '^') j++;
      if (body[j] === ']') j++;
      while (j < n && body[j] !== ']') j++;
      if (j >= n) throw new Error('unterminated');
      let cls = body.slice(i, j + 1);
      if (cls.startsWith('[!')) cls = `[^${cls.slice(2)}`;
      re += cls;
      i = j + 1;
    } else {
      re += escapeLiteral(ch);
      i++;
    }
  }
  return re;
}

// Trailing escaped-space probe using the parser's trimming rules.
function normalizePattern(pattern) {
  let line = pattern;
  let escapedTrailingSpace = false;
  while (line.endsWith(' ') && !line.endsWith('\\ ')) {
    line = line.slice(0, -1);
  }
  if (line.endsWith('\\ ')) escapedTrailingSpace = true;
  return { line, escapedTrailingSpace };
}

for (const p of ['foo\\ ', 'foo ', 'foo\\\\ ']) {
  const norm = normalizePattern(p);
  const regexSrc = translateGlob(norm.line);
  console.log({p, norm, regexSrc});
}

// Backtracking probe.
const cases = ['*a'.repeat(8), 'a*'.repeat(8) + 'b', '**/*a'.repeat(5)];
for (const body of cases) {
  const source = '^(?:.*/)?' + translateGlob(body) + '$';
  const re = new RegExp(source);
  const input = 'x'.repeat(1000);
  const t0 = process.hrtime.bigint();
  for (let i = 0; i < 20000; i++) re.test(input);
  const dt = Number(process.hrtime.bigint() - t0) / 1e6;
  console.log({body, source, ms: dt.toFixed(2)});
}
JS

Repository: itechmeat/open-second-brain

Length of output: 4587


Avoid backtracking regexes for ignore globs. Repeated **/ and similar wildcard chains can make path scans CPU-heavy; a linear matcher or strict wildcard budget would avoid a config-driven DoS. Escaped trailing spaces also need to be unescaped before translation, since foo\ currently matches a literal backslash plus space instead of foo .

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/fs/ignore.ts` around lines 65 - 115, Update translateGlob to avoid
emitting backtracking-prone regexes for repeated **/ and wildcard chains, using
a linear-time matching strategy or enforcing a strict wildcard complexity budget
while preserving glob semantics. Also unescape escaped trailing spaces before
translation so patterns such as foo\  match foo  rather than a backslash
followed by a space.

Source: Linters/SAST tools

Comment thread src/core/fs/ignore.ts Outdated
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@solaitken

Copy link
Copy Markdown
Collaborator Author

CodeRabbit review disposition

Thanks for the review. Each finding was verified against the current code before acting. Summary below.

# Location Finding Disposition
1 .codex-plugin/plugin.json:3 Revert hand-edited version bump Skipped. The version is synced across every manifest via scripts/sync-version.ts, not hand-edited; per this repo's process the bump rides in the feature PR, and the version is out of scope for this pass.
2 src/cli/brain/verbs/doctor.ts, src/mcp/brain/health-tools.ts Require repair mode when apply is supplied Fixed. CLI rejects --apply without --repair via usageError (plain stderr, exit 2); MCP handler throws when apply is set without repair. Schema shape left unchanged to preserve the frozen MCP parity tests.
3 src/cli/brain/verbs/scan-citations.ts:59 Use malformedMarkers.length instead of malformed Skipped (stale). CitationScanResult does have a malformed: number field, set to malformedMarkers.length, so result.malformed is defined and the strict-exit logic works.
4 src/core/brain/diagnostics.ts:337 Lock the workrun before recheck and append Fixed. The wal-gap fixer now acquires the file lock before the idempotency recheck and holds it through the append, mirroring the orphaned-reference fixer.
5 src/core/brain/diagnostics.ts:454 Restrict deletion to the ## Origin section Fixed. removeOriginBullet now tracks section boundaries and only filters bullets inside ## Origin.
6 src/core/brain/diagnostics.ts:489 Do not suppress every broken-wikilink as fixer-covered Skipped. broken-wikilink is emitted only for the exact preference/retired evidence and structural fields the orphaned-reference fixer enumerates (surfaced as applied or needs-review); the non-Brain-link edge case is a heavy-lift refactor deferred for now.
7 src/core/brain/diagnostics.ts:571 Make repair and audit event failure-atomic Skipped (heavy lift). Cross-file transactional rollback for a rare appendLogEvent failure is out of scope for this review pass.
8 src/core/brain/ingest/batch-plan.ts:197 Remove duplicate stderr logging from core logic Fixed. The process.stderr.write loop is removed; skippedNonExtractable stays on the returned plan and the CLI caller presents it.
9 src/core/brain/ingest/extractable-gate.ts:87 O(N) synchronous file I/O Skipped (heavy lift). Post-classification gating changes reporting semantics; the gate is opt-in and inactive with an empty allowlist. Deferred.
10 src/core/brain/ingest/ingest.ts:210 Return an unextracted result for all unreadable sources Fixed. runPreExtract now wraps the read in try/catch so directories, permission failures, and deletion races return extracted: false instead of aborting.
11 src/core/brain/ingest/pre-extract.ts:219 Do not extract from comments or string literals Skipped (heavy lift). The deterministic pre-extraction is intentionally a coarse seed rather than a full parser; adding lexical state is deferred.
12 src/core/brain/ingest/reconcile.ts:57 Treat manifest skips and resume completions as ingested Fixed. Manifest-unchanged skips and checkpoint completions are now classified as ingested; missing is reserved for dispatched batch sources with neither confirmation. Added regressions for an all-unchanged plan and a fully-resumed plan.
13 src/core/brain/operator-snapshot.ts:109 Pass the resolved search DB path into runDoctor Fixed. The resolved dbPath is threaded into runDoctor so DB-backed findings are included.
14 src/core/brain/operator-snapshot.ts:123 Do not turn failed probes into an all-clear Fixed. Each probe catch now records a degraded problem, so a failed doctor, hygiene, stale, or review probe flips healthy to false.
15 src/core/brain/temporal/citations.ts:332 Make citation dedup atomic with event persistence Skipped (heavy lift). Shared-lock check-and-append plus JSONL reconciliation is a substantial concurrency refactor, deferred.
16 src/core/fs/ignore.ts:115 Avoid backtracking regexes; unescape trailing spaces Partially addressed. The escaped-trailing-space half is fixed (see #17). The backtracking-regex rewrite is skipped: ignore patterns come from the repo's own .gitignore and operator --exclude, not untrusted input.
17 src/core/fs/ignore.ts:132 Remove the escape before retained trailing whitespace Fixed. stripTrailingWhitespace now consumes the escaping backslash so foo\ matches the literal foo . Added a regression test.
18 src/core/hygiene/scan-repo.ts:220 (outside diff) Apply the ignore scope to root scan files too Fixed. Each SCAN_ROOT_FILES entry is now checked against the base ignore scope. Added a regression test for a gitignored root file.
19 src/cli/command-manifest.ts:198 (nitpick) Add scan-citations flags to the manifest Fixed.
20 src/cli/command-manifest.ts:134 (nitpick) Add status flags to the manifest Fixed.
21 src/core/brain/ingest/batch-plan.ts:315 (nitpick) Case-insensitive extension matching Fixed. extname is lowercased before the set lookup.

Validation: bun run fmt, bun run lint (134 warnings / 0 errors, baseline held), bun run typecheck, and the full bun test suite (6352 pass / 0 fail) all clean.

@solaitken
solaitken merged commit 4b8100c into main Jul 19, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants