Skip to content

feat(doctor): implement check_mandate_backend (#235)#296

Merged
dep0we merged 2 commits into
mainfrom
235-check-mandate-backend
May 29, 2026
Merged

feat(doctor): implement check_mandate_backend (#235)#296
dep0we merged 2 commits into
mainfrom
235-check-mandate-backend

Conversation

@dep0we
Copy link
Copy Markdown
Owner

@dep0we dep0we commented May 29, 2026

Summary

Closes #235. Implements doctor.check_mandate_backend — the documented-but-missing doctor coherence check that the #124 MandateBackend arc declared in CLAUDE.md but never shipped in atomic_agents/doctor.py. With this PR, CLAUDE.md's MandateBackend lock-paragraph claim of doctor.check_mandate_backend is true at runtime.

Two bisectable commits:

  1. 7e3def7feat(doctor): 245-line check_mandate_backend implementation + 5 tests + run_doctor orchestrator wiring + agent-name-None SKIP enumeration update.
  2. 4ddb83bdocs(spec/27): removes the "Implementation status (2026-05-28)" disclaimer added in PR docs: consolidated doc-debt sweep — spec/27 catalogue + 6-spec marker scrub + Protocol exception catalog #295 (it pointed at this issue; with the impl shipped, spec/27 describes shipped behavior again); adds a third WARN ladder bullet documenting the WARN-on-mandates.md-absent path that Round 1 adversarial caught as missing; CHANGELOG [Unreleased] Added bullet.

What landed

check_mandate_backend(scope_root: Path) -> CheckResult mirrors check_policy_backend / check_persona_backend with 5 documented divergences:

  • No cascade kwarg (Mandate's two-tier scope is handled inside the backend via scope: str).
  • Probe scope is "project:doctor" not "" (empirically: _parse_scope("") rejects; project kind discards name component per _mandates_path).
  • Probes list_mandates() in addition to capabilities() so the detail dict carries real mandate_count matching persona-backend's shape.
  • Single except Exception block in the filesystem branch (Round 1 collapsed the dual MandateError + Exception fork because filesystem exceptions do not embed credentials).
  • _redact_for_error_message imported from mandate.backend for the unknown-id FAIL credential-redaction path mirroring persona's import shape.

PASS / WARN / FAIL ladder (2 PASS + 2 FAIL + 2 WARN, matching spec/27 1:1):

  • PASS unset/filesystem with valid mandates.md → capability snapshot + mandate_count.
  • WARN unset/filesystem with mandates.md absent → "no operator-granted authorities at this scope" (informational; single-agent home users do not see a noisy FAIL).
  • FAIL unknown env var id (redacted via _redact_for_error_message).
  • FAIL non-filesystem id construction raises (verbatim exception dropped; credentials cannot leak).
  • WARN non-filesystem id reachable but capabilities() / list_mandates() probe raises.
  • Same mandates.md absent WARN reachable through the non-filesystem branch.

Test Coverage

5 new tests at tests/test_doctor_check_mandate_backend.py covering: WARN-no-mandates-md, PASS-with-mandates-md, FAIL-unknown-env-var (bare), FAIL-URL-credential-redaction, capability-snapshot-in-detail-dict invariant.

Test suite: 2686 → 2691 passing + 48 skipped, zero regressions across all four pytest runs (pre-impl, post-impl, post-Round-1 fixes, post-CHANGELOG).

ruff check atomic_agents/doctor.py + ruff format --check atomic_agents/doctor.py: both clean.

Methodology trail

  • Pre-impl prep + dispatch: read check_policy_backend end-to-end as the canonical analog, verified MandateCapabilities field names from source (mandate/types.py:316-337), verified MandateBackend Protocol method signatures (mandate/backend.py:135,388,354,379), then briefed one Sonnet implementation agent with the closest-analog cite + verified field/method names + test pattern. The Sonnet agent surfaced 5 documented divergences from the brief during implementation (notably the "project:doctor" probe scope choice after empirically discovering _parse_scope("") rejects empty input).

  • Step 11 Opus adversarial Round 1: 0 P0 + 1 P1 + 2 P2.

    • P1: spec/27's mandate-backend entry didn't document the WARN-on-mandates.md-absent path even though the impl emits it. The PR's central purpose is closing the spec/27 documentation gap; missing a documented WARN path was a real gap. Added a third WARN bullet to the ladder.
    • P2 [v0.2] Eval runner — atomic_agents.eval #1: Dual except MandateError + except Exception block in the filesystem branch was Bucket-C noise. Sister checks (persona, tool-registry, profile) all use a single typed-or-generic except. Filesystem backends don't embed credentials in exception messages, so the discrimination gained nothing safety-wise. Collapsed into single except Exception and removed the now-unused MandateError import.
    • P2 [v0.3] Tuning analyzer — atomic_agents.tuning #2: Docstring spec cite at line 1694 attributed the "project-kind scope discards name component" behavior to spec/29 §"Resolution rules" — but that section describes disjoint-ID resolution between project-root and per-agent files, not name-component handling. The actual behavior lives in mandate/filesystem.py::_mandates_path. Swapped the cite to the file::function form so a future-Claude lands in the implementing function in one hop.
  • Step 11 Opus adversarial Round 2: 0 P0 + 0 P1 + 1 P2.

    • All 3 Round 1 fixes verified correct.
    • P2: CHANGELOG [Unreleased] was missing an Added entry. Per CLAUDE.md memory feedback_changelog_doc_shipping ("every shipped change goes in the CHANGELOG, docs included"), this PR ships a new public-API surface (atomic_agents.doctor.check_mandate_backend) — operators running atomic-agents doctor will see a new check fire that didn't fire in the prior release. Added the bullet in the docs(spec/27) sibling commit.
  • Round 2 closing rec: "fix and ship; no Round 3 required" matching the project's 2-round convergence pattern (PR 2 + PR 3 + PR 4 of [backend] PersonaBackend — load IDENTITY/SOUL/USER from a registry instead of fixed markdown files #62 + PR docs: consolidated doc-debt sweep — spec/27 catalogue + 6-spec marker scrub + Protocol exception catalog #295 all converged at Round 2).

  • Codex skipped per standing project rule.

Plan Completion

  • ✅ Implement check_mandate_backend in atomic_agents/doctor.py
  • ✅ Add 5+ tests covering PASS / WARN / FAIL paths + credential-redaction invariant
  • ✅ Wire check_mandate_backend into run_doctor orchestrator
  • ✅ Add "mandate-backend" to the agent-name-None SKIP enumeration
  • ✅ Remove the spec/27 "Implementation status (2026-05-28)" disclaimer
  • ✅ Add the missing WARN-on-mandates.md-absent bullet to spec/27 (Round 1 P1 fold)
  • ✅ Collapse the dual except handler + remove unused MandateError import (Round 1 P2 fold)
  • ✅ Swap the docstring spec cite to point at the implementing function (Round 1 P2 fold)
  • ✅ Add CHANGELOG [Unreleased] Added bullet (Round 2 P2 fold)

Test plan

  • uv run pytest -q: 2691 passing + 48 skipped, zero regressions
  • ruff check + ruff format --check clean on atomic_agents/doctor.py
  • from atomic_agents.doctor import check_mandate_backend succeeds
  • grep -c "Implementation status" docs/spec/27-doctor.md returns 0
  • check_mandate_backend exists in doctor.py (verifies the fix to the issue title)
  • spec/27 mandate-backend entry's 6-bullet PASS/WARN/FAIL ladder matches the impl's 6 distinct CheckResult emit paths 1:1
  • CLAUDE.md MandateBackend lock-paragraph claim of doctor.check_mandate_backend coherence check is now true at runtime

🤖 Generated with Claude Code

Dan Powers and others added 2 commits May 29, 2026 07:51
Closes the documented-but-unimplemented doctor check that the #124
MandateBackend arc declared in CLAUDE.md but never shipped in
atomic_agents/doctor.py. The new scope-scoped Protocol-coherence check
mirrors check_policy_backend and check_persona_backend with five
documented divergences from the closest sister (check_policy_backend):

- No cascade kwarg. Mandate's two-tier scope (project-level
  <scope_root>/mandates.md vs per-agent <scope_root>/<agent>/mandates.md)
  is handled inside the backend via the scope: str parameter; the
  doctor only probes project scope. Policy's cascade-aware scope
  warning at #236 is policy-specific.

- Probe scope is "project:doctor" rather than the empty string.
  Empirically, _parse_scope("") rejects empty input; the project: kind
  discards the name component in _mandates_path so any non-empty
  "project:<name>" reads <scope_root>/mandates.md regardless of name.

- Probe calls list_mandates() in addition to capabilities(). Policy's
  Protocol has no list method so it only probes capabilities; Mandate
  has list_mandates() so the probe surfaces a real mandate_count in
  the detail dict matching check_persona_backend's shape.

- Single except Exception block in the filesystem branch. Sister checks
  (persona, tool-registry, profile) all use a single typed-or-generic
  except. The dual MandateError / Exception split surfaced in Round 1
  adversarial review as Bucket-C noise with no safety gain (filesystem
  backends do not embed URL credentials in their exception messages).

- _redact_for_error_message imported from mandate.backend for the
  unknown-id FAIL credential-redaction path, mirroring persona's
  import shape.

PASS / WARN / FAIL ladder:

- PASS when ATOMIC_AGENTS_MANDATE_BACKEND is unset or "filesystem" and
  the default backend constructs + capabilities() + list_mandates()
  return cleanly with mandates.md present. Detail carries the full
  capability snapshot plus mandate_count + mandates_md_exists +
  resolved_path.

- WARN when construction succeeds but mandates.md is absent at
  scope_root — no operator-granted authorities at this scope.
  Informational so single-agent home users don't see a noisy FAIL.

- FAIL when ATOMIC_AGENTS_MANDATE_BACKEND is set to an id not in
  list_mandate_backends(). The echoed env value is redacted at ://
  via mandate.backend._redact_for_error_message so an operator who
  accidentally pasted a credential-bearing URL into the id env var
  doesn't leak through doctor output.

- FAIL when the registered backend's factory raises during
  construction (credentials dropped from the surfaced exception text;
  the verbatim exception is not included in either message or
  fix_hint).

- WARN when construction succeeds but capabilities() or
  list_mandates() raises — backend reachable but probe surface
  degraded. Matches the sister WARN-on-unreachable-probe pattern.

Adds 5 tests at tests/test_doctor_check_mandate_backend.py:

- test_check_mandate_backend_passes_with_no_mandates_md (WARN-no-md)
- test_check_mandate_backend_passes_with_mandates_md (PASS path)
- test_check_mandate_backend_fails_on_unknown_env_var
- test_check_mandate_backend_fails_on_unknown_env_var_url_credential_redaction
- test_check_mandate_backend_capability_snapshot_in_detail

Wires check_mandate_backend(resolved_root) into run_doctor at the
position matching spec/27 catalogue order (between
check_tool_registry_backend and check_policy_backend). Adds
"mandate-backend" to the agent-name-None SKIP enumeration so the new
scope-scoped check runs in --scope-only mode alongside its sisters.

Test suite: 2686 → 2691 passing + 48 skipped. Zero regressions on the
166 existing AtomicAgent(...) construction sites. ruff check + ruff
format --check both clean on atomic_agents/doctor.py.

Methodology: pre-impl prep dispatched one Sonnet implementation agent
with verified MandateCapabilities field names + closest-analog cite +
test pattern reference. Step 11 Opus adversarial 2 rounds: Round 1
caught 1 P1 (spec/27 missing WARN-on-md-absent bullet) + 2 P2 (dual
except redundant + docstring cite to wrong spec section); all folded.
Round 2 verified the 3 fixes correct + caught 1 P2 (CHANGELOG missing
entry, addressed in the sibling commit). Round 2 closing rec: "fix
and ship; no Round 3 required" matching the project's 2-round
convergence pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…aimer + CHANGELOG arc-close (#235)

The disclaimer at docs/spec/27-doctor.md:386-393 was added in PR #295
(consolidated doc-debt sweep) pointing at this issue because
check_mandate_backend was documented in the spec but missing from
atomic_agents/doctor.py. With the impl now landed in the sibling
commit, the disclaimer is removed; spec/27 describes shipped behavior
again.

Also adds a third WARN ladder bullet to the spec/27 mandate-backend
entry documenting the WARN-on-mandates.md-absent path. Round 1 Opus
adversarial caught the gap: the impl emits this WARN but spec/27 only
documented the capabilities()-probe-failure WARN. Spec/27's PASS / WARN
/ FAIL ladder now matches the impl 1:1 across all 6 paths (2 PASS + 2
FAIL + 2 WARN), mirroring policy-backend's analogous bullet for the
policy.md absent case.

CHANGELOG [Unreleased] gains an Added bullet covering the impl, the
spec/27 disclaimer removal, the test additions, and the methodology
trail (Step 11 Opus adversarial 2 rounds converging at "fix and ship").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dep0we dep0we merged commit 8658085 into main May 29, 2026
5 checks passed
@dep0we dep0we deleted the 235-check-mandate-backend branch May 29, 2026 12:53
dep0we added a commit that referenced this pull request Jun 1, 2026
…RON RULE regression suite (#304)

* feat(corpus): #65 PR 3 of 4. env-var resolution + CLI env-var swap

corpus/__init__.py: add sqlite branch to get_default_corpus_backend so
ATOMIC_AGENTS_CORPUS_BACKEND=sqlite resolves to SQLiteCorpusBackend with
default db at <agent_root>/.corpus.db, agent_scope=<agent_root.name>.
Mirrors profile/__init__.py:227-235 precedent. Empty-string env var
normalizes to filesystem. Wraps sqlite construction in (OSError,
PermissionError) for clean operator-facing error.

cli.py: replace hardcoded FilesystemCorpusBackend(agent_root) in
_cmd_corpus with get_default_corpus_backend(agent_root) so CLI honors
ATOMIC_AGENTS_CORPUS_BACKEND env var (closes silent CLI-vs-runtime drift).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(corpus): #65 PR 3 of 4. agent.py wiring (flag + delegate + OSError catch)

Constructor adds corpus_backend kwarg + class-level annotation. Resolves
via get_default_corpus_backend(self.agent_root) when not supplied. Mirrors
PersonaBackend D-ER-2 explicit-only threading: _corpus_backend_was_explicit
flag saved to self, consumed at delegate() for conditional kwarg insertion.

_load_indexes() at agent.py:2933-2985 routes wiki/INDEX.md read through
CorpusBackend Protocol when configured. Legacy direct-read fallback now
catches OSError and returns empty string with a logged warning marker
wiki_index_unreadable, matching FilesystemCorpusBackend.render_index_summary
behavior at corpus/filesystem.py:701-702. Brings both code paths into
behavioral agreement so the IRON RULE byte-identity assertion holds.

NOTE: legacy direct-read previously propagated OSError. This is an
intentional behavior change. Operators with a wiki/INDEX.md that becomes
briefly unreadable now see a logged warning and an empty wiki section
rather than a hard crash at agent construction.

delegate() at agent.py:4628-4629 threads corpus_backend ONLY when the
operator supplied it explicitly. Default-resolved backends do not leak
the coordinator's content_root to delegates (corpus is per-agent
semantic context, not fleet-scoped).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(corpus): #65 PR 3 of 4. bundle.py 3-level threading + byte-identity helper

render_bundle, _render_sections, and _render_memory_breakpoint all gain
corpus_backend: CorpusBackend | None = None parameter, threaded through
all three call levels. When corpus_backend is None at any caller, the
fallback path uses the legacy direct file read.

New private helper _render_wiki_index_section(label, path, content)
produces the bundle section in the canonical
## {label}\n\`{path}\`\n\n{content} format used by _render_file_section.
Both the corpus_backend Protocol path AND the legacy fallback path call
this helper with the same logical wiki path so byte-identical output
is guaranteed regardless of which path produced the content. Closes
the IRON RULE assertion 4 risk.

Both branches apply .strip() to match _render_file_section's
_safe_read_text(...).strip() behavior. Skip the section when content
is empty (no file or empty file).

_source_paths at bundle.py:266 gets a TODO(v1.1) comment noting the
deferred Protocol routing (filesystem-only function; SQLite has no
equivalent path to track). Follow-up issue filed at PR 4.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(corpus): #65 PR 3 of 4. per-runner corpus_backend kwargs

OutcomeRunner (outcome.py:255) and EvalRunner (eval.py:363) each accept
corpus_backend: CorpusBackend | None = None and thread it to their
internal AtomicAgent construction site. Mirrors the per-runner kwarg
shape locked at #63 PR 2 (AgentProfileBackend) and #62 PR 2
(PersonaBackend).

DreamRunner accepts the kwarg for API parity but does NOT thread it
to any internal AtomicAgent construction site (none exists in v1).
Stored as self._corpus_backend with a comment matching the existing
DreamRunner pattern for the other 4 backend kwargs (_policy_backend,
_persona_backend), documenting the future-state threading site.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(corpus): #65 PR 3 of 4. doctor.check_corpus_backend PASS/WARN/FAIL ladder

12th check_*_backend implementation in doctor.py. Mirrors check_mandate_backend
shape (the most recently merged precedent from #296).

PASS: backend constructs successfully + stats("wiki") and stats("raw")
both return without raising. Capability snapshot in detail dict
(backend_id, supports_full_text_search, supports_semantic_search,
supports_versioning, embedding_provider, wiki_page_count, raw_page_count).

WARN: 1. supports_full_text_search=False AND wiki_page_count > 1000 OR
raw_page_count > 1000 (the page-count cliff WARN per
/plan-eng-review 2026-05-29 finding P1). Hint names ATOMIC_AGENTS_CORPUS_BACKEND=sqlite
as the remedy.

WARN: 2. ATOMIC_AGENTS_CORPUS_BACKEND_URL set but
ATOMIC_AGENTS_CORPUS_BACKEND not. URL silently ignored otherwise; surface
this misconfiguration explicitly.

FAIL: backend cannot be constructed, OR stats() raises. URL credential
redaction via existing _redact_for_error_message helper used by the
other doctor checks.

Probes BOTH wiki and raw corpora (the page-count cliff WARN fires if
either exceeds the threshold). Registered in run_all_checks dispatch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(corpus): #65 PR 3 of 4. IRON RULE regression suite + wiring + doctor

35 net new tests + 2 augmented existing integration tests.

NEW tests/test_corpus_composition.py (4 tests): _corpus_backend_was_explicit
flag tracking + delegate explicit-only threading.

NEW tests/test_corpus_migration_regression.py (5 tests): the IRON RULE
suite. Assertions 1-4 verify byte-identity between corpus_backend=None
fallback path and corpus_backend=FilesystemCorpusBackend Protocol path
at both agent.py:_load_indexes and bundle.py:_render_memory_breakpoint
call sites. Assertion 5 (full pre-#65 suite passes unchanged) is a CI
criterion documented in the PR body. Plus 1 OSError catch test
exercising the new legacy-path soft-degrade behavior.

NEW tests/test_corpus_wiring.py (13 tests): env var resolution
(filesystem default, sqlite default, URL override, empty-URL fallback,
empty-backend-treated-as-unset, whitespace padding, filesystem URL,
agent_root empty name guard) + per-runner kwarg storage (Outcome,
Eval, Dream) + OutcomeRunner threading + CLI env-var activation.

NEW tests/test_corpus_doctor.py (11 tests): PASS/WARN/FAIL ladder
discrimination across capability conditions + page-count cliff WARN on
both wiki and raw corpora + URL-without-backend WARN + construction-fail
FAIL + unwritable-path FAIL + capability snapshot completeness + URL
credential redaction + run_all_checks integration.

AUGMENTED tests/test_agent_cascade_integration.py: _build_full_cascade_layout
fixture writes real wiki/INDEX.md content + assert wiki section header +
body content + section ordering after memory INDEX. Closes silent-corruption
risk class flagged by /plan-subagent S4 (9 wiki-touching tests created
empty wiki dirs with ZERO INDEX content assertions).

AUGMENTED tests/test_cascade_bundle.py: end-to-end render_bundle threading
test asserting byte-identity between Protocol path and fallback path +
_source_paths v1.1 deferral guard test (pins the deferral decision
mechanically; a future premature Protocol-routing of _source_paths would
fail this test).

Total suite: 2853 -> 2888 passing + 48 skipped. Zero regressions to the
pre-#65 surface.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(corpus): #65 PR 3 of 4. Round 1 adversarial review fixes + CHANGELOG

Round 1 adversarial review (Claude subagent + pre-landing review) caught
8 high-confidence findings + 2 INFORMATIONAL pre-landing findings. All
addressed in this commit. Round 2 + Round 3 to follow under /ship.

corpus/__init__.py SQLite branch:
- URL-encode agent_root.name via quote_plus so names containing URL
  metacharacters (spaces, +, &, ?, =) do not silently corrupt agent_scope
  or raise ValueError. A name like "my+agent" decoded as "my agent" via
  parse_qsl, causing cross-scope contamination with a real agent named
  "my agent".
- Widen the construction try/except from (OSError, PermissionError) to
  Exception. Re-raise as CorpusBackendNotRegistered with the URL remedy.
  Covers ValueError (malformed URL, invalid charset) and
  sqlite3.OperationalError (db locked at cold start, WAL transition
  failure on NFS) that previously escaped as raw library exceptions.
  PermissionError is redundant (subclass of OSError) so it drops.

corpus/filesystem.py render_index_summary:
- Add UnicodeDecodeError to the except clause. Pre-PR-3 bundle.py used
  _safe_read_text which catches UnicodeDecodeError; the Protocol path
  did not. A wiki/INDEX.md with non-UTF-8 bytes (Latin-1, BOM, mixed
  encodings) would crash agent construction via the Protocol path
  where the legacy bundle path gracefully degraded. Now at parity.

agent.py _load_indexes:
- Add broad try/except around the Protocol path call to
  corpus_backend.render_index_summary. Soft-degrade to empty string
  with a logged wiki_index_unreadable warning so any custom-backend
  exception (sqlite3.OperationalError, CorpusError, KeyError) does
  not crash agent construction. Matches the legacy direct-read soft
  degrade behavior.
- Update comment on the legacy direct-read else-branch: noting it is
  unreachable in production after Stream B's default-resolution at
  __init__ (self.corpus_backend is always non-None). Retained as a
  safety net for future refactors that remove the auto-resolve.
  Exercised by tests in test_corpus_migration_regression.py that
  force corpus_backend=None post-construction.

doctor.py check_corpus_backend:
- Rewrite the URL-without-backend WARN message. Pre-fix message said
  "the URL is being ignored" which was factually wrong: when backend
  unset and URL set, get_default_corpus_backend normalizes to
  filesystem and routes the URL through
  make_filesystem_corpus_backend_from_url. The URL is USED. New message
  describes the implicit-default state and recommends explicit binding.
- Update the docstring at line 2438-2447 to match (drop the misleading
  "or resolves to filesystem" qualifier).
- Replace bare assert wiki_stats is not None / assert raw_stats is not
  None at line 2583-2584 with a defensive conditional that returns
  CheckResult(status=FAIL) on the logically unreachable None state.
  Preserves the always-returns-CheckResult contract under python -O
  optimized builds.

tests/test_corpus_doctor.py:
- Update test_check_corpus_backend_warns_url_without_backend_id to
  assert on stable substrings rather than the verbatim previous message
  text. Matches the new WARN wording.

CHANGELOG.md:
- Add two PR 3 entries to [Unreleased] section: main PR 3 wiring entry
  + Round 1 adversarial fix entry, plus two Changed entries documenting
  the agent.py legacy-path OSError catch behavior change and the
  cli.py CLI env-var honoring behavior change.

Full pytest suite: 2853 -> 2888 passing, 48 skipped, zero regressions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(corpus): #65 PR 3 of 4. Round 2 adversarial review fixes

Round 2 hunted in the Round 1 fix commit (ad13220) per CLAUDE.md rule 11
("each fix changes the diff and exposes new edges"). Caught 3 MEDIUM +
4 LOW findings introduced by Round 1. F3, F4, F5 applied as FIXABLE +
F6 closed as a coverage gap. F1, F2, F7 defended as trade-off calls.

R2-F3 (FIXABLE, was MEDIUM):
- corpus/filesystem.py render_index_summary returned "" on
  UnicodeDecodeError, silently losing wiki body content where the
  pre-PR-3 bundle.py _safe_read_text preserved partial content. Round 1
  CHANGELOG claimed "matches the pre-#65 behavior" but the code did NOT.
- Rewrite to match _safe_read_text exactly: re-read with
  errors="replace" + prepend the same warning comment shape used by
  _safe_read_text. The Protocol and legacy paths now produce truly
  symmetric output for the Unicode case.
- Split the except clause into separate UnicodeDecodeError and OSError
  branches because they have different soft-degrade behavior
  (UnicodeDecodeError has partial content available, OSError does not).

R2-F4 (FIXABLE, was LOW):
- doctor.py:2476-2481 comment still said the URL was "silently ignored"
  after Round 1 fixed that. Updated to describe the post-fix behavior
  accurately: URL is honored via the filesystem factory; binding is
  implicit; the WARN surfaces the implicit-default state.

R2-F5 (FIXABLE, was LOW):
- doctor.py:2588-2598 defensive-conditional FAIL detail dict carried
  only backend_id, dropping the capability snapshot fields already
  available in caps. Operators debugging the (logically-unreachable)
  None state had no context.
- Expand the dict to include supports_full_text_search,
  supports_semantic_search, supports_versioning, embedding_provider.

R2-F6 (INVESTIGATE -> closed via new test):
- No test exercised the Protocol-path except Exception branch added
  in Round 1; only the legacy-path OSError catch had a test.
- Add test_agent_load_indexes_protocol_path_exception_soft_degrades to
  tests/test_corpus_migration_regression.py. Uses a _RaisingCorpusBackend
  stub whose render_index_summary raises sqlite3.OperationalError.
  Verifies _wiki_index_text == "" + log marker + backend-class-name
  in the warning message.

Defended:
- F1 (broad except in corpus/__init__.py misdirects with sqlite-URL hint
  on non-storage errors): cause type is included in error message so
  developers can debug; the production stability trade-off is the right
  default.
- F2 (Protocol-path broad except silently degrades on programmer errors
  like AttributeError): same trade-off; the logged wiki_index_unreadable
  warning is observable; strict-fail behavior is a follow-up env var
  (ATOMIC_AGENTS_CORPUS_STRICT) for a future PR.
- F7 (sqlite-specific URL remedy in error message): scoped correctly
  inside the sqlite branch only.

CHANGELOG updated with the Round 2 fix bullet under [Unreleased].

Test suite: 2888 -> 2889 passing + 48 skipped, zero regressions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs(corpus): #65 PR 3 of 4. Round 3 R3-F1 documenting comment

Round 3 R3-F1 (LOW, 7/10): the legacy direct-read else-branch in
_load_indexes catches OSError but not UnicodeDecodeError. The branch is
unreachable in production after PR 3 default-resolution, so adding a
catch would be dead code. Document the gap in the comment instead so
a future contributor reactivating the branch knows to mirror the
Protocol path's partial-content soft-degrade.

Round 3 R3-F2 (LOW, 8/10) and R3-F3 (LOW, 6/10) are coverage gaps
accepted as follow-up backlog candidates (UnicodeDecodeError branch in
render_index_summary lacks a direct test; defensive-FAIL detail dict
path remains logically unreachable). Neither blocks PR 3 merge.

Round 3 convergence shape: 3 LOW + zero CRITICAL/HIGH/MEDIUM. Matches
PR 2 of #65 precedent (PR 2 converged at Round 3 with 5 LOW = zero
higher tiers). PR 3 ready for merge per CLAUDE.md rule 11 ("2-3 rounds
is sufficient for most diffs").

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs(corpus): #65 PR 3 of 4. spec/34 PR 3 status + spec/27 corpus-backend stub

spec/34 inline note: PR 3 wiring IMPLEMENTED. Does NOT drop the RFC
banner or finalize the N-MUST Implementer Contract -- both PR 4 work.

spec/27 corpus-backend entry: 12th check_*_backend doctor entry. PASS/
WARN/FAIL ladder, capability snapshot, page-count cliff WARN at ~1000
pages for filesystem backends without supports_full_text_search.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Dan Powers <dep0we@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
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.

[backend] doctor.py is missing check_mandate_backend (regression from #124 arc)

1 participant