#643: codify custom_config/ as a json_structure standard — ALLOWED_JSON_SUBDIRS allowlist + check_branch_post() validates {branch}_json/ subdirs (custom_config/ + hidden dirs pass, other splits flagged); json_structure_content.py documents the structure + operator-config location. 5 tests. Surfaced devpulse_json/compass/ as unsanctioned (devpulse bypass next commit).
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user