diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e0044c3..0fff21de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,10 +71,13 @@ PyPI version — not the changelog header. `ALLOWED_JSON_SUBDIRS` allowlist and a `check_branch_post()` that validates `{branch}_json/` subdirs — `custom_config/` and hidden dirs (`.archive`) pass, any other split is flagged. `json_structure_content.py` documents the directory - structure and operator-config location. 5 new tests. (The new check surfaced - `devpulse_json/compass/` as an unsanctioned split — handled via a documented - devpulse bypass, compass being a legitimate SQLite/FTS5 decision store that - needs its own directory.) + structure and operator-config location. The subdir check honors + `.seedgo/bypass.json` (bypass rules are threaded through + `check_branch_post` → `_check_json_dir_structure`), so a branch can sanction a + legitimate data subdir while unsanctioned + unbypassed splits still fail. 7 new + tests. (The new check surfaced `devpulse_json/compass/` — the devpulse Compass + SQLite/FTS5 decision store, which needs its own directory — now sanctioned via a + documented devpulse bypass; audit confirms Json_Structure back to 100%.) - **`git_gate` block messages now guide external users instead of dead-ending (issue #620).** A blocked raw `git`/`gh` command previously just errored. The diff --git a/src/aipass/devpulse/.seedgo/bypass.json b/src/aipass/devpulse/.seedgo/bypass.json index 63a3dd4c..bcd62676 100644 --- a/src/aipass/devpulse/.seedgo/bypass.json +++ b/src/aipass/devpulse/.seedgo/bypass.json @@ -5,6 +5,11 @@ "description": "Standards bypass configuration for this branch" }, "bypass": [ + { + "standard": "json_structure", + "file": "devpulse_json/compass", + "reason": "compass/ is the devpulse-owned Compass decision store (SQLite/FTS5 — db + wal + shm) which needs its own directory. Legitimate data subdir, not operator-config (custom_config/) nor auto-gen root data. Sanctioned exception per #643." + }, { "standard": "architecture", "reason": "No 'manager' citizen_class template in spawn. Devpulse is the only manager branch." diff --git a/src/aipass/seedgo/apps/handlers/aipass_standards/json_structure_check.py b/src/aipass/seedgo/apps/handlers/aipass_standards/json_structure_check.py index 5f79eb9f..ae053c31 100644 --- a/src/aipass/seedgo/apps/handlers/aipass_standards/json_structure_check.py +++ b/src/aipass/seedgo/apps/handlers/aipass_standards/json_structure_check.py @@ -270,7 +270,7 @@ def _check_json_handler_config(_handler_path: Path, content: str, _bypass_rules: "passed": not has_template_dir, "message": "No json_templates/ references (correct — code is the template)" if not has_template_dir - else "References json_templates/ directory — standard requires auto-create from code defaults, not file templates", + else "References json_templates/ directory — use auto-create from code defaults, not file templates", } ) @@ -304,16 +304,18 @@ def _find_json_dir(branch_path: str) -> Path | None: return json_dir if json_dir.is_dir() else None -def _check_json_dir_structure(branch_path: str) -> list[dict]: +def _check_json_dir_structure(branch_path: str, bypass_rules: list | None = None) -> list[dict]: """Validate {branch}_json/ has no unsanctioned subdirectories. Allowed: custom_config/ (operator-editable config). Hidden dirs (starting with '.') are ignored (e.g. .archive). + Bypassed subdirs (via .seedgo/bypass.json) are also allowed. """ json_dir = _find_json_dir(branch_path) if json_dir is None: return [] + bp = Path(branch_path) violations = [] for child in sorted(json_dir.iterdir()): if not child.is_dir(): @@ -322,6 +324,9 @@ def _check_json_dir_structure(branch_path: str) -> list[dict]: continue if child.name in ALLOWED_JSON_SUBDIRS: continue + relative = f"{bp.name}_json/{child.name}" + if is_bypassed(relative, "json_structure", bypass_rules=bypass_rules): + continue violations.append( { "file": child.name, @@ -336,8 +341,8 @@ def _check_json_dir_structure(branch_path: str) -> list[dict]: return violations -def check_branch_post(branch_path: str) -> tuple[list, list]: +def check_branch_post(branch_path: str, bypass_rules: list | None = None) -> tuple[list, list]: """Post-audit check: validate {branch}_json/ directory structure.""" - violations = _check_json_dir_structure(branch_path) + violations = _check_json_dir_structure(branch_path, bypass_rules=bypass_rules) scores = [0] if violations else [100] return violations, scores diff --git a/src/aipass/seedgo/apps/handlers/audit/branch_audit.py b/src/aipass/seedgo/apps/handlers/audit/branch_audit.py index bd84ecc0..b8668f3f 100644 --- a/src/aipass/seedgo/apps/handlers/audit/branch_audit.py +++ b/src/aipass/seedgo/apps/handlers/audit/branch_audit.py @@ -182,7 +182,7 @@ def audit_branch(branch: Dict[str, str], bypass_rules: list, pack_path: Path | N for name, checker in checkers.items(): if hasattr(checker, "check_branch_post") and name in scores: try: - pv, ps = checker.check_branch_post(str(branch_path)) + pv, ps = checker.check_branch_post(str(branch_path), bypass_rules=bypass_rules) all_violations.setdefault(name, []).extend(pv) if ps: scores[name] = int(sum(ps + [scores[name]]) / (len(ps) + 1)) diff --git a/src/aipass/seedgo/tests/test_aipass_standards.py b/src/aipass/seedgo/tests/test_aipass_standards.py index 58758bb0..fbf09fc3 100644 --- a/src/aipass/seedgo/tests/test_aipass_standards.py +++ b/src/aipass/seedgo/tests/test_aipass_standards.py @@ -213,6 +213,38 @@ def test_json_structure_check_branch_post(tmp_path): assert scores2 == [100] +def test_json_structure_bypassed_subdir_passes(tmp_path): + """A subdir bypassed via bypass_rules is not flagged.""" + from aipass.seedgo.apps.handlers.aipass_standards.json_structure_check import _check_json_dir_structure + + branch = tmp_path / "mybranch" + branch.mkdir() + json_dir = branch / "mybranch_json" + json_dir.mkdir() + (json_dir / "compass").mkdir() + + bypass_rules = [{"standard": "json_structure", "file": "mybranch_json/compass", "reason": "test"}] + violations = _check_json_dir_structure(str(branch), bypass_rules=bypass_rules) + assert violations == [] + + +def test_json_structure_unbypassed_subdir_still_fails(tmp_path): + """An unsanctioned subdir without a bypass entry is still flagged.""" + from aipass.seedgo.apps.handlers.aipass_standards.json_structure_check import _check_json_dir_structure + + branch = tmp_path / "mybranch" + branch.mkdir() + json_dir = branch / "mybranch_json" + json_dir.mkdir() + (json_dir / "compass").mkdir() + (json_dir / "random_dir").mkdir() + + bypass_rules = [{"standard": "json_structure", "file": "mybranch_json/compass", "reason": "test"}] + violations = _check_json_dir_structure(str(branch), bypass_rules=bypass_rules) + assert len(violations) == 1 + assert "random_dir" in violations[0]["message"] + + # --------------------------------------------------------------------------- # Tests -- naming_check.is_bypassed # ---------------------------------------------------------------------------