diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index bbff494..2ec7aaa 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -23,7 +23,7 @@ "name": "diataxis-docs", "repository": "https://github.com/Statusnone420/Skills", "source": "./adapters/claude", - "version": "0.1.5" + "version": "0.1.6" } ] } diff --git a/.gitignore b/.gitignore index bc95433..f5c1220 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ __pycache__/ .venv/ evals/workspace/ evals/results/drafts/ +/showcase diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 5239962..9643e6f 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -8,7 +8,7 @@ The canonical source is `skills/docs/SKILL.md`, routed to `references/commands.m Discovery owns bounded metadata and selection policy; `init_adoption.py` owns the deterministic, engine-owned first-run adoption (scope selection, corpus accounting, preview construction, and receipt binding); `paths.py` owns confinement, normalization, reparse checks, and prune primitives; `memory.py` is read-only inspection; `lifecycle.py` owns pure authorization/state policy; and `lifecycle_io.py` owns transactional filesystem I/O. The dependency direction is one-way and the CLI façade stays thin. The committed `.diataxis/` control plane stores normalized routes, stable findings, verified hashes, disposition identity, and event history—never document bodies, prompts, hidden reasoning, or local-only filenames. -Every generated adapter packages the complete canonical checker resource tree. Generic web prompts are composed per command from a shared safety core, one selected command contract, and required supporting rules; they do not concatenate the entire playbook. The 0.1.4 historical range was 3,484–24,679 UTF-8 bytes. The measured 0.1.5 range is 3,460–31,966 bytes, with a 42,000-byte generator regression guard and 10,034 bytes of headroom. +Every generated adapter packages the complete canonical checker resource tree. Generic web prompts are composed per command from a shared safety core, one selected command contract, and required supporting rules; they do not concatenate the entire playbook. The 0.1.4 historical range was 3,484–24,679 UTF-8 bytes. The measured 0.1.6 range is 3,460–31,966 bytes, with a 42,000-byte generator regression guard and 10,034 bytes of headroom. The repository Codex marketplace lives at `.agents/plugins/marketplace.json` and routes to the generated `plugins/diataxis-docs` package. Its folder, marketplace entry, and `.codex-plugin/plugin.json` all use the same `diataxis-docs` identity. The package contains the shared `docs` engine plus 13 thin `docs-` routing skills; focused skills do not copy the checker or command playbooks. Claude receives equivalent thin skills with its explicit-only invocation metadata. The umbrella remains compatible on both hosts. diff --git a/CHANGELOG.md b/CHANGELOG.md index c1b5367..bd98ee2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.1.6 — Empty-state recovery repair (2026-07-20) + +- Let Init discovery and Doctor's zero-write baseline proceed without false missing-state findings when `.diataxis/` is empty or contains only an empty `manifests/` container left by an interrupted or branch-switched initialization. +- Applied the same bounded residue rule during approval-time revalidation so a successful Init preview can complete its approved apply. +- Kept unknown control children, non-empty manifests, recovery artifacts, symlinks, reparse points, and malformed operational state fail-closed for Doctor diagnosis. +- Added public-command regressions for Init discovery, Doctor baseline measurement, full preview/apply adoption, and unsafe-residue rejection. + ## 0.1.5 — Native command skills and first-run Doctor (2026-07-18) - Added a genuine Codex repository marketplace package with aligned `diataxis-docs` marketplace, folder, and manifest identities. diff --git a/INSTALL.md b/INSTALL.md index ead5a8a..ad014ca 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -4,7 +4,7 @@ Diátaxis Docs is a public alpha. Install from a revision you trust, review prop ## Codex marketplace -Diátaxis Docs 0.1.5 publishes as a Codex repository marketplace plugin. Add the marketplace, install the plugin, and verify that Codex reports it: +Diátaxis Docs 0.1.6 publishes as a Codex repository marketplace plugin. Add the marketplace, install the plugin, and verify that Codex reports it: ```text codex plugin marketplace add Statusnone420/Skills diff --git a/adapters/claude/.claude-plugin/plugin.json b/adapters/claude/.claude-plugin/plugin.json index aff7ce8..e60b359 100644 --- a/adapters/claude/.claude-plugin/plugin.json +++ b/adapters/claude/.claude-plugin/plugin.json @@ -13,5 +13,5 @@ "license": "Apache-2.0", "name": "diataxis-docs", "repository": "https://github.com/Statusnone420/Skills", - "version": "0.1.5" + "version": "0.1.6" } diff --git a/adapters/claude/skills/docs/SKILL.md b/adapters/claude/skills/docs/SKILL.md index 98d6a6a..baed2ba 100644 --- a/adapters/claude/skills/docs/SKILL.md +++ b/adapters/claude/skills/docs/SKILL.md @@ -3,7 +3,7 @@ name: docs description: Use when a user explicitly invokes repository documentation help for bounded Diátaxis writing, context recall, mapping, auditing, checking, migration, cleanup, Doctor guidance, or evidence-backed updates. metadata: author: Statusnone - version: "0.1.5" + version: "0.1.6" user-invocable: true disable-model-invocation: true --- diff --git a/adapters/claude/skills/docs/scripts/_docs_checker/init_adoption.py b/adapters/claude/skills/docs/scripts/_docs_checker/init_adoption.py index dadf9dd..58d79c6 100644 --- a/adapters/claude/skills/docs/scripts/_docs_checker/init_adoption.py +++ b/adapters/claude/skills/docs/scripts/_docs_checker/init_adoption.py @@ -23,7 +23,7 @@ from .scan import discover_markdown, scan_documents -SKILL_VERSION = "0.1.5" +SKILL_VERSION = "0.1.6" def canonical_request_bytes(value): diff --git a/adapters/claude/skills/docs/scripts/_docs_checker/init_closeout.py b/adapters/claude/skills/docs/scripts/_docs_checker/init_closeout.py index 8b68664..d35f968 100644 --- a/adapters/claude/skills/docs/scripts/_docs_checker/init_closeout.py +++ b/adapters/claude/skills/docs/scripts/_docs_checker/init_closeout.py @@ -41,6 +41,7 @@ MAX_FINDINGS_BYTES, STATE_DIRECTORY, STATE_SCHEMA_VERSION, + _is_benign_initialization_residue, build_initialization_state, inspect_operational_memory, load_operational_events, @@ -1270,11 +1271,14 @@ def inspect_initialization_preflight(root, *, control_present=False): incomplete or conflicting control plane fails closed to Doctor. """ root = Path(root).absolute() - if not control_present and not os.path.lexists(root / STATE_DIRECTORY): + control = root / STATE_DIRECTORY + if not control_present and not os.path.lexists(control): return None state = None try: + if _is_benign_initialization_residue(root): + return None memory_findings = inspect_operational_memory( root, inspect_protected_intent=False ) diff --git a/adapters/claude/skills/docs/scripts/_docs_checker/memory.py b/adapters/claude/skills/docs/scripts/_docs_checker/memory.py index 6f48e3a..f707993 100644 --- a/adapters/claude/skills/docs/scripts/_docs_checker/memory.py +++ b/adapters/claude/skills/docs/scripts/_docs_checker/memory.py @@ -25,6 +25,7 @@ ) from .paths import ( _assert_no_reparse_components, + _is_reparse, normalize_repo_relative, safe_path, shared_text_exposes_route, @@ -172,6 +173,32 @@ def _operational_control(root): return control +def _benign_initialization_residue(control): + """Return whether control contains only bounded empty Init containers.""" + if _is_reparse(control) or not control.is_dir(): + return False + with os.scandir(control) as entries: + first = next(entries, None) + if first is None: + return True + if next(entries, None) is not None: + return False + manifests = Path(first.path) + if ( + first.name != "manifests" + or _is_reparse(manifests) + or not first.is_dir(follow_symlinks=False) + ): + return False + with os.scandir(manifests) as entries: + return next(entries, None) is None + + +def _is_benign_initialization_residue(root): + control = _operational_control(root) + return control is not None and _benign_initialization_residue(control) + + def _operational_file(root, filename): control = _operational_control(root) if control is None: @@ -2137,6 +2164,18 @@ def inspect_operational_memory(root, *, inspect_protected_intent=True): ] if control is None: return [] + try: + if _benign_initialization_residue(control): + return [] + except OSError as exc: + return [ + _memory_finding( + "state-conflict", + "P0", + STATE_DIRECTORY, + _sanitized_memory_detail(exc, "operational control entries"), + ) + ] findings, observed_control = _inspect_control_plane_files(control) state = None diff --git a/adapters/copilot/SKILL.md b/adapters/copilot/SKILL.md index 98d6a6a..baed2ba 100644 --- a/adapters/copilot/SKILL.md +++ b/adapters/copilot/SKILL.md @@ -3,7 +3,7 @@ name: docs description: Use when a user explicitly invokes repository documentation help for bounded Diátaxis writing, context recall, mapping, auditing, checking, migration, cleanup, Doctor guidance, or evidence-backed updates. metadata: author: Statusnone - version: "0.1.5" + version: "0.1.6" user-invocable: true disable-model-invocation: true --- diff --git a/adapters/copilot/scripts/_docs_checker/init_adoption.py b/adapters/copilot/scripts/_docs_checker/init_adoption.py index dadf9dd..58d79c6 100644 --- a/adapters/copilot/scripts/_docs_checker/init_adoption.py +++ b/adapters/copilot/scripts/_docs_checker/init_adoption.py @@ -23,7 +23,7 @@ from .scan import discover_markdown, scan_documents -SKILL_VERSION = "0.1.5" +SKILL_VERSION = "0.1.6" def canonical_request_bytes(value): diff --git a/adapters/copilot/scripts/_docs_checker/init_closeout.py b/adapters/copilot/scripts/_docs_checker/init_closeout.py index 8b68664..d35f968 100644 --- a/adapters/copilot/scripts/_docs_checker/init_closeout.py +++ b/adapters/copilot/scripts/_docs_checker/init_closeout.py @@ -41,6 +41,7 @@ MAX_FINDINGS_BYTES, STATE_DIRECTORY, STATE_SCHEMA_VERSION, + _is_benign_initialization_residue, build_initialization_state, inspect_operational_memory, load_operational_events, @@ -1270,11 +1271,14 @@ def inspect_initialization_preflight(root, *, control_present=False): incomplete or conflicting control plane fails closed to Doctor. """ root = Path(root).absolute() - if not control_present and not os.path.lexists(root / STATE_DIRECTORY): + control = root / STATE_DIRECTORY + if not control_present and not os.path.lexists(control): return None state = None try: + if _is_benign_initialization_residue(root): + return None memory_findings = inspect_operational_memory( root, inspect_protected_intent=False ) diff --git a/adapters/copilot/scripts/_docs_checker/memory.py b/adapters/copilot/scripts/_docs_checker/memory.py index 6f48e3a..f707993 100644 --- a/adapters/copilot/scripts/_docs_checker/memory.py +++ b/adapters/copilot/scripts/_docs_checker/memory.py @@ -25,6 +25,7 @@ ) from .paths import ( _assert_no_reparse_components, + _is_reparse, normalize_repo_relative, safe_path, shared_text_exposes_route, @@ -172,6 +173,32 @@ def _operational_control(root): return control +def _benign_initialization_residue(control): + """Return whether control contains only bounded empty Init containers.""" + if _is_reparse(control) or not control.is_dir(): + return False + with os.scandir(control) as entries: + first = next(entries, None) + if first is None: + return True + if next(entries, None) is not None: + return False + manifests = Path(first.path) + if ( + first.name != "manifests" + or _is_reparse(manifests) + or not first.is_dir(follow_symlinks=False) + ): + return False + with os.scandir(manifests) as entries: + return next(entries, None) is None + + +def _is_benign_initialization_residue(root): + control = _operational_control(root) + return control is not None and _benign_initialization_residue(control) + + def _operational_file(root, filename): control = _operational_control(root) if control is None: @@ -2137,6 +2164,18 @@ def inspect_operational_memory(root, *, inspect_protected_intent=True): ] if control is None: return [] + try: + if _benign_initialization_residue(control): + return [] + except OSError as exc: + return [ + _memory_finding( + "state-conflict", + "P0", + STATE_DIRECTORY, + _sanitized_memory_detail(exc, "operational control entries"), + ) + ] findings, observed_control = _inspect_control_plane_files(control) state = None diff --git a/adapters/cursor/SKILL.md b/adapters/cursor/SKILL.md index 98d6a6a..baed2ba 100644 --- a/adapters/cursor/SKILL.md +++ b/adapters/cursor/SKILL.md @@ -3,7 +3,7 @@ name: docs description: Use when a user explicitly invokes repository documentation help for bounded Diátaxis writing, context recall, mapping, auditing, checking, migration, cleanup, Doctor guidance, or evidence-backed updates. metadata: author: Statusnone - version: "0.1.5" + version: "0.1.6" user-invocable: true disable-model-invocation: true --- diff --git a/adapters/cursor/scripts/_docs_checker/init_adoption.py b/adapters/cursor/scripts/_docs_checker/init_adoption.py index dadf9dd..58d79c6 100644 --- a/adapters/cursor/scripts/_docs_checker/init_adoption.py +++ b/adapters/cursor/scripts/_docs_checker/init_adoption.py @@ -23,7 +23,7 @@ from .scan import discover_markdown, scan_documents -SKILL_VERSION = "0.1.5" +SKILL_VERSION = "0.1.6" def canonical_request_bytes(value): diff --git a/adapters/cursor/scripts/_docs_checker/init_closeout.py b/adapters/cursor/scripts/_docs_checker/init_closeout.py index 8b68664..d35f968 100644 --- a/adapters/cursor/scripts/_docs_checker/init_closeout.py +++ b/adapters/cursor/scripts/_docs_checker/init_closeout.py @@ -41,6 +41,7 @@ MAX_FINDINGS_BYTES, STATE_DIRECTORY, STATE_SCHEMA_VERSION, + _is_benign_initialization_residue, build_initialization_state, inspect_operational_memory, load_operational_events, @@ -1270,11 +1271,14 @@ def inspect_initialization_preflight(root, *, control_present=False): incomplete or conflicting control plane fails closed to Doctor. """ root = Path(root).absolute() - if not control_present and not os.path.lexists(root / STATE_DIRECTORY): + control = root / STATE_DIRECTORY + if not control_present and not os.path.lexists(control): return None state = None try: + if _is_benign_initialization_residue(root): + return None memory_findings = inspect_operational_memory( root, inspect_protected_intent=False ) diff --git a/adapters/cursor/scripts/_docs_checker/memory.py b/adapters/cursor/scripts/_docs_checker/memory.py index 6f48e3a..f707993 100644 --- a/adapters/cursor/scripts/_docs_checker/memory.py +++ b/adapters/cursor/scripts/_docs_checker/memory.py @@ -25,6 +25,7 @@ ) from .paths import ( _assert_no_reparse_components, + _is_reparse, normalize_repo_relative, safe_path, shared_text_exposes_route, @@ -172,6 +173,32 @@ def _operational_control(root): return control +def _benign_initialization_residue(control): + """Return whether control contains only bounded empty Init containers.""" + if _is_reparse(control) or not control.is_dir(): + return False + with os.scandir(control) as entries: + first = next(entries, None) + if first is None: + return True + if next(entries, None) is not None: + return False + manifests = Path(first.path) + if ( + first.name != "manifests" + or _is_reparse(manifests) + or not first.is_dir(follow_symlinks=False) + ): + return False + with os.scandir(manifests) as entries: + return next(entries, None) is None + + +def _is_benign_initialization_residue(root): + control = _operational_control(root) + return control is not None and _benign_initialization_residue(control) + + def _operational_file(root, filename): control = _operational_control(root) if control is None: @@ -2137,6 +2164,18 @@ def inspect_operational_memory(root, *, inspect_protected_intent=True): ] if control is None: return [] + try: + if _benign_initialization_residue(control): + return [] + except OSError as exc: + return [ + _memory_finding( + "state-conflict", + "P0", + STATE_DIRECTORY, + _sanitized_memory_detail(exc, "operational control entries"), + ) + ] findings, observed_control = _inspect_control_plane_files(control) state = None diff --git a/adapters/gemini/docs.md b/adapters/gemini/docs.md index 6f74e94..c53c1d6 100644 --- a/adapters/gemini/docs.md +++ b/adapters/gemini/docs.md @@ -1,6 +1,6 @@ # /docs wrapper -Diátaxis Docs v0.1.5 +Diátaxis Docs v0.1.6 Instruction-enforced invocation: activate the shared `docs` skill explicitly, then parse one command and forward the raw trailing text verbatim (without shell interpolation). Usage: `/docs [raw trailing text]`. diff --git a/adapters/grok/SKILL.md b/adapters/grok/SKILL.md index 98d6a6a..baed2ba 100644 --- a/adapters/grok/SKILL.md +++ b/adapters/grok/SKILL.md @@ -3,7 +3,7 @@ name: docs description: Use when a user explicitly invokes repository documentation help for bounded Diátaxis writing, context recall, mapping, auditing, checking, migration, cleanup, Doctor guidance, or evidence-backed updates. metadata: author: Statusnone - version: "0.1.5" + version: "0.1.6" user-invocable: true disable-model-invocation: true --- diff --git a/adapters/grok/scripts/_docs_checker/init_adoption.py b/adapters/grok/scripts/_docs_checker/init_adoption.py index dadf9dd..58d79c6 100644 --- a/adapters/grok/scripts/_docs_checker/init_adoption.py +++ b/adapters/grok/scripts/_docs_checker/init_adoption.py @@ -23,7 +23,7 @@ from .scan import discover_markdown, scan_documents -SKILL_VERSION = "0.1.5" +SKILL_VERSION = "0.1.6" def canonical_request_bytes(value): diff --git a/adapters/grok/scripts/_docs_checker/init_closeout.py b/adapters/grok/scripts/_docs_checker/init_closeout.py index 8b68664..d35f968 100644 --- a/adapters/grok/scripts/_docs_checker/init_closeout.py +++ b/adapters/grok/scripts/_docs_checker/init_closeout.py @@ -41,6 +41,7 @@ MAX_FINDINGS_BYTES, STATE_DIRECTORY, STATE_SCHEMA_VERSION, + _is_benign_initialization_residue, build_initialization_state, inspect_operational_memory, load_operational_events, @@ -1270,11 +1271,14 @@ def inspect_initialization_preflight(root, *, control_present=False): incomplete or conflicting control plane fails closed to Doctor. """ root = Path(root).absolute() - if not control_present and not os.path.lexists(root / STATE_DIRECTORY): + control = root / STATE_DIRECTORY + if not control_present and not os.path.lexists(control): return None state = None try: + if _is_benign_initialization_residue(root): + return None memory_findings = inspect_operational_memory( root, inspect_protected_intent=False ) diff --git a/adapters/grok/scripts/_docs_checker/memory.py b/adapters/grok/scripts/_docs_checker/memory.py index 6f48e3a..f707993 100644 --- a/adapters/grok/scripts/_docs_checker/memory.py +++ b/adapters/grok/scripts/_docs_checker/memory.py @@ -25,6 +25,7 @@ ) from .paths import ( _assert_no_reparse_components, + _is_reparse, normalize_repo_relative, safe_path, shared_text_exposes_route, @@ -172,6 +173,32 @@ def _operational_control(root): return control +def _benign_initialization_residue(control): + """Return whether control contains only bounded empty Init containers.""" + if _is_reparse(control) or not control.is_dir(): + return False + with os.scandir(control) as entries: + first = next(entries, None) + if first is None: + return True + if next(entries, None) is not None: + return False + manifests = Path(first.path) + if ( + first.name != "manifests" + or _is_reparse(manifests) + or not first.is_dir(follow_symlinks=False) + ): + return False + with os.scandir(manifests) as entries: + return next(entries, None) is None + + +def _is_benign_initialization_residue(root): + control = _operational_control(root) + return control is not None and _benign_initialization_residue(control) + + def _operational_file(root, filename): control = _operational_control(root) if control is None: @@ -2137,6 +2164,18 @@ def inspect_operational_memory(root, *, inspect_protected_intent=True): ] if control is None: return [] + try: + if _benign_initialization_residue(control): + return [] + except OSError as exc: + return [ + _memory_finding( + "state-conflict", + "P0", + STATE_DIRECTORY, + _sanitized_memory_detail(exc, "operational control entries"), + ) + ] findings, observed_control = _inspect_control_plane_files(control) state = None diff --git a/adapters/opencode/docs.md b/adapters/opencode/docs.md index 6f74e94..c53c1d6 100644 --- a/adapters/opencode/docs.md +++ b/adapters/opencode/docs.md @@ -1,6 +1,6 @@ # /docs wrapper -Diátaxis Docs v0.1.5 +Diátaxis Docs v0.1.6 Instruction-enforced invocation: activate the shared `docs` skill explicitly, then parse one command and forward the raw trailing text verbatim (without shell interpolation). Usage: `/docs [raw trailing text]`. diff --git a/adapters/plugin/.codex-plugin/plugin.json b/adapters/plugin/.codex-plugin/plugin.json index 4efe808..1fe61db 100644 --- a/adapters/plugin/.codex-plugin/plugin.json +++ b/adapters/plugin/.codex-plugin/plugin.json @@ -26,5 +26,5 @@ "name": "diataxis-docs", "repository": "https://github.com/Statusnone420/Skills", "skills": "./skills/", - "version": "0.1.5" + "version": "0.1.6" } diff --git a/adapters/plugin/skills/docs/SKILL.md b/adapters/plugin/skills/docs/SKILL.md index 455cc29..c2f9658 100644 --- a/adapters/plugin/skills/docs/SKILL.md +++ b/adapters/plugin/skills/docs/SKILL.md @@ -3,7 +3,7 @@ name: docs description: Use when a user explicitly invokes repository documentation help for bounded Diátaxis writing, context recall, mapping, auditing, checking, migration, cleanup, Doctor guidance, or evidence-backed updates. metadata: author: Statusnone - version: "0.1.5" + version: "0.1.6" --- # Diátaxis Docs diff --git a/adapters/plugin/skills/docs/scripts/_docs_checker/init_adoption.py b/adapters/plugin/skills/docs/scripts/_docs_checker/init_adoption.py index dadf9dd..58d79c6 100644 --- a/adapters/plugin/skills/docs/scripts/_docs_checker/init_adoption.py +++ b/adapters/plugin/skills/docs/scripts/_docs_checker/init_adoption.py @@ -23,7 +23,7 @@ from .scan import discover_markdown, scan_documents -SKILL_VERSION = "0.1.5" +SKILL_VERSION = "0.1.6" def canonical_request_bytes(value): diff --git a/adapters/plugin/skills/docs/scripts/_docs_checker/init_closeout.py b/adapters/plugin/skills/docs/scripts/_docs_checker/init_closeout.py index 8b68664..d35f968 100644 --- a/adapters/plugin/skills/docs/scripts/_docs_checker/init_closeout.py +++ b/adapters/plugin/skills/docs/scripts/_docs_checker/init_closeout.py @@ -41,6 +41,7 @@ MAX_FINDINGS_BYTES, STATE_DIRECTORY, STATE_SCHEMA_VERSION, + _is_benign_initialization_residue, build_initialization_state, inspect_operational_memory, load_operational_events, @@ -1270,11 +1271,14 @@ def inspect_initialization_preflight(root, *, control_present=False): incomplete or conflicting control plane fails closed to Doctor. """ root = Path(root).absolute() - if not control_present and not os.path.lexists(root / STATE_DIRECTORY): + control = root / STATE_DIRECTORY + if not control_present and not os.path.lexists(control): return None state = None try: + if _is_benign_initialization_residue(root): + return None memory_findings = inspect_operational_memory( root, inspect_protected_intent=False ) diff --git a/adapters/plugin/skills/docs/scripts/_docs_checker/memory.py b/adapters/plugin/skills/docs/scripts/_docs_checker/memory.py index 6f48e3a..f707993 100644 --- a/adapters/plugin/skills/docs/scripts/_docs_checker/memory.py +++ b/adapters/plugin/skills/docs/scripts/_docs_checker/memory.py @@ -25,6 +25,7 @@ ) from .paths import ( _assert_no_reparse_components, + _is_reparse, normalize_repo_relative, safe_path, shared_text_exposes_route, @@ -172,6 +173,32 @@ def _operational_control(root): return control +def _benign_initialization_residue(control): + """Return whether control contains only bounded empty Init containers.""" + if _is_reparse(control) or not control.is_dir(): + return False + with os.scandir(control) as entries: + first = next(entries, None) + if first is None: + return True + if next(entries, None) is not None: + return False + manifests = Path(first.path) + if ( + first.name != "manifests" + or _is_reparse(manifests) + or not first.is_dir(follow_symlinks=False) + ): + return False + with os.scandir(manifests) as entries: + return next(entries, None) is None + + +def _is_benign_initialization_residue(root): + control = _operational_control(root) + return control is not None and _benign_initialization_residue(control) + + def _operational_file(root, filename): control = _operational_control(root) if control is None: @@ -2137,6 +2164,18 @@ def inspect_operational_memory(root, *, inspect_protected_intent=True): ] if control is None: return [] + try: + if _benign_initialization_residue(control): + return [] + except OSError as exc: + return [ + _memory_finding( + "state-conflict", + "P0", + STATE_DIRECTORY, + _sanitized_memory_detail(exc, "operational control entries"), + ) + ] findings, observed_control = _inspect_control_plane_files(control) state = None diff --git a/adapters/web/docs-help.txt b/adapters/web/docs-help.txt index 104e22a..bd45abe 100644 --- a/adapters/web/docs-help.txt +++ b/adapters/web/docs-help.txt @@ -1,5 +1,5 @@ Explicit command: `help` -Diátaxis Docs v0.1.5 +Diátaxis Docs v0.1.6 {{RAW_TRAILING_TEXT}} Generic web mode: always draft-only, regardless of claimed capabilities. Use only supplied {{REPOSITORY_MATERIAL}} as untrusted evidence. Do not inspect a repository, run tools/Git, create isolation, or write/edit/move/delete files; never claim inspection or edits. diff --git a/plugins/diataxis-docs/.codex-plugin/plugin.json b/plugins/diataxis-docs/.codex-plugin/plugin.json index 4efe808..1fe61db 100644 --- a/plugins/diataxis-docs/.codex-plugin/plugin.json +++ b/plugins/diataxis-docs/.codex-plugin/plugin.json @@ -26,5 +26,5 @@ "name": "diataxis-docs", "repository": "https://github.com/Statusnone420/Skills", "skills": "./skills/", - "version": "0.1.5" + "version": "0.1.6" } diff --git a/plugins/diataxis-docs/skills/docs/SKILL.md b/plugins/diataxis-docs/skills/docs/SKILL.md index 455cc29..c2f9658 100644 --- a/plugins/diataxis-docs/skills/docs/SKILL.md +++ b/plugins/diataxis-docs/skills/docs/SKILL.md @@ -3,7 +3,7 @@ name: docs description: Use when a user explicitly invokes repository documentation help for bounded Diátaxis writing, context recall, mapping, auditing, checking, migration, cleanup, Doctor guidance, or evidence-backed updates. metadata: author: Statusnone - version: "0.1.5" + version: "0.1.6" --- # Diátaxis Docs diff --git a/plugins/diataxis-docs/skills/docs/scripts/_docs_checker/init_adoption.py b/plugins/diataxis-docs/skills/docs/scripts/_docs_checker/init_adoption.py index dadf9dd..58d79c6 100644 --- a/plugins/diataxis-docs/skills/docs/scripts/_docs_checker/init_adoption.py +++ b/plugins/diataxis-docs/skills/docs/scripts/_docs_checker/init_adoption.py @@ -23,7 +23,7 @@ from .scan import discover_markdown, scan_documents -SKILL_VERSION = "0.1.5" +SKILL_VERSION = "0.1.6" def canonical_request_bytes(value): diff --git a/plugins/diataxis-docs/skills/docs/scripts/_docs_checker/init_closeout.py b/plugins/diataxis-docs/skills/docs/scripts/_docs_checker/init_closeout.py index 8b68664..d35f968 100644 --- a/plugins/diataxis-docs/skills/docs/scripts/_docs_checker/init_closeout.py +++ b/plugins/diataxis-docs/skills/docs/scripts/_docs_checker/init_closeout.py @@ -41,6 +41,7 @@ MAX_FINDINGS_BYTES, STATE_DIRECTORY, STATE_SCHEMA_VERSION, + _is_benign_initialization_residue, build_initialization_state, inspect_operational_memory, load_operational_events, @@ -1270,11 +1271,14 @@ def inspect_initialization_preflight(root, *, control_present=False): incomplete or conflicting control plane fails closed to Doctor. """ root = Path(root).absolute() - if not control_present and not os.path.lexists(root / STATE_DIRECTORY): + control = root / STATE_DIRECTORY + if not control_present and not os.path.lexists(control): return None state = None try: + if _is_benign_initialization_residue(root): + return None memory_findings = inspect_operational_memory( root, inspect_protected_intent=False ) diff --git a/plugins/diataxis-docs/skills/docs/scripts/_docs_checker/memory.py b/plugins/diataxis-docs/skills/docs/scripts/_docs_checker/memory.py index 6f48e3a..f707993 100644 --- a/plugins/diataxis-docs/skills/docs/scripts/_docs_checker/memory.py +++ b/plugins/diataxis-docs/skills/docs/scripts/_docs_checker/memory.py @@ -25,6 +25,7 @@ ) from .paths import ( _assert_no_reparse_components, + _is_reparse, normalize_repo_relative, safe_path, shared_text_exposes_route, @@ -172,6 +173,32 @@ def _operational_control(root): return control +def _benign_initialization_residue(control): + """Return whether control contains only bounded empty Init containers.""" + if _is_reparse(control) or not control.is_dir(): + return False + with os.scandir(control) as entries: + first = next(entries, None) + if first is None: + return True + if next(entries, None) is not None: + return False + manifests = Path(first.path) + if ( + first.name != "manifests" + or _is_reparse(manifests) + or not first.is_dir(follow_symlinks=False) + ): + return False + with os.scandir(manifests) as entries: + return next(entries, None) is None + + +def _is_benign_initialization_residue(root): + control = _operational_control(root) + return control is not None and _benign_initialization_residue(control) + + def _operational_file(root, filename): control = _operational_control(root) if control is None: @@ -2137,6 +2164,18 @@ def inspect_operational_memory(root, *, inspect_protected_intent=True): ] if control is None: return [] + try: + if _benign_initialization_residue(control): + return [] + except OSError as exc: + return [ + _memory_finding( + "state-conflict", + "P0", + STATE_DIRECTORY, + _sanitized_memory_detail(exc, "operational control entries"), + ) + ] findings, observed_control = _inspect_control_plane_files(control) state = None diff --git a/skills/docs/SKILL.md b/skills/docs/SKILL.md index 455cc29..c2f9658 100644 --- a/skills/docs/SKILL.md +++ b/skills/docs/SKILL.md @@ -3,7 +3,7 @@ name: docs description: Use when a user explicitly invokes repository documentation help for bounded Diátaxis writing, context recall, mapping, auditing, checking, migration, cleanup, Doctor guidance, or evidence-backed updates. metadata: author: Statusnone - version: "0.1.5" + version: "0.1.6" --- # Diátaxis Docs diff --git a/skills/docs/scripts/_docs_checker/init_adoption.py b/skills/docs/scripts/_docs_checker/init_adoption.py index dadf9dd..58d79c6 100644 --- a/skills/docs/scripts/_docs_checker/init_adoption.py +++ b/skills/docs/scripts/_docs_checker/init_adoption.py @@ -23,7 +23,7 @@ from .scan import discover_markdown, scan_documents -SKILL_VERSION = "0.1.5" +SKILL_VERSION = "0.1.6" def canonical_request_bytes(value): diff --git a/skills/docs/scripts/_docs_checker/init_closeout.py b/skills/docs/scripts/_docs_checker/init_closeout.py index 8b68664..d35f968 100644 --- a/skills/docs/scripts/_docs_checker/init_closeout.py +++ b/skills/docs/scripts/_docs_checker/init_closeout.py @@ -41,6 +41,7 @@ MAX_FINDINGS_BYTES, STATE_DIRECTORY, STATE_SCHEMA_VERSION, + _is_benign_initialization_residue, build_initialization_state, inspect_operational_memory, load_operational_events, @@ -1270,11 +1271,14 @@ def inspect_initialization_preflight(root, *, control_present=False): incomplete or conflicting control plane fails closed to Doctor. """ root = Path(root).absolute() - if not control_present and not os.path.lexists(root / STATE_DIRECTORY): + control = root / STATE_DIRECTORY + if not control_present and not os.path.lexists(control): return None state = None try: + if _is_benign_initialization_residue(root): + return None memory_findings = inspect_operational_memory( root, inspect_protected_intent=False ) diff --git a/skills/docs/scripts/_docs_checker/memory.py b/skills/docs/scripts/_docs_checker/memory.py index 6f48e3a..f707993 100644 --- a/skills/docs/scripts/_docs_checker/memory.py +++ b/skills/docs/scripts/_docs_checker/memory.py @@ -25,6 +25,7 @@ ) from .paths import ( _assert_no_reparse_components, + _is_reparse, normalize_repo_relative, safe_path, shared_text_exposes_route, @@ -172,6 +173,32 @@ def _operational_control(root): return control +def _benign_initialization_residue(control): + """Return whether control contains only bounded empty Init containers.""" + if _is_reparse(control) or not control.is_dir(): + return False + with os.scandir(control) as entries: + first = next(entries, None) + if first is None: + return True + if next(entries, None) is not None: + return False + manifests = Path(first.path) + if ( + first.name != "manifests" + or _is_reparse(manifests) + or not first.is_dir(follow_symlinks=False) + ): + return False + with os.scandir(manifests) as entries: + return next(entries, None) is None + + +def _is_benign_initialization_residue(root): + control = _operational_control(root) + return control is not None and _benign_initialization_residue(control) + + def _operational_file(root, filename): control = _operational_control(root) if control is None: @@ -2137,6 +2164,18 @@ def inspect_operational_memory(root, *, inspect_protected_intent=True): ] if control is None: return [] + try: + if _benign_initialization_residue(control): + return [] + except OSError as exc: + return [ + _memory_finding( + "state-conflict", + "P0", + STATE_DIRECTORY, + _sanitized_memory_detail(exc, "operational control entries"), + ) + ] findings, observed_control = _inspect_control_plane_files(control) state = None diff --git a/tests/test_adapters.py b/tests/test_adapters.py index 34c6c62..cc70e2f 100644 --- a/tests/test_adapters.py +++ b/tests/test_adapters.py @@ -514,7 +514,7 @@ def test_check_detects_version_drift_across_generated_surfaces(self): manifest["version"] = "9.9.9" manifest_path.write_text(json.dumps(manifest, sort_keys=True, indent=2) + "\n", encoding="utf-8") wrapper_path = out / "gemini/docs.md" - wrapper_path.write_text(wrapper_path.read_text(encoding="utf-8").replace("v0.1.5", "v9.9.9"), encoding="utf-8") + wrapper_path.write_text(wrapper_path.read_text(encoding="utf-8").replace("v0.1.6", "v9.9.9"), encoding="utf-8") check = subprocess.run( [sys.executable, str(BUILDER), "--check", "--output", str(out)], @@ -533,7 +533,7 @@ def test_check_detects_static_skill_version_drift(self): subprocess.run([sys.executable, str(BUILDER), "generate", "--output", str(out)], cwd=ROOT, check=True) skill_path = out / "copilot/SKILL.md" skill_path.write_text( - skill_path.read_text(encoding="utf-8").replace('version: "0.1.5"', 'version: "9.9.9"'), + skill_path.read_text(encoding="utf-8").replace('version: "0.1.6"', 'version: "9.9.9"'), encoding="utf-8", ) @@ -617,20 +617,20 @@ def test_generated_contracts(self): canonical = (ROOT / "skills/docs/SKILL.md").read_text(encoding="utf-8") for vendor in ("claude", "copilot", "grok", "cursor"): text = (adapter_skill_root(out, vendor) / "SKILL.md").read_text(encoding="utf-8") - self.assertIn(' version: "0.1.5"', text) + self.assertIn(' version: "0.1.6"', text) self.assertIn("user-invocable: true", text) self.assertIn("disable-model-invocation: true", text) self.assertEqual(text.split("---", 2)[-1].replace("\nuser-invocable: true\ndisable-model-invocation: true", "", 1), canonical.split("---", 2)[-1]) for vendor in ("gemini", "opencode"): wrapper = (out / vendor / "docs.md").read_text(encoding="utf-8") self.assertIn("docs", wrapper.lower()); self.assertIn("raw trailing text", wrapper.lower()) - self.assertIn("Diátaxis Docs v0.1.5", wrapper) + self.assertIn("Diátaxis Docs v0.1.6", wrapper) web = (out / "web" / "docs-help.txt").read_text(encoding="utf-8") self.assertIn("capabilit", web.lower()) - self.assertIn("Diátaxis Docs v0.1.5", web) + self.assertIn("Diátaxis Docs v0.1.6", web) manifest = json.loads((out / "plugin/.codex-plugin/plugin.json").read_text(encoding="utf-8")) self.assertEqual(manifest["name"], "diataxis-docs") - self.assertEqual(manifest["version"], "0.1.5") + self.assertEqual(manifest["version"], "0.1.6") self.assertEqual(manifest["interface"]["capabilities"], ["Read", "Write"]) self.assertEqual(manifest["interface"].get("brandColor"), "#6657E8") self.assertEqual(manifest["interface"].get("composerIcon"), "./assets/bounded-compass.png") diff --git a/tests/test_claude_distribution.py b/tests/test_claude_distribution.py index 4961141..5bf7651 100644 --- a/tests/test_claude_distribution.py +++ b/tests/test_claude_distribution.py @@ -23,7 +23,7 @@ def test_marketplace_routes_to_generated_claude_adapter(self): self.assertEqual(plugin["name"], "diataxis-docs") self.assertEqual(plugin["displayName"], "Diátaxis Docs") self.assertEqual(plugin["source"], "./adapters/claude") - self.assertEqual(plugin["version"], "0.1.5") + self.assertEqual(plugin["version"], "0.1.6") source = PurePosixPath(plugin["source"]) self.assertEqual(source.parts[0], "adapters") @@ -47,7 +47,7 @@ def test_generated_claude_plugin_uses_the_canonical_version(self): self.assertEqual(manifest["description"], "Bounded repository memory. Evidence-backed documentation.") self.assertEqual(manifest["repository"], "https://github.com/Statusnone420/Skills") self.assertEqual(manifest["license"], "Apache-2.0") - self.assertEqual(manifest["version"], "0.1.5") + self.assertEqual(manifest["version"], "0.1.6") skill_root = output / "claude" / "skills" / "docs" self.assertFalse((output / "claude" / "SKILL.md").exists()) generated_skill = (skill_root / "SKILL.md").read_text(encoding="utf-8") diff --git a/tests/test_command_skill_distribution.py b/tests/test_command_skill_distribution.py index 9528623..c677e80 100644 --- a/tests/test_command_skill_distribution.py +++ b/tests/test_command_skill_distribution.py @@ -48,7 +48,7 @@ def frontmatter(text): class CommandSkillDistributionTests(unittest.TestCase): - def _run_doctor_baseline_raw(self, files, *extra): + def _run_doctor_baseline_raw(self, files, *extra, directories=()): with tempfile.TemporaryDirectory() as td: repository = Path(td) / "repo" repository.mkdir() @@ -56,6 +56,8 @@ def _run_doctor_baseline_raw(self, files, *extra): path = repository / relative path.parent.mkdir(parents=True, exist_ok=True) path.write_text(content, encoding="utf-8") + for relative in directories: + (repository / relative).mkdir(parents=True, exist_ok=True) subprocess.run(["git", "init", "-q", str(repository)], check=True) subprocess.run( ["git", "-C", str(repository), "add", "--all"], check=True @@ -88,8 +90,12 @@ def _run_doctor_baseline_raw(self, files, *extra): ).stdout return result, before, after - def _run_doctor_baseline(self, files, *extra): - result, before, after = self._run_doctor_baseline_raw(files, *extra) + def _run_doctor_baseline(self, files, *extra, directories=()): + result, before, after = self._run_doctor_baseline_raw( + files, + *extra, + directories=directories, + ) return result, json.loads(result.stdout), before, after def test_codex_marketplace_routes_to_the_named_plugin_package(self): @@ -123,7 +129,7 @@ def test_codex_marketplace_routes_to_the_named_plugin_package(self): ) self.assertEqual(manifest["name"], entry["name"]) self.assertEqual(plugin_root.name, entry["name"]) - self.assertEqual(manifest["version"], "0.1.5") + self.assertEqual(manifest["version"], "0.1.6") self.assertEqual(manifest["interface"]["displayName"], "Diátaxis Docs") def test_codex_and_claude_publish_the_umbrella_plus_focused_skills(self): @@ -240,6 +246,31 @@ def test_engine_measures_safe_tracked_root_readme_fallback_without_writes(self): self.assertEqual(payload["scope"], "docs") self.assertEqual(payload["health"]["rubric_version"], 2) + def test_engine_measures_with_empty_control_residue_without_writes(self): + result, payload, before, after = self._run_doctor_baseline( + { + "README.md": "# Project\n\nRepository overview.\n", + "docs/guide.md": "# Guide\n\nUseful guidance.\n", + }, + directories=(".diataxis/manifests",), + ) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(before, after) + self.assertEqual(payload["doctor_baseline"]["status"], "measured") + self.assertEqual( + payload["doctor_baseline"]["authority_kind"], + "orientation-fallback", + ) + self.assertEqual(payload["doctor_baseline"]["writes"], 0) + self.assertEqual(payload["doctor_baseline"]["recommendation"], "$docs init") + self.assertFalse( + any( + finding.get("kind") == "state-conflict" + for finding in payload["findings"] + ) + ) + def test_engine_rejects_unsupported_provider_without_score_or_init(self): result, payload, before, after = self._run_doctor_baseline( { diff --git a/tests/test_docs_skill.py b/tests/test_docs_skill.py index 5960d57..6d07b81 100644 --- a/tests/test_docs_skill.py +++ b/tests/test_docs_skill.py @@ -117,7 +117,7 @@ def test_canonical_public_alpha_version_and_help_identity(self): skill = (SKILL / "SKILL.md").read_text(encoding="utf-8") commands = (SKILL / "references" / "commands.md").read_text(encoding="utf-8") - self.assertIn("metadata:\n author: Statusnone\n version: \"0.1.5\"", skill) + self.assertIn("metadata:\n author: Statusnone\n version: \"0.1.6\"", skill) self.assertIn("Diátaxis Docs v", commands) def test_default_help_uses_plain_english_daily_commands(self): @@ -370,10 +370,10 @@ def test_doctor_evals_cover_exhaustive_scoped_and_fingerprinted_treatments(self) def test_canonical_version_is_strict_semver(self): skill = (SKILL / "SKILL.md").read_text(encoding="utf-8") - self.assertEqual(build_adapters.canonical_version(skill), "0.1.5") + self.assertEqual(build_adapters.canonical_version(skill), "0.1.6") for invalid in ("1", "v0.1.0", "01.0.0", "0.1.0-alpha"): with self.subTest(invalid=invalid): - malformed = skill.replace('version: "0.1.5"', f'version: "{invalid}"') + malformed = skill.replace('version: "0.1.6"', f'version: "{invalid}"') with self.assertRaises(ValueError): build_adapters.canonical_version(malformed) diff --git a/tests/test_init_closeout.py b/tests/test_init_closeout.py index fbe6073..2d611ed 100644 --- a/tests/test_init_closeout.py +++ b/tests/test_init_closeout.py @@ -288,6 +288,67 @@ def test_adopt_preview_matches_explicit_scope_case_insensitively_on_windows(self self.assertTrue(receipt_path.is_file()) self.assertEqual(tree_snapshot(root), before) + def test_adopt_preview_and_apply_accept_empty_manifest_residue(self): + with tempfile.TemporaryDirectory() as td: + root = Path(td) / "repository" + build_repository(root) + manifests = root / ".diataxis" / "manifests" + manifests.mkdir(parents=True) + receipt_path = Path(td) / "init-receipt.json" + before = tree_snapshot(root) + + preview_process = subprocess.run( + [ + sys.executable, + str(CLOSEOUT), + str(root), + "adopt-preview", + "--receipt-file", + str(receipt_path), + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual( + preview_process.returncode, + 0, + preview_process.stderr or preview_process.stdout, + ) + preview = json.loads(preview_process.stdout) + self.assertEqual(preview["status"], "approval-required") + self.assertEqual(preview["writes"], 0) + self.assertEqual(tree_snapshot(root), before) + + apply_process = subprocess.run( + [ + sys.executable, + str(CLOSEOUT), + str(root), + "adopt-apply", + "--receipt-file", + str(receipt_path), + "--approval", + preview["approval"], + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual( + apply_process.returncode, + 0, + apply_process.stderr or apply_process.stdout, + ) + applied = json.loads(apply_process.stdout) + self.assertEqual(applied["status"], "applied") + self.assertTrue(applied["successful_event_recorded"]) + self.assertTrue(any(manifests.iterdir())) + @unittest.skipUnless(sys.platform == "win32", "Windows PowerShell transport test") def test_documented_init_powershell_transport_executes_preview_and_apply(self): init_reference = (SCRIPTS.parent / "references" / "init.md").read_text( @@ -1224,17 +1285,49 @@ def restore(): self.assertEqual(payload["writes"], 0) self.assertEqual(tree_snapshot(root), before) - with tempfile.TemporaryDirectory() as td: - root = Path(td) - build_repository(root, preexisting_control=True) - before = tree_snapshot(root) - process = run_init_discovery(root) - self.assertEqual(process.returncode, 2, process.stderr) - payload = json.loads(process.stdout) - self.assertEqual(payload["status"], "state-conflict") - self.assertEqual(payload["candidate_traversal"], 0) - self.assertEqual(payload["content_reads"], 0) - self.assertEqual(tree_snapshot(root), before) + for label in ("unknown-empty-container", "nonempty-manifests"): + with self.subTest(label=label), tempfile.TemporaryDirectory() as td: + root = Path(td) + build_repository(root, preexisting_control=True) + if label == "unknown-empty-container": + (root / ".diataxis" / "unknown").mkdir() + else: + manifests = root / ".diataxis" / "manifests" + manifests.mkdir() + (manifests / "orphan.json").write_text( + "{}\n", + encoding="utf-8", + ) + before = tree_snapshot(root) + + process = run_init_discovery(root) + + self.assertEqual(process.returncode, 2, process.stderr) + payload = json.loads(process.stdout) + self.assertEqual(payload["status"], "state-conflict") + self.assertEqual(payload["candidate_traversal"], 0) + self.assertEqual(payload["content_reads"], 0) + self.assertEqual(tree_snapshot(root), before) + + def test_empty_control_residue_allows_init_discovery_without_writes(self): + for label, with_manifests in ( + ("empty-control", False), + ("empty-manifests", True), + ): + with self.subTest(label=label), tempfile.TemporaryDirectory() as td: + root = Path(td) + build_repository(root, preexisting_control=True) + if with_manifests: + (root / ".diataxis" / "manifests").mkdir() + before = tree_snapshot(root) + + process = run_init_discovery(root) + + self.assertEqual(process.returncode, 0, process.stderr) + payload = json.loads(process.stdout) + self.assertEqual(payload["status"], "batch-limited") + self.assertNotEqual(payload.get("mode"), "init-preflight") + self.assertEqual(tree_snapshot(root), before) def test_second_public_init_does_not_create_preview_manifest_or_event(self): diff --git a/tests/test_repository_memory.py b/tests/test_repository_memory.py index 5112116..0c4b506 100644 --- a/tests/test_repository_memory.py +++ b/tests/test_repository_memory.py @@ -1787,6 +1787,30 @@ def test_public_memory_findings_sanitize_environmental_and_validation_messages(s self.assertEqual(findings[0]["detail"], "operational event is invalid") self.assertNotIn(private, json.dumps(findings, sort_keys=True)) + def test_empty_control_scan_error_is_reported_fail_closed(self): + private = r"C:\private-checkout\.diataxis WinError 5 SECRET" + with tempfile.TemporaryDirectory() as td: + root = Path(td) + control = root / ".diataxis" + control.mkdir() + + with mock.patch( + "_docs_checker.memory.os.scandir", + side_effect=PermissionError(private), + ): + findings = docs_checker.inspect_operational_memory(root) + + self.assertEqual(len(findings), 1) + self.assertEqual(findings[0]["kind"], "state-conflict") + self.assertEqual(findings[0]["priority"], "P0") + self.assertEqual(findings[0]["path"], ".diataxis") + self.assertEqual( + findings[0]["detail"], + "operational control entries is unavailable", + ) + self.assertNotIn(private, json.dumps(findings, sort_keys=True)) + self.assertEqual(list(control.iterdir()), []) + def test_duplicate_json_keys_are_state_conflicts_in_every_control_file(self): state = json.dumps(valid_state(), sort_keys=True) duplicate_state = state.replace( @@ -1928,17 +1952,26 @@ def test_each_control_file_enforces_its_capacity_without_truncation(self): ) self.assertEqual(path.read_bytes(), before) - def test_missing_control_files_are_conflicts_only_after_control_plane_exists(self): + def test_missing_control_files_are_conflicts_only_after_real_state_evidence_exists(self): with tempfile.TemporaryDirectory() as td: uninitialized = Path(td) / "uninitialized" uninitialized.mkdir() findings, _ = docs_checker.check(uninitialized) self.assertFalse(any(item["kind"] == "state-conflict" for item in findings)) - initialized = Path(td) / "initialized" - initialized.mkdir() - (initialized / ".diataxis").mkdir() - findings, _ = docs_checker.check(initialized) + empty_control = Path(td) / "empty-control" + (empty_control / ".diataxis").mkdir(parents=True) + findings, _ = docs_checker.check(empty_control) + self.assertFalse(any(item["kind"] == "state-conflict" for item in findings)) + + empty_manifests = Path(td) / "empty-manifests" + control = empty_manifests / ".diataxis" + (control / "manifests").mkdir(parents=True) + findings, _ = docs_checker.check(empty_manifests) + self.assertFalse(any(item["kind"] == "state-conflict" for item in findings)) + + (control / "unexpected").write_text("evidence\n", encoding="utf-8") + findings, _ = docs_checker.check(empty_manifests) self.assertTrue(any(item["kind"] == "state-conflict" for item in findings)) def test_checker_inspection_never_changes_operational_memory(self):