diff --git a/CHANGELOG.md b/CHANGELOG.md index a4ebbbb4..1e0044c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,19 @@ PyPI version — not the changelog header. ### Fixed +- **seedgo `json_structure` now sanctions `custom_config/` for operator-editable + config (issue #643).** The standard said "`{branch}_json/` root, one directory, + no splits" and the checker ignored subdirs, so `custom_config/` (home of + operator-tunable runtime config like `cadence_config.json`, `memory.config.json`) + was an undocumented convention. `json_structure_check.py` gained an + `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.) + - **`git_gate` block messages now guide external users instead of dead-ending (issue #620).** A blocked raw `git`/`gh` command previously just errored. The block message now explains *why* git is enforced (prevents cross-agent state 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 c78d79b8..5f79eb9f 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 @@ -31,6 +31,8 @@ from aipass.seedgo.apps.handlers.bypass.utils import is_bypassed # Audit scope: scan every .py file, not just entry point AUDIT_SCOPE = "all_files" +ALLOWED_JSON_SUBDIRS: frozenset[str] = frozenset({"custom_config"}) + def check_module(module_path: str, bypass_rules: list | None = None) -> Dict: """ @@ -183,7 +185,7 @@ def _check_code_wiring(_path: Path, content: str) -> List[Dict]: } ) - return checks + return checks # noqa: RET504 def _check_json_handler_config(_handler_path: Path, content: str, _bypass_rules: list | None = None) -> List[Dict]: @@ -288,3 +290,54 @@ def _check_json_handler_config(_handler_path: Path, content: str, _bypass_rules: ) return checks + + +# ------------------------------------------------------------------ +# Branch-level post-check: {branch}_json/ directory structure +# ------------------------------------------------------------------ + + +def _find_json_dir(branch_path: str) -> Path | None: + """Locate {branch}_json/ under a branch root.""" + bp = Path(branch_path) + json_dir = bp / f"{bp.name}_json" + return json_dir if json_dir.is_dir() else None + + +def _check_json_dir_structure(branch_path: str) -> list[dict]: + """Validate {branch}_json/ has no unsanctioned subdirectories. + + Allowed: custom_config/ (operator-editable config). + Hidden dirs (starting with '.') are ignored (e.g. .archive). + """ + json_dir = _find_json_dir(branch_path) + if json_dir is None: + return [] + + violations = [] + for child in sorted(json_dir.iterdir()): + if not child.is_dir(): + continue + if child.name.startswith("."): + continue + if child.name in ALLOWED_JSON_SUBDIRS: + continue + violations.append( + { + "file": child.name, + "path": str(child), + "score": 0, + "issues": [f"Unsanctioned subdir '{child.name}/' under {json_dir.name}/ — only custom_config/ allowed"], + "message": ( + f"Unsanctioned subdir '{child.name}/' under {json_dir.name}/ — only custom_config/ allowed" + ), + } + ) + return violations + + +def check_branch_post(branch_path: str) -> tuple[list, list]: + """Post-audit check: validate {branch}_json/ directory structure.""" + violations = _check_json_dir_structure(branch_path) + scores = [0] if violations else [100] + return violations, scores diff --git a/src/aipass/seedgo/apps/handlers/aipass_standards/json_structure_content.py b/src/aipass/seedgo/apps/handlers/aipass_standards/json_structure_content.py index c6c5af15..22c7908d 100644 --- a/src/aipass/seedgo/apps/handlers/aipass_standards/json_structure_content.py +++ b/src/aipass/seedgo/apps/handlers/aipass_standards/json_structure_content.py @@ -42,7 +42,12 @@ def get_json_structure_standards() -> str: "", " [bold]Location:[/bold] All JSON goes to [green]{branch}_json/[/green] at branch root", " [dim]src/aipass/{branch}/{branch}_json/[/dim]", - " One directory, no splits.", + " One directory, no arbitrary splits.", + "", + " [bold]Operator Config:[/bold] [green]{branch}_json/custom_config/[/green]", + " The sanctioned subdir for human-editable runtime settings.", + " Separates operator-tunable config from auto-generated logs/data.", + " [dim]Examples: cadence_config.json, memory.config.json[/dim]", "", "─" * 70, "", @@ -126,6 +131,7 @@ def get_json_structure_standards() -> str: " [green]Checked:[/green]", " [green]+[/green] Modules: [dim]apps/modules/*.py[/dim]", " [green]+[/green] Handlers: [dim]apps/handlers/**/*.py[/dim]", + " [green]+[/green] Directory: [dim]{branch}_json/[/dim] subdirs (post-audit)", "", " [red]Skipped:[/red]", " [red]-[/red] [dim]__init__.py[/dim] files (structural, not functional)", @@ -146,6 +152,22 @@ def get_json_structure_standards() -> str: "", "─" * 70, "", + "[bold cyan]DIRECTORY STRUCTURE:[/bold cyan]", + "", + " [dim]{branch}_json/[/dim] is flat by default — all auto-generated JSON at root.", + " One sanctioned subdir: [green]custom_config/[/green] for operator-editable settings.", + "", + " [dim]{branch}_json/[/dim]", + " [dim]├── config.json[/dim] [dim]# auto-generated[/dim]", + " [dim]├── data.json[/dim] [dim]# auto-generated[/dim]", + " [dim]├── log.json[/dim] [dim]# auto-generated[/dim]", + " [dim]└── custom_config/[/dim] [dim]# operator-editable[/dim]", + "", + " Any other subdir is a split violation and will be flagged.", + " Hidden dirs (e.g. [dim].archive/[/dim]) are exempt.", + "", + "─" * 70, + "", "[bold cyan]KEY WARNINGS:[/bold cyan]", " [yellow]![/yellow] The CODE PATTERN is the template -- no json_templates/ directory", " [yellow]![/yellow] JSON files auto-create on first log_operation() call", diff --git a/src/aipass/seedgo/tests/test_aipass_standards.py b/src/aipass/seedgo/tests/test_aipass_standards.py index 6db3a9e0..58758bb0 100644 --- a/src/aipass/seedgo/tests/test_aipass_standards.py +++ b/src/aipass/seedgo/tests/test_aipass_standards.py @@ -133,6 +133,86 @@ def test_json_structure_check_has_standard_field(tmp_path): assert "standard" in result +def test_json_structure_custom_config_subdir_passes(tmp_path): + """Branch with {branch}_json/custom_config/ passes directory check.""" + 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() + cc = json_dir / "custom_config" + cc.mkdir() + (cc / "settings.json").write_text("{}", encoding="utf-8") + (json_dir / "config.json").write_text("{}", encoding="utf-8") + + violations = _check_json_dir_structure(str(branch)) + assert violations == [] + + +def test_json_structure_random_subdir_fails(tmp_path): + """Branch with an unsanctioned subdir under {branch}_json/ is 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 / "custom_config").mkdir() + (json_dir / "extra_stuff").mkdir() + + violations = _check_json_dir_structure(str(branch)) + assert len(violations) == 1 + assert "extra_stuff" in violations[0]["message"] + + +def test_json_structure_hidden_subdir_ignored(tmp_path): + """Hidden subdirs (e.g. .archive) under {branch}_json/ are 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 / ".archive").mkdir() + + violations = _check_json_dir_structure(str(branch)) + assert violations == [] + + +def test_json_structure_no_json_dir_passes(tmp_path): + """Branch with no {branch}_json/ directory produces no violations.""" + from aipass.seedgo.apps.handlers.aipass_standards.json_structure_check import _check_json_dir_structure + + branch = tmp_path / "mybranch" + branch.mkdir() + + violations = _check_json_dir_structure(str(branch)) + assert violations == [] + + +def test_json_structure_check_branch_post(tmp_path): + """check_branch_post returns violations and scores.""" + from aipass.seedgo.apps.handlers.aipass_standards.json_structure_check import check_branch_post + + branch = tmp_path / "mybranch" + branch.mkdir() + json_dir = branch / "mybranch_json" + json_dir.mkdir() + (json_dir / "bad_split").mkdir() + + violations, scores = check_branch_post(str(branch)) + assert len(violations) == 1 + assert scores == [0] + + # Clean branch + (json_dir / "bad_split").rmdir() + (json_dir / "custom_config").mkdir() + violations2, scores2 = check_branch_post(str(branch)) + assert violations2 == [] + assert scores2 == [100] + + # --------------------------------------------------------------------------- # Tests -- naming_check.is_bypassed # ---------------------------------------------------------------------------