diff --git a/src/aipass/seedgo/apps/handlers/aipass_proof/__init__.py b/src/aipass/seedgo/apps/handlers/aipass_proof/__init__.py new file mode 100644 index 00000000..86239a5f --- /dev/null +++ b/src/aipass/seedgo/apps/handlers/aipass_proof/__init__.py @@ -0,0 +1,2 @@ +# aipass_proof — Proof handlers for the aipass standards pack. +# Discovery: seedgo_proof module finds *_proof/ dirs, runs *_proof.py files inside. diff --git a/src/aipass/seedgo/apps/handlers/aipass_proof/content_naming.md b/src/aipass/seedgo/apps/handlers/aipass_proof/content_naming.md new file mode 100644 index 00000000..3d4de8a0 --- /dev/null +++ b/src/aipass/seedgo/apps/handlers/aipass_proof/content_naming.md @@ -0,0 +1,89 @@ +# Content Naming Proof — Function Name Convention +**Version:** 1.0.0 +**Created:** 2026-03-22 +**Modified:** 2026-03-22 +**Status:** Placeholder + +--- + +## Purpose + +The content naming proof verifies that every `*_content.py` file exports a function whose name matches the convention the `standards_query` module expects. The query system derives the function name from the filename — if the name doesn't match, the query fails silently. + +--- + +## The Naming Convention + +The convention is deterministic: the standard name is extracted from the filename, then used to construct the function name. + +``` +Filename: {name}_content.py +Function: get_{name}_standards() +``` + +### Correct Examples + +| File | Expected Function | +|------|-------------------| +| `architecture_content.py` | `get_architecture_standards()` | +| `imports_content.py` | `get_imports_standards()` | +| `log_handler_content.py` | `get_log_handler_standards()` | +| `error_handling_content.py` | `get_error_handling_standards()` | +| `json_structure_content.py` | `get_json_structure_standards()` | + +### Incorrect Examples + +| File | Wrong Function | Why | +|------|----------------|-----| +| `architecture_content.py` | `get_architecture_content()` | Suffix must be `_standards`, not `_content` | +| `architecture_content.py` | `get_arch_standards()` | Name must match full prefix, no abbreviations | +| `imports_content.py` | `get_import_standards()` | Must be `imports` (plural), exactly as in filename | + +--- + +## How standards_query Resolves Function Names + +1. Discover all `*_content.py` files via glob +2. For each file, extract the name: strip `_content.py` suffix to get `{name}` +3. Construct expected function name: `get_{name}_standards` +4. Import the module and call `getattr(module, function_name)` +5. If the attribute doesn't exist, the standard silently has no queryable content + + + +--- + +## What the Scan Checks + +1. **Glob** all `*_content.py` files in the pack +2. **Extract** the standard name from each filename +3. **Import** the module (or parse with AST) +4. **Verify** the expected `get_{name}_standards` function exists +5. **Report** any mismatches + +--- + +## Common Failures + +- **Wrong suffix**: `get_{name}_content()` instead of `get_{name}_standards()` +- **Abbreviated name**: `get_arch_standards()` instead of `get_architecture_standards()` +- **Missing function**: File exists but contains no matching function at all +- **Typo in name**: `get_arcitecture_standards()` — close but not exact + +--- + +## How to Fix + +1. Open the content file +2. Extract the standard name from the filename (everything before `_content.py`) +3. Ensure the file contains exactly: `def get_{name}_standards() -> str:` +4. Verify the function returns a string of formatted content + +--- + +## Related + +- **DPLAN-0044**: Self-audit tooling design +- **tools/content_naming_scanner.py**: Original prototype +- **Checker**: `content_naming.py` in this directory diff --git a/src/aipass/seedgo/apps/handlers/aipass_proof/content_naming.py b/src/aipass/seedgo/apps/handlers/aipass_proof/content_naming.py new file mode 100644 index 00000000..e3ab6361 --- /dev/null +++ b/src/aipass/seedgo/apps/handlers/aipass_proof/content_naming.py @@ -0,0 +1,141 @@ +# =================== AIPass ==================== +# Name: content_naming.py +# Description: Verify content file naming convention (get_{name}_standards) +# Version: 1.0.0 +# Created: 2026-03-22 +# Modified: 2026-03-22 +# ============================================= + +"""Content Naming Proof -- Verify every *_content.py has correctly named get_{name}_standards(). + +Convention: {name}_content.py must provide a get_{name}_standards() function at module level. +This is how standards_query discovers and calls content providers. + +Interface: + scan(pack_dir: Path) -> dict + Returns: {"passed": bool, "total": int, "correct": list, "incorrect": list, + "issues": list, "summary": str} + +Reference: tools/content_naming_scanner.py (original prototype) +""" + +from __future__ import annotations + +import ast +from pathlib import Path + +from aipass.prax import logger +from aipass.seedgo.apps.handlers.json import json_handler + +# Directories to skip during scanning +_SKIP_DIRS = {".archive", ".sorting_unprocessed", "__pycache__"} + + +def _parse_public_functions(file_path: Path) -> list[str]: + """Parse a Python file and return module-level public function names. + + Uses ast.parse to inspect the file without importing it. Only returns + top-level FunctionDef nodes whose names do not start with underscore. + + Args: + file_path: Path to the Python file to parse. + + Returns: + List of public function names found at module level. + """ + source = file_path.read_text(encoding="utf-8") + tree = ast.parse(source, filename=str(file_path)) + return [ + node.name + for node in ast.iter_child_nodes(tree) + if isinstance(node, ast.FunctionDef) and not node.name.startswith("_") + ] + + +def scan(pack_dir: Path) -> dict: + """Scan all *_content.py files and verify naming conventions. + + For each content file, derives the expected function name + (get_{stem}_standards) and checks whether it exists at module level + via AST parsing. + + Args: + pack_dir: Path to the standards pack directory (e.g. handlers/aipass_standards/). + + Returns: + Dict with keys: passed, total, correct, incorrect, issues, summary. + """ + logger.info(f"[content_naming] Scanning {pack_dir}") + + correct: list[str] = [] + incorrect: list[str] = [] + issues: list[dict[str, str | list[str]]] = [] + + if not pack_dir.is_dir(): + return { + "passed": True, + "total": 0, + "correct": [], + "incorrect": [], + "issues": [{"file": str(pack_dir), "issue": "Directory does not exist"}], + "summary": "0 content files (directory missing)", + } + + for content_file in sorted(pack_dir.glob("*_content.py")): + # Skip files inside excluded directories + if any(part in _SKIP_DIRS for part in content_file.relative_to(pack_dir).parts): + continue + + # Skip underscore-prefixed files (__init__.py, _helpers.py, etc.) + if content_file.name.startswith("_"): + continue + + # Derive expected function name: {name}_content.py -> get_{name}_standards + stem = content_file.stem.removesuffix("_content") + expected_fn = f"get_{stem}_standards" + + # Parse via AST -- no imports + try: + public_fns = _parse_public_functions(content_file) + except SyntaxError as exc: + incorrect.append(content_file.name) + issues.append({ + "file": content_file.name, + "expected": expected_fn, + "issue": f"SyntaxError: {exc}", + }) + continue + + if expected_fn in public_fns: + correct.append(content_file.name) + else: + incorrect.append(content_file.name) + issues.append({ + "file": content_file.name, + "expected": expected_fn, + "found_functions": public_fns, + "issue": f"Missing expected function {expected_fn}()", + }) + + total = len(correct) + len(incorrect) + passed = len(incorrect) == 0 + + # Build human-readable summary + parts = [f"{len(correct)} correct"] + if incorrect: + parts.append(f"{len(incorrect)} incorrect") + parts.append(f"{total} total") + summary = " | ".join(parts) + + logger.info(f"[content_naming] Result: {summary}") + + json_handler.log_operation("proof_scan", {"proof": "content_naming", "passed": passed}) + + return { + "passed": passed, + "total": total, + "correct": correct, + "incorrect": incorrect, + "issues": issues, + "summary": summary, + } diff --git a/src/aipass/seedgo/apps/handlers/aipass_proof/content_naming_content.py b/src/aipass/seedgo/apps/handlers/aipass_proof/content_naming_content.py new file mode 100644 index 00000000..f5d053a4 --- /dev/null +++ b/src/aipass/seedgo/apps/handlers/aipass_proof/content_naming_content.py @@ -0,0 +1,81 @@ +# =================== AIPass ==================== +# Name: content_naming_content.py +# Description: Queryable content for the content naming proof +# Version: 1.0.0 +# Created: 2026-03-22 +# Modified: 2026-03-22 +# ============================================= + +""" +Content Naming Proof Content Handler + +Provides formatted content naming proof content for the query system. +Module orchestrates, handler implements. +""" + +from aipass.seedgo.apps.handlers.json import json_handler + + +def get_content_naming_proof() -> str: + """Return content naming proof content for query system. + + Returns: + str: Formatted proof text with Rich styling + """ + sep = "\u2500" * 70 + lines = [ + f"[dim]{sep}[/dim]", + "[bold red]CONTENT NAMING PROOF \u2014 Function Name Convention[/bold red]", + "[dim]Every *_content.py must have get_{name}_standards() matching its filename.[/dim]", + f"[dim]{sep}[/dim]", + "", + "[bold cyan]WHAT THIS CHECKS:[/bold cyan]", + " Each content file [dim]{name}_content.py[/dim] must export a function named", + " [dim]get_{name}_standards()[/dim] that the standards_query module can call.", + "", + "[bold cyan]THE NAMING CONVENTION:[/bold cyan]", + " [yellow]File:[/yellow] [dim]architecture_content.py[/dim]", + " [yellow]Function:[/yellow] [dim]get_architecture_standards()[/dim]", + "", + " [yellow]File:[/yellow] [dim]imports_content.py[/dim]", + " [yellow]Function:[/yellow] [dim]get_imports_standards()[/dim]", + "", + " [yellow]File:[/yellow] [dim]log_handler_content.py[/dim]", + " [yellow]Function:[/yellow] [dim]get_log_handler_standards()[/dim]", + "", + "[bold cyan]WHY IT MATTERS:[/bold cyan]", + " The [dim]standards_query[/dim] module calls this exact function name to retrieve", + " content. It derives the function name from the filename:", + " [dim]{name}_content.py[/dim] \u2192 [dim]get_{name}_standards()[/dim]", + "", + " Wrong name = query fails silently. The standard appears to have no", + " content even though the file exists.", + "", + f"[dim]{sep}[/dim]", + "[bold cyan]COMMON FAILURES:[/bold cyan]", + "", + "[yellow]Wrong function name:[/yellow]", + " [red]\u2717[/red] [dim]def get_architecture_content()[/dim] \u2014 wrong suffix", + " [red]\u2717[/red] [dim]def get_arch_standards()[/dim] \u2014 abbreviated name", + " [green]\u2713[/green] [dim]def get_architecture_standards()[/dim]", + "", + "[yellow]Missing function entirely:[/yellow]", + " [red]\u2717[/red] File exists but has no function matching the convention", + "", + # TODO: Expand with details on how standards_query resolves + # function names and what happens on resolution failure. + "", + f"[dim]{sep}[/dim]", + "[bold cyan]HOW TO FIX:[/bold cyan]", + " [yellow]1.[/yellow] Extract the standard name from the filename: [dim]{name}_content.py[/dim] \u2192 [dim]{name}[/dim]", + " [yellow]2.[/yellow] Ensure the file contains: [dim]def get_{name}_standards() -> str:[/dim]", + " [yellow]3.[/yellow] Verify the function returns a string of formatted content", + "", + "[bold cyan]RELATED:[/bold cyan]", + " [dim]DPLAN-0044: Self-audit tooling design[/dim]", + " [dim]tools/content_naming_scanner.py: Original prototype[/dim]", + f"[dim]{sep}[/dim]", + ] + + json_handler.log_operation("proof_content_queried", {"proof": "content_naming"}) + return "\n".join(lines) diff --git a/src/aipass/seedgo/apps/handlers/aipass_proof/interface.md b/src/aipass/seedgo/apps/handlers/aipass_proof/interface.md new file mode 100644 index 00000000..fe1d9244 --- /dev/null +++ b/src/aipass/seedgo/apps/handlers/aipass_proof/interface.md @@ -0,0 +1,116 @@ +# Interface Proof — Checker Interface Compliance +**Version:** 1.0.0 +**Created:** 2026-03-22 +**Modified:** 2026-03-22 +**Status:** Placeholder + +--- + +## Purpose + +The interface proof verifies that every checker in a pack declares the correct `AUDIT_SCOPE` variable and uses the function signature that the audit engine expects. Without these, the audit engine cannot scope or call the checker correctly. + +--- + +## AUDIT_SCOPE + +Every checker must declare `AUDIT_SCOPE` at module level. This tells the audit engine what to pass to the checker. + +| Value | Meaning | What the checker receives | +|-------|---------|--------------------------| +| `"all_files"` | Checker runs once per Python file in the branch | `file_path`, `module_name`, `branch_path` | +| `"entry_point"` | Checker runs only on the main entry point | `file_path`, `module_name`, `branch_path` | +| `"branch_level"` | Checker runs once for the whole branch | `branch_path`, `module_name` | + +```python +# Example declaration +AUDIT_SCOPE = "all_files" +``` + +--- + +## Function Signatures + +The audit engine calls a specific function depending on the AUDIT_SCOPE: + +### File-Level Checkers (`all_files` / `entry_point`) + +```python +def check_module(file_path: str, module_name: str, branch_path: str) -> dict: + """Validate a single file against the standard. + + Args: + file_path: Absolute path to the file being checked + module_name: Name of the module (derived from filename) + branch_path: Absolute path to the branch root + + Returns: + dict with at minimum: {"passed": bool, "issues": list} + """ +``` + +### Branch-Level Checkers (`branch_level`) + +```python +def check_branch(branch_path: str, module_name: str) -> dict: + """Validate the branch as a whole against the standard. + + Args: + branch_path: Absolute path to the branch root + module_name: Name of the standard being checked + + Returns: + dict with at minimum: {"passed": bool, "issues": list} + """ +``` + +--- + +## How the Audit Engine Discovers and Calls Checkers + +1. **Discovery**: Glob `*_check.py` in the pack directory +2. **Import**: `importlib.import_module()` on each discovered file +3. **Scope read**: Read `AUDIT_SCOPE` from the module +4. **Dispatch**: Based on scope, call `check_module()` or `check_branch()` with the correct arguments +5. **Collect results**: Aggregate return dicts into the audit report + +If `AUDIT_SCOPE` is missing, the engine cannot determine how to call the checker. If the function signature is wrong, the call raises `TypeError` at runtime. + +--- + +## What the Scan Checks + +1. **AUDIT_SCOPE exists** as a module-level variable in every `*_check.py` +2. **AUDIT_SCOPE value** is one of the three valid strings +3. **Function name** matches the scope: `check_module` for file-level, `check_branch` for branch-level +4. **Parameter count** matches the expected signature + + + +--- + +## Common Failures + +| Failure | Impact | +|---------|--------| +| Missing `AUDIT_SCOPE` | Audit engine skips or crashes on the checker | +| Wrong function name (`check` instead of `check_module`) | `AttributeError` at runtime | +| Missing parameters | `TypeError` when audit engine calls with expected args | +| Invalid scope value | Engine doesn't know how to route the checker | + +--- + +## How to Fix + +1. Add `AUDIT_SCOPE = "all_files"` (or `"entry_point"` / `"branch_level"`) at the top of the checker +2. Ensure the function name is `check_module` (file-level) or `check_branch` (branch-level) +3. Ensure all parameters are present in the signature + +--- + +## Related + +- **DPLAN-0044**: Self-audit tooling design +- **tools/interface_scanner.py**: Original prototype +- **Checker**: `interface.py` in this directory diff --git a/src/aipass/seedgo/apps/handlers/aipass_proof/interface.py b/src/aipass/seedgo/apps/handlers/aipass_proof/interface.py new file mode 100644 index 00000000..89958ff8 --- /dev/null +++ b/src/aipass/seedgo/apps/handlers/aipass_proof/interface.py @@ -0,0 +1,238 @@ +# =================== AIPass ==================== +# Name: interface.py +# Description: Verify checker interface (AUDIT_SCOPE + function signature) +# Version: 1.0.0 +# Created: 2026-03-22 +# Modified: 2026-03-22 +# ============================================= + +"""Interface Proof -- Verify each checker declares AUDIT_SCOPE and correct function signature. + +Every *_check.py in a standards pack must: + 1. Declare AUDIT_SCOPE at module level (one of: all_files, entry_point, branch_level) + 2. Implement the correct entry function (check_module or check_branch) + 3. Have the correct first parameter (module_path or branch_path) plus bypass_rules + +Interface: + scan(pack_dir: Path) -> dict + Returns: {"passed": bool, "total": int, "pass_count": int, "fail_count": int, + "results": list[dict], "issues": list, "summary": str} + +Reference: tools/interface_scanner.py (original prototype) +""" + +from __future__ import annotations + +import ast +from pathlib import Path + +from aipass.prax import logger +from aipass.seedgo.apps.handlers.json import json_handler + +VALID_SCOPES = {"all_files", "entry_point", "branch_level"} + +# Directories / files to skip inside the pack +_SKIP_DIRS = frozenset({".archive", ".sorting_unprocessed"}) + + +# -- AST helpers --------------------------------------------------------------- + + +def _extract_audit_scope(tree: ast.Module) -> str | None: + """Return the string value of AUDIT_SCOPE if defined at module level.""" + for node in ast.iter_child_nodes(tree): + if not isinstance(node, ast.Assign): + continue + for target in node.targets: + if isinstance(target, ast.Name) and target.id == "AUDIT_SCOPE": + if isinstance(node.value, ast.Constant) and isinstance( + node.value.value, str + ): + return node.value.value + return None + + +def _find_function(tree: ast.Module, name: str) -> ast.FunctionDef | None: + """Return the FunctionDef node for *name* if it exists at module level.""" + for node in ast.iter_child_nodes(tree): + if isinstance(node, ast.FunctionDef) and node.name == name: + return node + return None + + +def _check_params(func: ast.FunctionDef, expected_first: str) -> tuple[bool, str]: + """Verify the function has (expected_first, bypass_rules=None). + + Returns (ok, detail_message). + """ + args = func.args + positional = [a.arg for a in args.posonlyargs] + [a.arg for a in args.args] + + if not positional: + return False, "no parameters" + + if positional[0] != expected_first: + return False, f"first param is '{positional[0]}', expected '{expected_first}'" + + if len(positional) < 2 or positional[1] != "bypass_rules": + # Also accept bypass_rules as a keyword-only arg + kw_names = [a.arg for a in args.kwonlyargs] + if "bypass_rules" not in kw_names: + return False, "missing 'bypass_rules' parameter" + + return True, "ok" + + +# -- Skip logic ---------------------------------------------------------------- + + +def _should_skip(path: Path) -> bool: + """Return True if the file should be excluded from scanning.""" + if path.name == "__init__.py": + return True + # Skip underscore-prefixed files (but not dunder) + if path.name.startswith("_") and not path.name.startswith("__"): + return True + for part in path.parts: + if part in _SKIP_DIRS: + return True + return False + + +# -- Core scan ----------------------------------------------------------------- + + +def scan(pack_dir: Path) -> dict: + """Scan all *_check.py files in *pack_dir* and verify interface compliance. + + Args: + pack_dir: Path to the standards pack directory (e.g. handlers/aipass_standards/). + + Returns: + Dict with keys: passed, total, pass_count, fail_count, results, issues, summary. + """ + results: list[dict] = [] + issues: list[str] = [] + + if not pack_dir.is_dir(): + msg = f"Pack directory not found: {pack_dir}" + logger.warning(msg) + return { + "passed": False, + "total": 0, + "pass_count": 0, + "fail_count": 0, + "results": results, + "issues": [msg], + "summary": msg, + } + + check_files = sorted(pack_dir.glob("*_check.py")) + + for check_file in check_files: + if _should_skip(check_file): + continue + + name = check_file.stem # e.g. "cli_check" + standard_name = name.removesuffix("_check") # e.g. "cli" + + entry: dict = { + "file": check_file.name, + "standard": standard_name, + "audit_scope": None, + "scope_valid": False, + "has_function": False, + "expected_function": None, + "params_ok": False, + "params_detail": "", + "compliant": False, + "issues": [], + } + + # Parse the file + try: + source = check_file.read_text(encoding="utf-8") + tree = ast.parse(source, filename=str(check_file)) + except SyntaxError as exc: + err = f"{check_file.name}: SyntaxError: {exc.msg} (line {exc.lineno})" + entry["issues"].append(err) + issues.append(err) + results.append(entry) + continue + + # 1. Check AUDIT_SCOPE + scope = _extract_audit_scope(tree) + entry["audit_scope"] = scope + + if scope is None: + err = f"{check_file.name}: AUDIT_SCOPE not defined" + entry["issues"].append(err) + issues.append(err) + elif scope not in VALID_SCOPES: + err = f"{check_file.name}: AUDIT_SCOPE='{scope}' not in {VALID_SCOPES}" + entry["issues"].append(err) + issues.append(err) + else: + entry["scope_valid"] = True + + # 2. Determine expected function based on scope + if scope in ("all_files", "entry_point", None): + expected_func = "check_module" + expected_first_param = "module_path" + else: # branch_level + expected_func = "check_branch" + expected_first_param = "branch_path" + + entry["expected_function"] = expected_func + + # 3. Check function exists + func_node = _find_function(tree, expected_func) + + if func_node is None: + err = f"{check_file.name}: missing {expected_func}()" + entry["has_function"] = False + entry["issues"].append(err) + issues.append(err) + else: + entry["has_function"] = True + + # 4. Check parameters + ok, detail = _check_params(func_node, expected_first_param) + entry["params_ok"] = ok + entry["params_detail"] = detail + if not ok: + err = f"{check_file.name}: {expected_func}() params: {detail}" + entry["issues"].append(err) + issues.append(err) + + # Final compliance + entry["compliant"] = ( + entry["scope_valid"] and entry["has_function"] and entry["params_ok"] + ) + results.append(entry) + + pass_count = sum(1 for r in results if r["compliant"]) + fail_count = len(results) - pass_count + total = len(results) + passed = fail_count == 0 and total > 0 + + if passed: + summary = f"All {total} checkers comply with the interface." + elif total == 0: + summary = "No *_check.py files found in pack directory." + else: + summary = f"{fail_count}/{total} checkers have interface issues." + + logger.info(f"interface proof: {summary}") + + json_handler.log_operation("proof_scan", {"proof": "interface", "passed": passed}) + + return { + "passed": passed, + "total": total, + "pass_count": pass_count, + "fail_count": fail_count, + "results": results, + "issues": issues, + "summary": summary, + } diff --git a/src/aipass/seedgo/apps/handlers/aipass_proof/interface_content.py b/src/aipass/seedgo/apps/handlers/aipass_proof/interface_content.py new file mode 100644 index 00000000..c370a342 --- /dev/null +++ b/src/aipass/seedgo/apps/handlers/aipass_proof/interface_content.py @@ -0,0 +1,83 @@ +# =================== AIPass ==================== +# Name: interface_content.py +# Description: Queryable content for the interface proof +# Version: 1.0.0 +# Created: 2026-03-22 +# Modified: 2026-03-22 +# ============================================= + +""" +Interface Proof Content Handler + +Provides formatted interface proof content for the query system. +Module orchestrates, handler implements. +""" + +from aipass.seedgo.apps.handlers.json import json_handler + + +def get_interface_proof() -> str: + """Return interface proof content for query system. + + Returns: + str: Formatted proof text with Rich styling + """ + sep = "\u2500" * 70 + lines = [ + f"[dim]{sep}[/dim]", + "[bold red]INTERFACE PROOF \u2014 Checker Interface Compliance[/bold red]", + "[dim]Every checker must declare AUDIT_SCOPE and use the correct function signature.[/dim]", + f"[dim]{sep}[/dim]", + "", + "[bold cyan]WHAT THIS CHECKS:[/bold cyan]", + " [yellow]1.[/yellow] AUDIT_SCOPE variable is declared at module level", + " [yellow]2.[/yellow] Function signature matches the expected pattern", + " [yellow]3.[/yellow] Parameters match the audit engine's calling convention", + "", + "[bold cyan]AUDIT_SCOPE VALUES:[/bold cyan]", + ' [green]"all_files"[/green] \u2014 Checker receives every file in the branch', + ' [green]"entry_point"[/green] \u2014 Checker receives only the main entry point', + ' [green]"branch_level"[/green] \u2014 Checker receives the branch root path', + "", + "[bold cyan]FUNCTION SIGNATURES:[/bold cyan]", + ' [yellow]all_files / entry_point:[/yellow] [dim]def check_module(file_path, module_name, branch_path)[/dim]', + ' [yellow]branch_level:[/yellow] [dim]def check_branch(branch_path, module_name)[/dim]', + "", + "[bold cyan]WHY IT MATTERS:[/bold cyan]", + " Without AUDIT_SCOPE, the audit engine can't scope the checker.", + " It won't know whether to pass individual files or the branch root.", + " Without the correct function signature, the checker crashes at runtime.", + "", + f"[dim]{sep}[/dim]", + "[bold cyan]COMMON FAILURES:[/bold cyan]", + "", + "[yellow]Missing AUDIT_SCOPE:[/yellow]", + " [red]\u2717[/red] Checker has no AUDIT_SCOPE declaration at all", + " [green]\u2713[/green] [dim]AUDIT_SCOPE = \"all_files\"[/dim]", + "", + "[yellow]Wrong function name:[/yellow]", + " [red]\u2717[/red] [dim]def check(file_path, module_name, branch_path)[/dim]", + " [green]\u2713[/green] [dim]def check_module(file_path, module_name, branch_path)[/dim]", + "", + "[yellow]Wrong parameters:[/yellow]", + " [red]\u2717[/red] [dim]def check_module(file_path)[/dim] (missing module_name, branch_path)", + " [green]\u2713[/green] [dim]def check_module(file_path, module_name, branch_path)[/dim]", + "", + # TODO: Expand with actual validation logic details once the + # interface scanner is fully implemented. + "", + f"[dim]{sep}[/dim]", + "[bold cyan]HOW TO FIX:[/bold cyan]", + ' [yellow]1.[/yellow] Add [dim]AUDIT_SCOPE = "all_files" | "entry_point" | "branch_level"[/dim]', + " [yellow]2.[/yellow] Ensure function matches expected interface:", + " [dim]check_module(file_path, module_name, branch_path)[/dim] for file-level", + " [dim]check_branch(branch_path, module_name)[/dim] for branch-level", + "", + "[bold cyan]RELATED:[/bold cyan]", + " [dim]DPLAN-0044: Self-audit tooling design[/dim]", + " [dim]tools/interface_scanner.py: Original prototype[/dim]", + f"[dim]{sep}[/dim]", + ] + + json_handler.log_operation("proof_content_queried", {"proof": "interface"}) + return "\n".join(lines) diff --git a/src/aipass/seedgo/apps/handlers/aipass_proof/plugin_integrity.md b/src/aipass/seedgo/apps/handlers/aipass_proof/plugin_integrity.md new file mode 100644 index 00000000..043b3076 --- /dev/null +++ b/src/aipass/seedgo/apps/handlers/aipass_proof/plugin_integrity.md @@ -0,0 +1,139 @@ +# Plugin Integrity Proof — Dynamic Discovery Contract +**Version:** 1.0.0 +**Created:** 2026-03-22 +**Modified:** 2026-03-22 +**Status:** Placeholder + +--- + +## Purpose + +The plugin integrity proof verifies that core audit modules do not hardcode standard names. The seedgo audit system uses a plugin architecture — standards are discovered dynamically via `glob` + `importlib`. If core modules hardcode standard names, the "drop-in" contract breaks: adding a new standard by dropping a file into the pack no longer works without also editing core code. + +--- + +## Modules Scanned + +These are the core audit modules that must stay clean of hardcoded standard names: + +| Module | Role | +|--------|------| +| `standards_audit.py` | Discovers and executes checkers via glob + importlib | +| `branch_audit.py` | Orchestrates branch-level audit runs | +| `audit_display.py` | Formats and displays audit results to the terminal | +| `seedgo.py` | Main entry point for the seedgo system | + +--- + +## What Counts as Hardcoded + +A standard name is "hardcoded" when it appears as a string literal inside a core module in a context that affects logic or routing. The scanner checks for known standard names (e.g., `"architecture"`, `"encapsulation"`, `"imports"`) appearing in these modules. + +### The AMBIGUOUS_NAMES Filter + +Some standard names are common English words that appear in non-standard contexts. The scanner maintains an `AMBIGUOUS_NAMES` set to filter false positives: + + + +Examples of ambiguous names that may need filtering: +- `"meta"` — appears in Python metadata contexts +- `"naming"` — appears in general discussions of naming +- `"testing"` — appears in general testing references +- `"trigger"` — appears in event/action contexts + +--- + +## Cosmetic vs Structural Hardcoding + +Not all hardcoded references have the same severity: + +### Structural (must fix) + +Standard names used in logic that affects which standards run or how results are processed: + +```python +# BAD — hardcoded routing +if standard_name == "architecture": + result = check_architecture(branch_path) +elif standard_name == "encapsulation": + result = check_encapsulation(branch_path) +``` + +```python +# GOOD — dynamic iteration +for checker in discovered_checkers: + result = checker.check_module(file_path, module_name, branch_path) +``` + +### Cosmetic (upgrade target) + +Standard names in display strings, section headers, or help text: + +```python +# Cosmetic — doesn't affect logic, but still couples display to specific standards +console.print("[bold]Architecture:[/bold]") +console.print(results["architecture"]["summary"]) +``` + +**`audit_display.py`** has known cosmetic hardcoding. This is tracked as **DPLAN-0047** — the upgrade path is to iterate result dicts dynamically instead of hardcoding section order. + +--- + +## Known Special Cases + +### branch_audit.py — 3 Legitimate Cases + +`branch_audit.py` has 3 standards that require special handling because they operate at a different scope than file-level checkers: + +1. **architecture** — Template baseline check requires branch-level context +2. **meta** — Passport/trinity validation requires branch-level context +3. **testing** — Test discovery requires branch-level context + +These are not bugs — they are architectural necessities documented in the checker interfaces. They use `AUDIT_SCOPE = "branch_level"` which requires distinct calling logic. + +### audit_display.py — Cosmetic References + +Display formatting references specific standard names for section headers and ordering. This is functional but not ideal. The upgrade path (DPLAN-0047) would replace hardcoded sections with dynamic iteration over whatever results the audit returns. + +--- + +## What the Scan Checks + +1. **Read** each core module's source code +2. **Extract** all string literals +3. **Compare** against the known standard names list +4. **Filter** through AMBIGUOUS_NAMES to remove false positives +5. **Classify** remaining matches as structural or cosmetic +6. **Report** violations with file, line number, and context + + + +--- + +## How to Fix + +1. **Structural hardcoding**: Replace with dynamic iteration over discovered checkers or result dicts +2. **If/elif chains**: Convert to loop over `glob('*_check.py')` results +3. **Dict key references**: Use `.items()` iteration instead of hardcoded key access +4. **Display sections**: Iterate result keys dynamically (DPLAN-0047 upgrade) +5. **Document special cases**: If a reference is truly necessary, document why in the code and in this proof + +--- + +## The Drop-In Contract + +The fundamental promise of the plugin architecture: + +> To add a new standard to a pack, drop `{name}_check.py` into the pack directory. The audit engine discovers it automatically. No other files need to change. + +Every hardcoded standard name in a core module is a violation of this contract. It means adding a standard requires editing core code, which defeats the purpose of plugin discovery. + +--- + +## Related + +- **DPLAN-0044**: Self-audit tooling design +- **DPLAN-0047**: audit_display.py cosmetic hardcoding upgrade +- **tools/plugin_integrity_scanner.py**: Original prototype +- **Checker**: `plugin_integrity.py` in this directory diff --git a/src/aipass/seedgo/apps/handlers/aipass_proof/plugin_integrity.py b/src/aipass/seedgo/apps/handlers/aipass_proof/plugin_integrity.py new file mode 100644 index 00000000..999f7ece --- /dev/null +++ b/src/aipass/seedgo/apps/handlers/aipass_proof/plugin_integrity.py @@ -0,0 +1,518 @@ +# =================== AIPass ==================== +# Name: plugin_integrity.py +# Description: Verify core audit modules don't hardcode standard names +# Version: 1.0.0 +# Created: 2026-03-22 +# Modified: 2026-03-22 +# ============================================= + +"""Plugin Integrity Proof -- Verify plugin architecture stays clean. + +The plugin architecture depends on dynamic discovery (glob + importlib). +Core audit modules should never reference specific standard names in code +logic. This handler scans target modules with AST + regex and reports +any hardcoded standard-name references. + +Interface: + scan(pack_dir: Path) -> dict + Returns: {"passed": bool, "issues": list, "summary": str, ...} + +Note: audit_display.py cosmetic refs are a known upgrade target (DPLAN-0047). + +Reference: tools/plugin_integrity_scanner.py (original prototype) +""" + +from __future__ import annotations + +import ast +import re +from pathlib import Path + +from aipass.prax import logger +from aipass.seedgo.apps.handlers.json import json_handler + + +# ============================================================================= +# CONFIGURATION +# ============================================================================= + +# Modules whose hardcoded refs are cosmetic (display layer, not routing). +COSMETIC_MODULES: set[str] = {"audit_display.py"} + +# Standard names that double as common infrastructure/English words. +# These appear as directory names, import paths, variable names, Rich markup +# labels, etc. -- not as hardcoded standard routing. Never flag bare AST +# string literals for these; only the regex patterns catch truly suspicious use. +AMBIGUOUS_NAMES: set[str] = { + "architecture", + "cli", + "cli_flags", + "documentation", + "encapsulation", + "error_handling", + "handlers", + "imports", + "introspection", + "meta", + "modules", + "naming", + "readme", + "testing", + "trigger", + "shebang", + "permission_flags", +} + + +# ============================================================================= +# TARGET MODULE RESOLUTION +# ============================================================================= + +def _resolve_target_modules( + pack_dir: Path, +) -> list[dict[str, Path | str | bool]]: + """Build the list of core modules to scan, resolved from pack_dir. + + Path layout (from pack_dir = .../apps/handlers/aipass_standards/): + seedgo_root = pack_dir.parent.parent.parent (.../seedgo/) + modules_dir = seedgo_root / "apps" / "modules" + audit_dir = pack_dir.parent / "audit" (sibling handler dir) + entry_point = seedgo_root / "apps" / "seedgo.py" + """ + seedgo_root = pack_dir.parent.parent.parent + modules_dir = seedgo_root / "apps" / "modules" + audit_dir = pack_dir.parent / "audit" + entry_point = seedgo_root / "apps" / "seedgo.py" + + return [ + { + "path": modules_dir / "standards_audit.py", + "label": "standards_audit.py", + "cosmetic": False, + }, + { + "path": audit_dir / "branch_audit.py", + "label": "branch_audit.py", + "cosmetic": False, + }, + { + "path": audit_dir / "audit_display.py", + "label": "audit_display.py", + "cosmetic": True, + }, + { + "path": modules_dir / "standards_query.py", + "label": "standards_query.py", + "cosmetic": False, + }, + { + "path": entry_point, + "label": "seedgo.py", + "cosmetic": False, + }, + ] + + +# ============================================================================= +# STANDARD NAME DISCOVERY +# ============================================================================= + +def _discover_standard_names(pack_dir: Path) -> list[str]: + """Discover standard names from *_check.py files in the pack directory. + + Returns: + Sorted list of standard names (e.g. ["architecture", "cli", ...]). + """ + if not pack_dir.is_dir(): + return [] + names: list[str] = [] + for check_file in sorted(pack_dir.glob("*_check.py")): + name = check_file.stem.removesuffix("_check") + names.append(name) + return names + + +# ============================================================================= +# AST HELPERS +# ============================================================================= + +def _enclosing_context(node: ast.AST, parents: dict[int, ast.AST]) -> str: + """Walk up the parent chain to find the enclosing function/class name.""" + parts: list[str] = [] + current = node + while id(current) in parents: + current = parents[id(current)] + if isinstance(current, ast.FunctionDef | ast.AsyncFunctionDef): + parts.append(f"def {current.name}()") + elif isinstance(current, ast.ClassDef): + parts.append(f"class {current.name}") + if parts: + return " > ".join(reversed(parts)) + return "" + + +def _is_docstring(node: ast.Constant, tree: ast.Module) -> bool: + """Check if a string constant is a docstring.""" + for parent in ast.walk(tree): + body = getattr(parent, "body", None) + if not isinstance(body, list) or not body: + continue + first = body[0] + if ( + isinstance(first, ast.Expr) + and isinstance(first.value, ast.Constant) + and first.value is node + ): + return True + return False + + +def _is_display_string( + node: ast.Constant, + parents: dict[int, ast.AST], +) -> bool: + """Check if a string constant is used in a display/print context. + + Strings passed to console.print(), header(), logger.*, error(), warning() + or used inside f-strings are display text, not hardcoded routing. + """ + parent = parents.get(id(node)) + if parent is None: + return False + + # Inside an f-string -> display formatting + if isinstance(parent, ast.JoinedStr): + return True + + # Direct argument to a display/logging call + if isinstance(parent, ast.Call): + func = parent.func + if isinstance(func, ast.Attribute) and func.attr in ( + "print", "info", "error", "warning", "debug", + ): + return True + if isinstance(func, ast.Name) and func.id in ( + "header", "error", "warning", + ): + return True + + # Keyword arg inside a display call + grandparent = parents.get(id(parent)) + if isinstance(grandparent, ast.Call): + func = grandparent.func + if isinstance(func, ast.Attribute) and func.attr in ( + "print", "log_operation", + ): + return True + if isinstance(func, ast.Name) and func.id in ( + "header", "error", "warning", + ): + return True + + return False + + +def _is_dict_key_access( + node: ast.Constant, + parents: dict[int, ast.AST], +) -> bool: + """Check if a string is a dict key inside generic data-structure access. + + Patterns like result.get('key') or result['key'] when iterating over + dynamic data are not hardcoded routing. + """ + parent = parents.get(id(node)) + if parent is None: + return False + + # .get('key', default) + if isinstance(parent, ast.Call): + func = parent.func + if isinstance(func, ast.Attribute) and func.attr == "get": + return True + + # ['key'] subscript + if isinstance(parent, ast.Subscript): + return True + + return False + + +# ============================================================================= +# AST SCANNER +# ============================================================================= + +def _scan_file_ast( + file_path: Path, + standard_names: list[str], +) -> list[dict[str, str | int]]: + """Parse a file with AST and find string literals matching standard names. + + Filters out docstrings, display strings, dict-key access, and + ambiguous names. Only flags string literals that look like hardcoded + routing. + """ + source = file_path.read_text(encoding="utf-8") + try: + tree = ast.parse(source, filename=str(file_path)) + except SyntaxError: + return [] + + # Build child -> parent map + parents: dict[int, ast.AST] = {} + for parent in ast.walk(tree): + for child in ast.iter_child_nodes(parent): + parents[id(child)] = parent + + name_set = set(standard_names) + findings: list[dict[str, str | int]] = [] + + for node in ast.walk(tree): + if not isinstance(node, ast.Constant): + continue + if not isinstance(node.value, str): + continue + + value = node.value + + if _is_docstring(node, tree): + continue + if _is_display_string(node, parents): + continue + if _is_dict_key_access(node, parents): + continue + if value not in name_set: + continue + if value in AMBIGUOUS_NAMES: + continue + + context = _enclosing_context(node, parents) + findings.append({ + "line": getattr(node, "lineno", 0), + "name": value, + "context": context, + "value": value, + "kind": "ast_string_literal", + }) + + return findings + + +# ============================================================================= +# REGEX SCANNER +# ============================================================================= + +def _scan_file_regex( + file_path: Path, + standard_names: list[str], +) -> list[dict[str, str | int]]: + """Regex scan for standard names used as hardcoded identifiers. + + Patterns detected: + - check_( -- direct checker function call + - _violations -- hardcoded violation key construction + - == '' -- hardcoded branching + """ + source = file_path.read_text(encoding="utf-8") + lines = source.splitlines() + + findings: list[dict[str, str | int]] = [] + in_docstring = False + docstring_delim: str | None = None + + for lineno, line in enumerate(lines, start=1): + stripped = line.strip() + + # Track triple-quoted docstrings (simple heuristic) + for delim in ('"""', "'''"): + count = stripped.count(delim) + if in_docstring: + if delim == docstring_delim and count >= 1: + in_docstring = False + docstring_delim = None + continue + if count == 1: + in_docstring = True + docstring_delim = delim + break + + if in_docstring: + continue + + # Skip pure comment lines + if stripped.startswith("#"): + continue + + # Extract code portion (before inline comment) + code_part = line.split("#")[0] if "#" in line else line + + for sn in standard_names: + # Pattern 1: check_( -- direct checker call + fn_pat = rf"\bcheck_{re.escape(sn)}\s*\(" + if re.search(fn_pat, code_part): + findings.append({ + "line": lineno, + "name": sn, + "context": stripped[:80], + "value": stripped[:80], + "kind": "hardcoded_function_call", + }) + + # Pattern 2: _violations -- hardcoded violation key + viol_pat = rf"\b{re.escape(sn)}_violations\b" + if re.search(viol_pat, code_part): + findings.append({ + "line": lineno, + "name": sn, + "context": stripped[:80], + "value": stripped[:80], + "kind": "hardcoded_violation_key", + }) + + # Pattern 3: == '' -- hardcoded branching + branch_pat = rf"""==\s*['"]{re.escape(sn)}['"]""" + if re.search(branch_pat, code_part): + findings.append({ + "line": lineno, + "name": sn, + "context": stripped[:80], + "value": stripped[:80], + "kind": "hardcoded_branch", + }) + + return findings + + +# ============================================================================= +# PUBLIC SCAN INTERFACE +# ============================================================================= + +def scan(pack_dir: Path) -> dict: + """Run the full plugin integrity scan. + + Args: + pack_dir: Path to the standards pack directory + (e.g. handlers/aipass_standards/). + + Returns: + Dict with keys: + passed: True if flagged_count == 0 (cosmetic modules don't fail) + standard_names: list of discovered standard names + modules: list of per-module result dicts + clean_count: number of clean modules + flagged_count: modules with unexpected hardcoded references + cosmetic_count: modules with only cosmetic references + missing_count: modules whose file was not found + issues: list of human-readable issue strings + summary: one-line summary string + """ + logger.info("plugin_integrity: scanning pack_dir=%s", pack_dir) + + standard_names = _discover_standard_names(pack_dir) + target_modules = _resolve_target_modules(pack_dir) + + module_results: list[dict] = [] + issues: list[str] = [] + + for module_info in target_modules: + file_path = Path(str(module_info["path"])) + label = str(module_info["label"]) + is_cosmetic = bool(module_info.get("cosmetic", False)) or label in COSMETIC_MODULES + + result: dict = { + "label": label, + "path": str(file_path), + "exists": file_path.is_file(), + "cosmetic_module": is_cosmetic, + "findings": [], + "status": "clean", + } + + if not file_path.is_file(): + result["status"] = "missing" + issues.append(f"{label}: file not found at {file_path}") + module_results.append(result) + continue + + # Run both AST and regex scans + ast_findings = _scan_file_ast(file_path, standard_names) + regex_findings = _scan_file_regex(file_path, standard_names) + + # Deduplicate by (line, name, kind) -- AST findings take priority + seen: set[tuple[int, str, str]] = set() + combined: list[dict] = [] + for finding in ast_findings: + key = (int(finding["line"]), str(finding["name"]), str(finding["kind"])) + if key not in seen: + seen.add(key) + combined.append(finding) + for finding in regex_findings: + key = (int(finding["line"]), str(finding["name"]), str(finding["kind"])) + if key not in seen: + seen.add(key) + combined.append(finding) + + # Sort by line number + combined.sort(key=lambda x: int(x["line"])) + + result["findings"] = combined + + if combined: + if is_cosmetic: + result["status"] = "cosmetic" + else: + result["status"] = "flagged" + # Build issue strings for flagged (non-cosmetic) modules + for finding in combined: + kind_label = { + "ast_string_literal": "string literal", + "hardcoded_function_call": "function call", + "hardcoded_violation_key": "violation key", + "hardcoded_branch": "branch condition", + }.get(str(finding["kind"]), str(finding["kind"])) + issues.append( + f"{label} L{finding['line']}: hardcoded {kind_label} " + f"referencing standard '{finding['name']}'" + ) + else: + result["status"] = "clean" + + module_results.append(result) + + clean_count = sum(1 for m in module_results if m["status"] == "clean") + flagged_count = sum(1 for m in module_results if m["status"] == "flagged") + cosmetic_count = sum(1 for m in module_results if m["status"] == "cosmetic") + missing_count = sum(1 for m in module_results if m["status"] == "missing") + + passed = flagged_count == 0 + + # Build summary + total = len(module_results) + parts: list[str] = [f"{clean_count}/{total} clean"] + if cosmetic_count: + parts.append(f"{cosmetic_count} cosmetic") + if flagged_count: + parts.append(f"{flagged_count} FLAGGED") + if missing_count: + parts.append(f"{missing_count} missing") + + if passed: + summary = f"Plugin integrity clean: {', '.join(parts)}" + else: + summary = f"Plugin integrity FAILED: {', '.join(parts)}" + + logger.info("plugin_integrity: passed=%s, summary=%s", passed, summary) + + json_handler.log_operation("proof_scan", {"proof": "plugin_integrity", "passed": passed}) + + return { + "passed": passed, + "standard_names": standard_names, + "modules": module_results, + "clean_count": clean_count, + "flagged_count": flagged_count, + "cosmetic_count": cosmetic_count, + "missing_count": missing_count, + "issues": issues, + "summary": summary, + } diff --git a/src/aipass/seedgo/apps/handlers/aipass_proof/plugin_integrity_content.py b/src/aipass/seedgo/apps/handlers/aipass_proof/plugin_integrity_content.py new file mode 100644 index 00000000..ef80fbf6 --- /dev/null +++ b/src/aipass/seedgo/apps/handlers/aipass_proof/plugin_integrity_content.py @@ -0,0 +1,81 @@ +# =================== AIPass ==================== +# Name: plugin_integrity_content.py +# Description: Queryable content for the plugin integrity proof +# Version: 1.0.0 +# Created: 2026-03-22 +# Modified: 2026-03-22 +# ============================================= + +""" +Plugin Integrity Proof Content Handler + +Provides formatted plugin integrity proof content for the query system. +Module orchestrates, handler implements. +""" + +from aipass.seedgo.apps.handlers.json import json_handler + + +def get_plugin_integrity_proof() -> str: + """Return plugin integrity proof content for query system. + + Returns: + str: Formatted proof text with Rich styling + """ + sep = "\u2500" * 70 + lines = [ + f"[dim]{sep}[/dim]", + "[bold red]PLUGIN INTEGRITY PROOF \u2014 Dynamic Discovery Contract[/bold red]", + "[dim]Core audit modules must not hardcode standard names.[/dim]", + f"[dim]{sep}[/dim]", + "", + "[bold cyan]WHAT THIS CHECKS:[/bold cyan]", + " Core audit modules must use dynamic discovery (glob + importlib),", + " not hardcoded standard names. The plugin architecture depends on it.", + "", + "[bold cyan]MODULES SCANNED:[/bold cyan]", + " [yellow]\u2022[/yellow] [dim]standards_audit.py[/dim] \u2014 Runs checker discovery and execution", + " [yellow]\u2022[/yellow] [dim]branch_audit.py[/dim] \u2014 Orchestrates branch-level audits", + " [yellow]\u2022[/yellow] [dim]audit_display.py[/dim] \u2014 Formats and displays audit results", + " [yellow]\u2022[/yellow] [dim]seedgo.py[/dim] \u2014 Main entry point for seedgo", + "", + "[bold cyan]WHY IT MATTERS:[/bold cyan]", + " The plugin architecture depends on dynamic discovery. When a new standard", + " is added (drop a [dim]*_check.py[/dim] file in the pack), it should be automatically", + " picked up by glob + importlib. Hardcoded names break this contract:", + " [red]\u2717[/red] New standard added but not in the hardcoded list \u2192 silently skipped", + " [red]\u2717[/red] Standard renamed but hardcoded ref not updated \u2192 crash or stale results", + "", + f"[dim]{sep}[/dim]", + "[bold cyan]WHAT COUNTS AS HARDCODED:[/bold cyan]", + " Any string literal matching a known standard name inside the core modules.", + " The scanner uses an AMBIGUOUS_NAMES filter to exclude false positives", + ' (e.g. "meta", "naming" may appear in non-standard contexts).', + "", + "[bold cyan]COSMETIC vs STRUCTURAL:[/bold cyan]", + " [yellow]Structural:[/yellow] Standard name used in logic (if/elif chains, dict keys for routing)", + " [yellow]Cosmetic:[/yellow] Standard name in display strings, help text, comments", + "", + " [dim]audit_display.py[/dim] cosmetic refs are a known upgrade target ([dim]DPLAN-0047[/dim]).", + " [dim]branch_audit.py[/dim] has 3 legitimate special cases (architecture, meta, testing)", + " that require branch-level handling distinct from file-level standards.", + "", + # TODO: Expand with AMBIGUOUS_NAMES list, detection regex patterns, + # and full accounting of known special cases once scanner is built. + "", + f"[dim]{sep}[/dim]", + "[bold cyan]HOW TO FIX:[/bold cyan]", + " [yellow]1.[/yellow] Replace hardcoded standard names with dynamic iteration over results", + " [yellow]2.[/yellow] Use [dim]glob('*_check.py')[/dim] + [dim]importlib.import_module()[/dim] pattern", + " [yellow]3.[/yellow] For display: iterate result dicts instead of hardcoding section order", + " [yellow]4.[/yellow] For special cases: document why they exist, track in DPLAN-0047", + "", + "[bold cyan]RELATED:[/bold cyan]", + " [dim]DPLAN-0044: Self-audit tooling design[/dim]", + " [dim]DPLAN-0047: audit_display.py cosmetic hardcoding upgrade[/dim]", + " [dim]tools/plugin_integrity_scanner.py: Original prototype[/dim]", + f"[dim]{sep}[/dim]", + ] + + json_handler.log_operation("proof_content_queried", {"proof": "plugin_integrity"}) + return "\n".join(lines) diff --git a/src/aipass/seedgo/apps/handlers/aipass_proof/readme_currency.md b/src/aipass/seedgo/apps/handlers/aipass_proof/readme_currency.md new file mode 100644 index 00000000..23769509 --- /dev/null +++ b/src/aipass/seedgo/apps/handlers/aipass_proof/readme_currency.md @@ -0,0 +1,93 @@ +# README Currency Proof — Documentation Accuracy +**Version:** 1.0.0 +**Created:** 2026-03-22 +**Modified:** 2026-03-22 +**Status:** Placeholder + +--- + +## Purpose + +The README currency proof verifies that a pack's `README.md` accurately reflects the actual state of the pack. README files are the first thing branches and humans read to understand pack coverage. Stale READMEs erode trust and cause confusion. + +--- + +## What Gets Checked + +### 1. Checker Count Accuracy + +The README typically states how many standards the pack contains (e.g., "24 active standards"). This proof compares that stated count against the actual number of `*_check.py` files in the pack. + +``` +README says: "24 active standards" +Actual count: glob("*_check.py") -> 26 files +Result: FAIL — count mismatch (off by 2) +``` + +### 2. Undocumented Standards + +Every `*_check.py` in the pack should have a corresponding mention in the README. A standard that exists in the pack but is never mentioned in the README is "undocumented." + +``` +Pack contains: stderr_routing_check.py +README mentions: (no reference to stderr_routing) +Result: FAIL — undocumented standard +``` + +### 3. Stale References + +The README should not reference standards that no longer exist in the pack. If a standard was archived or removed, its README entry must also be removed. + +``` +README mentions: diagnostics_check.py +Pack contains: (no such file — moved to .archive/) +Result: FAIL — stale reference +``` + +--- + +## How Detection Works + + + +1. **Count extraction**: Parse the README for numeric patterns near keywords like "standards", "checkers", "active" +2. **Standard enumeration**: Glob `*_check.py` in the pack directory, extract standard names +3. **Cross-reference**: For each standard name, search the README text for a mention +4. **Stale detection**: For each standard name found in the README, verify the corresponding `*_check.py` exists + +--- + +## What "Undocumented" Means + +A standard is undocumented if: +- Its `*_check.py` file exists in the pack directory +- The README.md contains no reference to that standard's name +- It is not listed in any standards table or section of the README + +This does not require a full section per standard — a mention in a list or table is sufficient. But total absence means the README is incomplete. + +--- + +## Common Failures + +| Failure | Cause | Fix | +|---------|-------|-----| +| Count mismatch | Standards added/removed without updating README count | Update the count | +| Undocumented standard | New checker added, README not updated | Add entry to README | +| Stale reference | Standard archived/removed, README not updated | Remove the entry | + +--- + +## How to Fix + +1. **Count**: Run `ls *_check.py | wc -l` in the pack directory, update the README count +2. **Undocumented**: Add a line or section for each missing standard +3. **Stale**: Remove or mark as archived any references to non-existent standards + +--- + +## Related + +- **DPLAN-0044**: Self-audit tooling design +- **tools/readme_currency_scanner.py**: Original prototype +- **Checker**: `readme_currency.py` in this directory diff --git a/src/aipass/seedgo/apps/handlers/aipass_proof/readme_currency.py b/src/aipass/seedgo/apps/handlers/aipass_proof/readme_currency.py new file mode 100644 index 00000000..4e799e7b --- /dev/null +++ b/src/aipass/seedgo/apps/handlers/aipass_proof/readme_currency.py @@ -0,0 +1,237 @@ +# =================== AIPass ==================== +# Name: readme_currency.py +# Description: Verify README.md accuracy against actual pack state +# Version: 1.0.0 +# Created: 2026-03-22 +# Modified: 2026-03-22 +# ============================================= + +"""README Currency Proof -- Compare README checker counts and standard lists against actual pack. + +Checks that the seedgo branch README.md accurately reflects the current state +of the standards pack: correct checker count, all standards documented, no stale +references to removed standards. + +Interface: + scan(pack_dir: Path) -> dict + Returns: {"passed": bool, "readme_found": bool, "actual_check_count": int, + "readme_counts": list, "undocumented": list, "stale_refs": list, + "issues": list, "summary": str} + +Reference: tools/readme_currency_scanner.py (original prototype) +""" + +from __future__ import annotations + +import re +from pathlib import Path + +from aipass.prax import logger +from aipass.seedgo.apps.handlers.json import json_handler + + +# -- Helpers ------------------------------------------------------------------- + + +def _standard_name_from_file(filename: str) -> str: + """Extract the standard name from a *_check.py filename. + + Example: 'cli_flags_check.py' -> 'cli_flags' + """ + return filename.removesuffix("_check.py") + + +def _normalize_name(name: str) -> str: + """Normalize a standard name for flexible matching. + + Lowercases, replaces spaces/hyphens with underscores, strips + surrounding whitespace, and collapses repeated underscores. + """ + name = name.lower().strip() + name = re.sub(r"[\s\-]+", "_", name) + name = re.sub(r"_+", "_", name) + return name + + +def _extract_readme_count_references( + readme_text: str, +) -> list[dict[str, str | int]]: + """Find lines that reference a number followed by checker/standard/etc. + + Returns a list of dicts with 'line', 'number', and 'context' keys. + """ + results: list[dict[str, str | int]] = [] + pattern = re.compile( + r"\b(\d+)\s+" + r"(checker|checkers|standard|standards|check|checks)", + re.IGNORECASE, + ) + for lineno, line in enumerate(readme_text.splitlines(), start=1): + for match in pattern.finditer(line): + results.append( + { + "line": lineno, + "number": int(match.group(1)), + "context": line.strip(), + } + ) + return results + + +def _extract_readme_standard_names(readme_text: str) -> set[str]: + """Extract standard names mentioned in the README. + + Looks for the 'Checker Packs' section listing. Handles comma-separated + lists like 'architecture, CLI, CLI flags, ... and diagnostics patterns'. + """ + names: set[str] = set() + + # Parse the comma/and-separated list in the Checker Packs section. + checks_pattern = re.compile( + r"pack\s+checks?:\s*(.+?)(?:\.|$)", re.IGNORECASE | re.DOTALL + ) + match = checks_pattern.search(readme_text) + if match: + raw_list = match.group(1) + # Split on commas and ", and " + raw_list = re.sub(r",?\s+and\s+", ",", raw_list) + for item in raw_list.split(","): + item = item.strip().rstrip(".") + if item: + names.add(_normalize_name(item)) + + return names + + +# -- Core scan ----------------------------------------------------------------- + + +def scan(pack_dir: Path) -> dict: + """Compare README.md checker counts and standard lists against actual pack state. + + Args: + pack_dir: Path to the standards pack directory (e.g. handlers/aipass_standards/). + The seedgo README.md is located at pack_dir.parent.parent.parent / "README.md". + + Returns: + Dict with keys: passed, readme_found, actual_check_count, readme_counts, + undocumented, stale_refs, issues, summary. + """ + issues: list[str] = [] + + # -- Gather actual standards from pack_dir -- + if not pack_dir.is_dir(): + msg = f"Pack directory not found: {pack_dir}" + logger.warning(msg) + return { + "passed": False, + "readme_found": False, + "actual_check_count": 0, + "readme_counts": [], + "undocumented": [], + "stale_refs": [], + "issues": [msg], + "summary": msg, + } + + check_files = sorted(pack_dir.glob("*_check.py")) + actual_names: set[str] = {_standard_name_from_file(f.name) for f in check_files} + actual_check_count = len(check_files) + + # -- Locate README -- + # pack_dir is e.g. .../seedgo/apps/handlers/aipass_standards/ + # seedgo root = pack_dir.parent.parent.parent + seedgo_root = pack_dir.parent.parent.parent + readme_path = seedgo_root / "README.md" + + if not readme_path.is_file(): + msg = f"README not found: {readme_path}" + issues.append(msg) + return { + "passed": False, + "readme_found": False, + "actual_check_count": actual_check_count, + "readme_counts": [], + "undocumented": sorted(actual_names), + "stale_refs": [], + "issues": issues, + "summary": msg, + } + + readme_text = readme_path.read_text(encoding="utf-8") + + # -- Count references -- + count_refs = _extract_readme_count_references(readme_text) + count_mismatch = any(ref["number"] != actual_check_count for ref in count_refs) + if count_mismatch: + for ref in count_refs: + if ref["number"] != actual_check_count: + issues.append( + f"Line {ref['line']}: README says {ref['number']} but " + f"actual count is {actual_check_count}" + ) + + # -- Name references -- + readme_names = _extract_readme_standard_names(readme_text) + + # Build lookup tables for normalized matching + actual_lookup: dict[str, str] = {_normalize_name(n): n for n in actual_names} + readme_lookup: dict[str, str] = {_normalize_name(n): n for n in readme_names} + + actual_norm = set(actual_lookup.keys()) + readme_norm = set(readme_lookup.keys()) + + # Flexible matching: a README name matches if it is a substring of or + # equal to any actual normalized name, and vice-versa. + matched_actual: set[str] = set() + matched_readme: set[str] = set() + + for rn in readme_norm: + for an in actual_norm: + if rn == an or rn in an or an in rn: + matched_actual.add(an) + matched_readme.add(rn) + + stale_norm = readme_norm - matched_readme + missing_norm = actual_norm - matched_actual + + stale_refs = sorted(readme_lookup[n] for n in stale_norm) + undocumented = sorted(actual_lookup[n] for n in missing_norm) + + if stale_refs: + issues.append(f"Stale references in README: {', '.join(stale_refs)}") + if undocumented: + issues.append(f"Undocumented standards: {', '.join(undocumented)}") + + passed = not count_mismatch and not stale_refs and not undocumented + + # -- Summary -- + if passed: + summary = ( + f"README is current. {actual_check_count} checkers, " + f"all documented, no stale references." + ) + else: + parts: list[str] = [] + if count_mismatch: + parts.append("count mismatch") + if stale_refs: + parts.append(f"{len(stale_refs)} stale reference(s)") + if undocumented: + parts.append(f"{len(undocumented)} undocumented standard(s)") + summary = f"README is stale: {', '.join(parts)}." + + logger.info(f"readme_currency proof: {summary}") + + json_handler.log_operation("proof_scan", {"proof": "readme_currency", "passed": passed}) + + return { + "passed": passed, + "readme_found": True, + "actual_check_count": actual_check_count, + "readme_counts": count_refs, + "undocumented": undocumented, + "stale_refs": stale_refs, + "issues": issues, + "summary": summary, + } diff --git a/src/aipass/seedgo/apps/handlers/aipass_proof/readme_currency_content.py b/src/aipass/seedgo/apps/handlers/aipass_proof/readme_currency_content.py new file mode 100644 index 00000000..0ee3e4f7 --- /dev/null +++ b/src/aipass/seedgo/apps/handlers/aipass_proof/readme_currency_content.py @@ -0,0 +1,73 @@ +# =================== AIPass ==================== +# Name: readme_currency_content.py +# Description: Queryable content for the README currency proof +# Version: 1.0.0 +# Created: 2026-03-22 +# Modified: 2026-03-22 +# ============================================= + +""" +README Currency Proof Content Handler + +Provides formatted README currency proof content for the query system. +Module orchestrates, handler implements. +""" + +from aipass.seedgo.apps.handlers.json import json_handler + + +def get_readme_currency_proof() -> str: + """Return README currency proof content for query system. + + Returns: + str: Formatted proof text with Rich styling + """ + sep = "\u2500" * 70 + lines = [ + f"[dim]{sep}[/dim]", + "[bold red]README CURRENCY PROOF \u2014 Documentation Accuracy[/bold red]", + "[dim]README.md must reflect actual pack state at all times.[/dim]", + f"[dim]{sep}[/dim]", + "", + "[bold cyan]WHAT THIS CHECKS:[/bold cyan]", + " [yellow]1.[/yellow] Checker count in README matches actual [dim]*_check.py[/dim] file count", + " [yellow]2.[/yellow] All standards in the pack are documented in the README", + " [yellow]3.[/yellow] No stale references to removed or renamed standards", + "", + "[bold cyan]WHY IT MATTERS:[/bold cyan]", + " A stale README misleads everyone about what the pack actually contains.", + " Branches query README to understand pack coverage. Humans read it to", + " orient themselves. Wrong counts and missing standards erode trust.", + "", + f"[dim]{sep}[/dim]", + "[bold cyan]COMMON FAILURES:[/bold cyan]", + "", + "[yellow]Wrong checker count:[/yellow]", + ' [red]\u2717[/red] README says "24 standards" but pack has 26 [dim]*_check.py[/dim] files', + " [green]\u2713[/green] Count matches actual glob of [dim]*_check.py[/dim]", + "", + "[yellow]Undocumented standards:[/yellow]", + " [red]\u2717[/red] [dim]stderr_routing_check.py[/dim] exists but README never mentions it", + " [green]\u2713[/green] Every checker has a corresponding entry in the README", + "", + "[yellow]Stale references:[/yellow]", + " [red]\u2717[/red] README mentions [dim]diagnostics_check.py[/dim] which was archived", + " [green]\u2713[/green] All referenced standards actually exist in the pack", + "", + # TODO: Expand with detection algorithm details, regex patterns + # for count extraction, and stale reference identification logic. + "", + f"[dim]{sep}[/dim]", + "[bold cyan]HOW TO FIX:[/bold cyan]", + " [yellow]1.[/yellow] Update the checker count to match [dim]len(glob('*_check.py'))[/dim]", + " [yellow]2.[/yellow] Add entries for all undocumented standards", + " [yellow]3.[/yellow] Remove references to standards that no longer exist", + "", + "[bold cyan]RELATED:[/bold cyan]", + " [dim]DPLAN-0044: Self-audit tooling design[/dim]", + " [dim]tools/readme_currency_scanner.py: Original prototype[/dim]", + f"[dim]{sep}[/dim]", + ] + + json_handler.log_operation("proof_content_queried", {"proof": "readme_currency"}) + return "\n".join(lines) diff --git a/src/aipass/seedgo/apps/handlers/aipass_proof/triplet.md b/src/aipass/seedgo/apps/handlers/aipass_proof/triplet.md new file mode 100644 index 00000000..bda7c223 --- /dev/null +++ b/src/aipass/seedgo/apps/handlers/aipass_proof/triplet.md @@ -0,0 +1,94 @@ +# Triplet Proof — Standard File Completeness +**Version:** 1.0.0 +**Created:** 2026-03-22 +**Modified:** 2026-03-22 +**Status:** Placeholder + +--- + +## Purpose + +The triplet proof verifies that every standard in a pack exists as a complete set of 3 files. A standard is only fully realized when all three components are present — the runtime checker, the queryable content, and the human-readable documentation. + +--- + +## The 3-File Convention + +Every standard `{name}` in a pack must have: + +| File | Role | Consumer | +|------|------|----------| +| `{name}_check.py` | Runtime checker — validates branches against the standard | Audit engine | +| `{name}_content.py` | Queryable content — returns formatted text for the query system | standards_query module | +| `{name}.md` | Full documentation — complete specification for humans | Developers, branch managers | + +--- + +## What the Scan Checks + +1. **Glob all `*_check.py` files** in the pack directory to establish the standard names +2. **For each standard name**, verify that `{name}_content.py` and `{name}.md` also exist +3. **Detect orphans** — content or md files that exist without a matching checker +4. **Report completeness** — count of complete triplets vs incomplete standards + +--- + +## Examples + +### Complete Triplet (passing) + +``` +architecture_check.py -- checker +architecture_content.py -- queryable content +architecture.md -- documentation +``` + +All three files present. The standard is fully realized. + +### Incomplete Standard (failing) + +``` +naming_check.py -- checker exists + -- naming_content.py MISSING + -- naming.md MISSING +``` + +The checker runs, but branches cannot query the standard and humans have no documentation. + +### Orphaned Content (failing) + +``` + -- bypass_check.py MISSING +bypass_content.py -- content exists with no checker +``` + +Content exists but there is no checker to enforce it. + +--- + +## How to Fix Failures + +1. **Missing content file**: Create `{name}_content.py` with a `get_{name}_standards()` function that returns formatted Rich text +2. **Missing md file**: Create `{name}.md` with full documentation (purpose, rules, examples, how to fix violations) +3. **Orphaned content/md**: Either create the missing checker or remove the orphan if the standard was intentionally dropped + +Use existing complete triplets as templates. The `architecture` standard is a reliable reference. + +--- + +## Scan Interface + +```python +scan(pack_dir: Path) -> dict +# Returns: {"passed": bool, "issues": list, "summary": str, ...} +``` + + + +--- + +## Related + +- **DPLAN-0044**: Self-audit tooling design +- **tools/triplet_scanner.py**: Original prototype that this proof is based on +- **Checker**: `triplet.py` in this directory diff --git a/src/aipass/seedgo/apps/handlers/aipass_proof/triplet.py b/src/aipass/seedgo/apps/handlers/aipass_proof/triplet.py new file mode 100644 index 00000000..6938ce68 --- /dev/null +++ b/src/aipass/seedgo/apps/handlers/aipass_proof/triplet.py @@ -0,0 +1,165 @@ +# =================== AIPass ==================== +# Name: triplet.py +# Description: Verify standard triplet completeness (check + content + md) +# Version: 1.0.0 +# Created: 2026-03-22 +# Modified: 2026-03-22 +# ============================================= + +"""Triplet Proof -- Verify every standard has all 3 files: *_check.py, *_content.py, *.md + +Interface: + scan(pack_dir: Path) -> dict + Returns: {"passed": bool, "total": int, "complete": list, "check_only": list, + "missing_check": list, "other_incomplete": list, "orphaned": list, + "issues": list, "summary": str} + +Reference: tools/triplet_scanner.py (original prototype) +""" + +from __future__ import annotations + +from pathlib import Path + +from aipass.prax import logger +from aipass.seedgo.apps.handlers.json import json_handler + +# Directories to skip entirely +_SKIP_DIRS = {".archive", ".sorting_unprocessed", "__pycache__"} + + +def _top_level_files(pack_dir: Path) -> list[Path]: + """Return all regular files directly in *pack_dir*, skipping hidden/skip dirs.""" + if not pack_dir.is_dir(): + return [] + return [ + p + for p in pack_dir.iterdir() + if p.is_file() + and p.name not in _SKIP_DIRS + and not p.name.startswith("_") + ] + + +def scan(pack_dir: Path) -> dict: + """Run triplet completeness scan on a standards pack directory. + + Every standard should have three files: {name}_check.py, {name}_content.py, {name}.md. + Files that don't fit any triplet pattern are flagged as orphaned. + + Args: + pack_dir: Path to the standards pack directory (e.g. handlers/aipass_standards/). + + Returns: + Dict with keys: passed, total, complete, check_only, missing_check, + other_incomplete, orphaned, issues, summary. + """ + logger.info(f"[triplet] Scanning {pack_dir}") + + checks: set[str] = set() + contents: set[str] = set() + docs: set[str] = set() + orphaned_files: list[str] = [] + + for filepath in _top_level_files(pack_dir): + name = filepath.name + + if name.endswith("_check.py"): + checks.add(name.removesuffix("_check.py")) + + elif name.endswith("_content.py"): + contents.add(name.removesuffix("_content.py")) + + elif name.endswith(".md"): + # Only lowercase-starting .md files participate in triplet matching. + # Uppercase .md (e.g. README.md, SOP docs) are ancillary -- not orphaned. + stem = filepath.stem + if stem[0:1].islower(): + docs.add(stem) + + elif name.endswith(".py"): + # .py files that are neither _check nor _content -- orphaned + orphaned_files.append(name) + + elif name.endswith(".json"): + # Config / ancillary JSON files -- not orphaned + pass + + else: + orphaned_files.append(name) + + # Union of all discovered standard names + all_names = sorted(checks | contents | docs) + + complete: list[str] = [] + check_only: list[str] = [] + missing_check: list[str] = [] + other_incomplete: list[str] = [] + issues: list[dict[str, str | bool]] = [] + + for std_name in all_names: + has_check = std_name in checks + has_content = std_name in contents + has_md = std_name in docs + + entry = { + "name": std_name, + "has_check": has_check, + "has_content": has_content, + "has_md": has_md, + } + + if has_check and has_content and has_md: + complete.append(std_name) + elif has_check and not has_content and not has_md: + check_only.append(std_name) + issues.append({**entry, "issue": "check only -- missing content + md"}) + elif not has_check: + missing_check.append(std_name) + missing = [] + if not has_check: + missing.append("check") + if not has_content: + missing.append("content") + if not has_md: + missing.append("md") + issues.append({**entry, "issue": f"missing: {', '.join(missing)}"}) + else: + other_incomplete.append(std_name) + missing = [] + if not has_content: + missing.append("content") + if not has_md: + missing.append("md") + issues.append({**entry, "issue": f"missing: {', '.join(missing)}"}) + + passed = len(issues) == 0 and len(orphaned_files) == 0 + + # Build human-readable summary + parts = [f"{len(complete)} complete"] + if check_only: + parts.append(f"{len(check_only)} check-only") + if missing_check: + parts.append(f"{len(missing_check)} missing-check") + if other_incomplete: + parts.append(f"{len(other_incomplete)} other-incomplete") + if orphaned_files: + parts.append(f"{len(orphaned_files)} orphaned") + parts.append(f"{len(all_names)} total") + summary = " | ".join(parts) + + logger.info(f"[triplet] Result: {summary}") + + json_handler.log_operation("proof_scan", {"proof": "triplet", "passed": passed}) + + return { + "passed": passed, + "total": len(all_names), + "complete": complete, + "check_only": check_only, + "missing_check": missing_check, + "other_incomplete": other_incomplete, + "orphaned": orphaned_files, + "issues": issues, + "summary": summary, + } diff --git a/src/aipass/seedgo/apps/handlers/aipass_proof/triplet_content.py b/src/aipass/seedgo/apps/handlers/aipass_proof/triplet_content.py new file mode 100644 index 00000000..ea7e4ff0 --- /dev/null +++ b/src/aipass/seedgo/apps/handlers/aipass_proof/triplet_content.py @@ -0,0 +1,72 @@ +# =================== AIPass ==================== +# Name: triplet_content.py +# Description: Queryable content for the triplet proof +# Version: 1.0.0 +# Created: 2026-03-22 +# Modified: 2026-03-22 +# ============================================= + +""" +Triplet Proof Content Handler + +Provides formatted triplet proof content for the query system. +Module orchestrates, handler implements. +""" + +from aipass.seedgo.apps.handlers.json import json_handler + + +def get_triplet_proof() -> str: + """Return triplet proof content for query system. + + Returns: + str: Formatted proof text with Rich styling + """ + sep = "\u2500" * 70 + lines = [ + f"[dim]{sep}[/dim]", + "[bold red]TRIPLET PROOF \u2014 Standard File Completeness[/bold red]", + "[dim]Every standard in a pack must have 3 files.[/dim]", + f"[dim]{sep}[/dim]", + "", + "[bold cyan]WHAT THIS CHECKS:[/bold cyan]", + " Every standard must exist as a complete triplet:", + " [yellow]1.[/yellow] [dim]{name}_check.py[/dim] \u2014 The checker (runtime validation)", + " [yellow]2.[/yellow] [dim]{name}_content.py[/dim] \u2014 Queryable content (for standards_query)", + " [yellow]3.[/yellow] [dim]{name}.md[/dim] \u2014 Full documentation (for humans)", + "", + "[bold cyan]WHY IT MATTERS:[/bold cyan]", + " A checker without content means branches can't query the standard to understand it.", + " A checker without documentation means humans can't read the full spec.", + " Incomplete standards create knowledge gaps in the system.", + "", + f"[dim]{sep}[/dim]", + "[bold cyan]COMMON FAILURES:[/bold cyan]", + "", + "[yellow]Check-only standards:[/yellow]", + " Checker exists but content and md are missing.", + " [red]\u2717[/red] [dim]naming_check.py exists, naming_content.py missing, naming.md missing[/dim]", + "", + "[yellow]Orphaned content:[/yellow]", + " Content file exists with no matching checker.", + " [red]\u2717[/red] [dim]bypass_content.py exists, no bypass_check.py[/dim]", + "", + f"[dim]{sep}[/dim]", + "[bold cyan]HOW TO FIX:[/bold cyan]", + " [yellow]1.[/yellow] Create the missing [dim]*_content.py[/dim] with a [dim]get_{name}_standards()[/dim] function", + " [yellow]2.[/yellow] Create the missing [dim]*.md[/dim] with full documentation", + " [yellow]3.[/yellow] Follow existing triplets as examples:", + " [green]\u2713[/green] [dim]architecture_check.py + architecture_content.py + architecture.md[/dim]", + "", + # TODO: Expand with actual scan logic details, threshold counts, + # and per-pack statistics once the scanner is implemented. + "", + f"[dim]{sep}[/dim]", + "[bold cyan]RELATED:[/bold cyan]", + " [dim]DPLAN-0044: Self-audit tooling design[/dim]", + " [dim]tools/triplet_scanner.py: Original prototype[/dim]", + f"[dim]{sep}[/dim]", + ] + + json_handler.log_operation("proof_content_queried", {"proof": "triplet"}) + return "\n".join(lines) diff --git a/src/aipass/seedgo/apps/modules/checklist.py b/src/aipass/seedgo/apps/modules/checklist.py index 6643923c..264b92c4 100644 --- a/src/aipass/seedgo/apps/modules/checklist.py +++ b/src/aipass/seedgo/apps/modules/checklist.py @@ -269,7 +269,21 @@ def handle_command(command: str, args: List[str]) -> bool: resolved = Path.cwd() / resolved resolved = resolved.resolve() - # Run checklist + # Directory mode — run checklist on all .py files in directory + if resolved.is_dir(): + py_files = sorted(resolved.glob("*.py")) + py_files = [f for f in py_files if not f.name.startswith("_")] + if not py_files: + error("No .py files found in directory", suggestion=f"Directory: {resolved}") + return True + console.print(f"\n[bold cyan]Checklist — {resolved.name}/[/bold cyan] [dim]({len(py_files)} files)[/dim]\n") + for f in py_files: + results = run_checklist(str(f), pack_name=pack_name) + _print_results(results, str(f)) + console.print() + return True + + # Single file mode results = run_checklist(str(resolved), pack_name=pack_name) # Print results @@ -333,7 +347,8 @@ def print_help() -> None: console.print() console.print("[yellow]USAGE:[/yellow]") - console.print(" [green]drone @seedgo checklist [/green] [dim]# Check file (aipass pack)[/dim]") + console.print(" [green]drone @seedgo checklist [/green] [dim]# Check single file[/dim]") + console.print(" [green]drone @seedgo checklist [/green] [dim]# Check all .py files in directory[/dim]") console.print(" [green]drone @seedgo checklist --pack [/green] [dim]# Check with specific pack[/dim]") console.print(" [green]drone @seedgo checklist --help[/green] [dim]# This help message[/dim]") console.print() diff --git a/src/aipass/seedgo/apps/modules/proof_query.py b/src/aipass/seedgo/apps/modules/proof_query.py new file mode 100644 index 00000000..67347e81 --- /dev/null +++ b/src/aipass/seedgo/apps/modules/proof_query.py @@ -0,0 +1,322 @@ +# =================== AIPass ==================== +# Name: proof_query.py +# Description: Proof Query Module +# Version: 1.0.0 +# Created: 2026-03-22 +# Modified: 2026-03-22 +# ============================================= + +""" +Proof Query Module + +Auto-discovering content query module for proof packs. +Mirrors standards_query.py pattern but for proof content. + +Run: seedgo proof_query +""" + +import importlib.util +from pathlib import Path +from typing import List + +# ============================================================================= +# INFRASTRUCTURE SETUP +# ============================================================================= + +# IMPORTS +# ============================================================================= + +# Prax logger (system-wide, always first) +from aipass.prax import logger + +# CLI services (display/output formatting) +from aipass.cli import console, header +from aipass.cli.apps.modules import error, warning + +# JSON handler for tracking +from aipass.seedgo.apps.handlers.json import json_handler + + +# ============================================================================= +# PACK DISCOVERY +# ============================================================================= + +def _discover_proof_packs() -> dict: + """Discover available proof packs from handlers/ directory. + + Convention: directories named *_proof/ containing *_content.py files. + Pack identifier is the full directory name (e.g., "aipass_proof"). + + Returns: + Dict mapping pack name to Path, e.g. {"aipass_proof": Path("handlers/aipass_proof")} + """ + handlers_dir = Path(__file__).parent.parent / "handlers" + packs = {} + if not handlers_dir.exists(): + return packs + for d in sorted(handlers_dir.iterdir()): + if not d.is_dir(): + continue + if not d.name.endswith("_proof"): + continue + # Must contain at least one *_content.py file + content_files = list(d.glob("*_content.py")) + if content_files: + packs[d.name] = d + return packs + + +# ============================================================================= +# PROOF CONTENT DISCOVERY +# ============================================================================= + +def _discover_proof_content(pack_dir: Path) -> dict: + """Discover available proof content files within a pack. + + Globs *_content.py from the pack directory. + + Args: + pack_dir: Path to the *_proof/ directory + + Returns: + Dict mapping proof name to Path, + e.g. {"triplet": Path("triplet_content.py"), ...} + """ + proofs = {} + for f in sorted(pack_dir.glob("*_content.py")): + # Strip _content.py suffix to get proof name + proof_name = f.stem.removesuffix("_content") + proofs[proof_name] = f + return proofs + + +# ============================================================================= +# CONTENT LOADING +# ============================================================================= + +def _load_proof_content(content_file: Path, proof_name: str) -> str | None: + """Load and return formatted proof content from a content handler. + + Imports the content file and calls get_{proof_name}_proof(). + + Args: + content_file: Path to the *_content.py file + proof_name: Name of the proof (e.g., "triplet") + + Returns: + Rich-formatted string, or None on failure + """ + try: + spec = importlib.util.spec_from_file_location(content_file.stem, content_file) + if spec is None or spec.loader is None: + logger.error(f"[proof_query] Failed to create spec for {content_file}") + return None + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + + fn_name = f"get_{proof_name}_proof" + fn = getattr(mod, fn_name, None) + if fn is None: + logger.error(f"[proof_query] No {fn_name}() in {content_file.name}") + console.print(f"[red]Content handler missing:[/red] {fn_name}() not found in {content_file.name}") + return None + + return fn() + except Exception as e: + logger.error(f"[proof_query] Failed to load {content_file.name}: {e}") + console.print(f"[red]Failed to load content:[/red] {e}") + return None + + +# ============================================================================= +# DISPLAY HELPERS +# ============================================================================= + +def _show_query_introspection() -> None: + """No-args display: list available proof packs with content counts.""" + console.print() + console.print("[bold cyan]proof_query Module[/bold cyan]") + console.print("Pack-aware content query — browse proof content by pack and name") + console.print() + + # Show discovered packs + packs = _discover_proof_packs() + console.print("[yellow]Discovered Proof Packs:[/yellow]") + for name, pack_path in packs.items(): + proofs = _discover_proof_content(pack_path) + console.print(f" [cyan]{name}[/cyan] ({len(proofs)} proof{'s' if len(proofs) != 1 else ''})") + if not packs: + console.print(" [dim]No proof packs found[/dim]") + console.print() + + console.print("[yellow]Connected Handlers:[/yellow]") + for name, pack_path in packs.items(): + console.print(f" [cyan]handlers/{name}/[/cyan]") + proofs = _discover_proof_content(pack_path) + for proof_name, proof_path in proofs.items(): + console.print(f" [dim]- {proof_path.name} (get_{proof_name}_proof)[/dim]") + console.print() + + # Navigation hints with drone commands + if packs: + console.print("[yellow]Next:[/yellow] Pick a pack to see its proofs") + for name in packs: + console.print(f" [green]drone @seedgo proof_query {name}[/green]") + console.print() + + +def _list_pack_proofs(pack_name: str, pack_dir: Path) -> None: + """List all proofs available in a pack. + + Args: + pack_name: Full pack directory name (e.g., "aipass_proof") + pack_dir: Path to the pack directory + """ + proofs = _discover_proof_content(pack_dir) + + console.print() + header(f"PROOFS IN {pack_name.upper()}") + console.print() + + if not proofs: + warning("No content handlers found.") + console.print(f"[dim]Add *_content.py files to handlers/{pack_name}/[/dim]") + console.print() + return + + console.print(f"[yellow]Available Proofs:[/yellow] ({len(proofs)})") + console.print() + for name in proofs: + console.print(f" [cyan]{name}[/cyan]") + console.print() + + console.print("[yellow]Next:[/yellow] Pick a proof to see its content") + first_proof = next(iter(proofs)) + console.print(f" [green]drone @seedgo proof_query {pack_name} {first_proof}[/green]") + console.print() + + +def _show_proof_content(pack_name: str, pack_dir: Path, proof_name: str) -> None: + """Display specific proof content. + + Args: + pack_name: Full pack directory name (e.g., "aipass_proof") + pack_dir: Path to the pack directory + proof_name: Name of the proof (e.g., "triplet") + """ + proofs = _discover_proof_content(pack_dir) + if proof_name not in proofs: + console.print(f"[red]Unknown proof:[/red] '{proof_name}'") + console.print() + warning(f"Available proofs in {pack_name}:") + for name in proofs: + console.print(f" [cyan]{name}[/cyan]") + console.print() + return + + content = _load_proof_content(proofs[proof_name], proof_name) + json_handler.log_operation("proof_queried", {"pack": pack_name, "proof": proof_name}) + if content: + console.print() + # Handle both str and List[str] return types from content handlers + if isinstance(content, list): + for line in content: + console.print(line) + else: + console.print(content) + console.print() + + +# ============================================================================= +# COMMAND HANDLER +# ============================================================================= + +def handle_command(command: str, args: List[str]) -> bool: + """ + Handle 'proof_query' command with pack-aware drill-down. + + Args: + command: Command name + args: Additional arguments + [] → show introspection (available packs) + ["aipass_proof"] → list proofs in pack + ["aipass_proof", "triplet"] → show proof content + ["--help"] → help + + Returns: + True if handled, False if not this module's command + """ + if command != "proof_query": + return False + + if not args: + print_introspection() + return True + + if args[0] in ["--help", "-h", "help"]: + print_help() + return True + + # First arg = pack name + packs = _discover_proof_packs() + pack_name = args[0] + if pack_name not in packs: + console.print(f"[red]Unknown pack:[/red] '{pack_name}'") + console.print() + console.print("[yellow]Available packs:[/yellow]") + for name in packs: + console.print(f" [cyan]{name}[/cyan]") + console.print() + return True + + # No second arg = list proofs in pack + if len(args) < 2: + _list_pack_proofs(pack_name, packs[pack_name]) + return True + + # Second arg = proof name + _show_proof_content(pack_name, packs[pack_name], args[1]) + return True + + +# ============================================================================= +# INTROSPECTION & HELP +# ============================================================================= + +def print_introspection() -> None: + """Display module info for seedgo introspection system.""" + _show_query_introspection() + + +def print_help() -> None: + """Print help information.""" + console.print() + console.print("[bold cyan]Proof Query Module[/bold cyan]") + console.print("Pack-aware content query — browse and display proof content") + console.print() + + console.print("[yellow]COMMANDS:[/yellow]") + console.print(" [green]drone @seedgo proof_query[/green] [dim]List available proof packs[/dim]") + console.print(" [green]drone @seedgo proof_query [/green] [dim]List proofs in pack[/dim]") + console.print(" [green]drone @seedgo proof_query [/green] [dim]Show proof content[/dim]") + console.print(" [green]drone @seedgo proof_query --help[/green] [dim]This help message[/dim]") + console.print() + + console.print("[yellow]EXAMPLES:[/yellow]") + console.print(" [dim]# List all proof packs[/dim]") + console.print(" [green]drone @seedgo proof_query[/green]") + console.print() + console.print(" [dim]# List proofs in aipass proof pack[/dim]") + console.print(" [green]drone @seedgo proof_query aipass_proof[/green]") + console.print() + console.print(" [dim]# Show triplet proof content[/dim]") + console.print(" [green]drone @seedgo proof_query aipass_proof triplet[/green]") + console.print() + + console.print("[yellow]REFERENCE:[/yellow]") + console.print(" Auto-discovers *_content.py handlers from proof pack directories.") + console.print(" Each content handler provides get__proof() returning Rich-formatted text.") + console.print() + + console.print("[dim]Commands: proof_query, --help[/dim]") + console.print() diff --git a/src/aipass/seedgo/apps/modules/seedgo_proof.py b/src/aipass/seedgo/apps/modules/seedgo_proof.py new file mode 100644 index 00000000..d97e17ee --- /dev/null +++ b/src/aipass/seedgo/apps/modules/seedgo_proof.py @@ -0,0 +1,497 @@ +# =================== AIPass ==================== +# Name: seedgo_proof.py +# Description: Self-proof orchestrator — discovers and runs proof packs +# Version: 1.0.0 +# Created: 2026-03-22 +# Modified: 2026-03-22 +# ============================================= + +"""Seedgo Proof — Self-check orchestrator for standards pack integrity. + +Discovers *_proof/ directories in handlers/, runs all proof handler .py files +inside, aggregates results, and reports CERTIFIED or NOT CERTIFIED. + +Commands: + seedgo proof — List available proof packs + seedgo proof aipass — Run all proofs for aipass pack + seedgo proof aipass --json — Machine-readable JSON output + +Discovery pattern mirrors standards_audit.py: + handlers/*_proof/ dirs → handler .py files → scan(pack_dir) interface + +Interface per proof handler: + scan(pack_dir: Path) -> dict with keys: passed, issues, summary + +Related: + DPLAN-0048 (seedgo self-check system) + tools/ (original prototypes — standalone, not connected) +""" + +import json +import importlib.util +from pathlib import Path +from typing import List + +from rich.table import Table + +# ============================================================================= +# INFRASTRUCTURE SETUP +# ============================================================================= + +# IMPORTS +# ============================================================================= + +# Prax logger (system-wide, always first) +from aipass.prax import logger + +# CLI services (display/output formatting) +from aipass.cli import console, header +from aipass.cli.apps.modules import error, warning + +# JSON handler for tracking +from aipass.seedgo.apps.handlers.json import json_handler + + +# ============================================================================= +# PACK DISCOVERY +# ============================================================================= + +def _discover_proof_packs() -> dict: + """Discover available proof packs from handlers/ directory. + + Convention: directories named *_proof/ containing at least one .py handler file. + Pack display name strips the _proof suffix. + + Excludes from handler counting: + - __init__.py + - *_content.py + - files starting with _ + - non-.py files + + Returns: + Dict mapping pack name to Path, e.g. {"aipass": Path("handlers/aipass_proof")} + """ + handlers_dir = Path(__file__).parent.parent / "handlers" + packs = {} + if not handlers_dir.exists(): + return packs + for d in sorted(handlers_dir.iterdir()): + if not d.is_dir(): + continue + if not d.name.endswith("_proof"): + continue + # Must contain at least one valid handler .py file + handler_files = _discover_proof_handlers(d) + if handler_files: + pack_name = d.name.removesuffix("_proof") + packs[pack_name] = d + return packs + + +def _discover_proof_handlers(pack_dir: Path) -> List[Path]: + """Find all handler .py files in a proof pack directory. + + Skips: + - __init__.py + - *_content.py + - *.md files + - files starting with _ + + Args: + pack_dir: Path to the *_proof/ directory + + Returns: + Sorted list of handler Paths + """ + handlers = [] + if not pack_dir.exists(): + return handlers + for f in sorted(pack_dir.iterdir()): + if not f.is_file(): + continue + if f.suffix != ".py": + continue + if f.name.startswith("_"): + continue + if f.name.endswith("_content.py"): + continue + handlers.append(f) + return handlers + + +def _resolve_target_pack(proof_pack_name: str) -> Path | None: + """Resolve the standards pack directory that a proof pack targets. + + Mapping: strip '_proof' suffix from proof dir name, add '_standards'. + E.g., handlers/aipass_proof/ -> handlers/aipass_standards/ + + Args: + proof_pack_name: The display name of the proof pack (e.g. "aipass") + + Returns: + Path to the target standards pack, or None if it does not exist + """ + handlers_dir = Path(__file__).parent.parent / "handlers" + target_dir = handlers_dir / f"{proof_pack_name}_standards" + if target_dir.is_dir(): + return target_dir + return None + + +# ============================================================================= +# PROOF EXECUTION +# ============================================================================= + +def _load_and_run_proof(handler_path: Path, pack_dir: Path) -> dict: + """Import a proof handler module dynamically and call its scan() function. + + Args: + handler_path: Path to the handler .py file + pack_dir: Path to the target standards pack directory (passed to scan()) + + Returns: + Result dict from scan(), or error dict on failure + """ + handler_name = handler_path.stem + try: + spec = importlib.util.spec_from_file_location(handler_name, handler_path) + if spec is None or spec.loader is None: + logger.error(f"[seedgo_proof] Failed to create spec for {handler_path}") + return { + "passed": False, + "issues": [f"Failed to create import spec for {handler_path.name}"], + "summary": "Import error", + "error": True, + } + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + + scan_fn = getattr(mod, "scan", None) + if scan_fn is None: + logger.warning(f"[seedgo_proof] No scan() in {handler_path.name} — skipping") + return { + "passed": False, + "issues": [f"No scan() function found in {handler_path.name}"], + "summary": "Missing scan() interface", + "error": True, + "not_implemented": True, + } + + result = scan_fn(pack_dir) + if not isinstance(result, dict): + return { + "passed": False, + "issues": [f"scan() returned {type(result).__name__}, expected dict"], + "summary": "Invalid return type", + "error": True, + } + return result + + except Exception as e: + logger.error(f"[seedgo_proof] Error running {handler_name}: {e}") + return { + "passed": False, + "issues": [f"Exception: {e}"], + "summary": f"Error: {e}", + "error": True, + } + + +def _run_proof_pack(pack_name: str, pack_dir: Path) -> dict: + """Run all proof handlers in a pack against the target standards pack. + + For each handler: load, run scan(), collect results. + Aggregate: count passed/failed/errors. + + Args: + pack_name: Display name of the proof pack (e.g. "aipass") + pack_dir: Path to the proof pack directory (e.g. handlers/aipass_proof/) + + Returns: + Aggregated dict with keys: + pack_name, target_dir, results (per-handler), passed, failed, + errors, total, certified + """ + handlers = _discover_proof_handlers(pack_dir) + target_dir = _resolve_target_pack(pack_name) + + if target_dir is None: + logger.error(f"[seedgo_proof] No target standards pack found for '{pack_name}'") + return { + "pack_name": pack_name, + "target_dir": None, + "results": {}, + "passed": 0, + "failed": 0, + "errors": 1, + "total": 0, + "certified": False, + "error": f"No matching standards pack: {pack_name}_standards", + } + + results = {} + passed_count = 0 + failed_count = 0 + error_count = 0 + + for handler_path in handlers: + handler_name = handler_path.stem + result = _load_and_run_proof(handler_path, target_dir) + results[handler_name] = result + + if result.get("error"): + error_count += 1 + elif result.get("passed"): + passed_count += 1 + else: + failed_count += 1 + + total = len(handlers) + certified = (failed_count == 0 and error_count == 0 and total > 0) + + return { + "pack_name": pack_name, + "target_dir": str(target_dir), + "results": results, + "passed": passed_count, + "failed": failed_count, + "errors": error_count, + "total": total, + "certified": certified, + } + + +# ============================================================================= +# DISPLAY +# ============================================================================= + +def _display_proof_results(pack_name: str, results: dict) -> None: + """Rich console output for proof pack results. + + Shows header, per-proof results table, and final verdict. + + Args: + pack_name: Display name of the proof pack + results: Aggregated results dict from _run_proof_pack() + """ + console.print() + header(f"SEEDGO PROOF — {pack_name.upper()}") + console.print() + + if results.get("error") and not results.get("results"): + error(results["error"]) + console.print() + return + + target_dir = results.get("target_dir", "unknown") + console.print(f"[dim]Target pack: {target_dir}[/dim]") + console.print(f"[dim]Proof handlers: {results['total']}[/dim]") + console.print() + + # Per-proof results table + table = Table( + title="Proof Results", + show_header=True, + header_style="bold cyan", + show_lines=False, + pad_edge=True, + ) + table.add_column("Proof", style="cyan", min_width=20) + table.add_column("Status", justify="center", min_width=12) + table.add_column("Summary", min_width=30) + + for proof_name, result in results.get("results", {}).items(): + if result.get("not_implemented"): + status = "[yellow]PENDING[/yellow]" + elif result.get("error"): + status = "[red]ERROR[/red]" + elif result.get("passed"): + status = "[green]PASSED[/green]" + else: + status = "[red]FAILED[/red]" + + summary = result.get("summary", "No summary") + issue_count = len(result.get("issues", [])) + if issue_count > 0 and not result.get("not_implemented"): + summary += f" ({issue_count} issue{'s' if issue_count != 1 else ''})" + + table.add_row(proof_name, status, summary) + + console.print(table) + console.print() + + # Aggregate summary + console.print( + f" [green]Passed:[/green] {results['passed']} " + f"[red]Failed:[/red] {results['failed']} " + f"[yellow]Errors:[/yellow] {results['errors']} " + f"[dim]Total:[/dim] {results['total']}" + ) + console.print() + + # Final verdict + if results["certified"]: + console.print("[bold green] CERTIFIED[/bold green]") + else: + console.print("[bold red] NOT CERTIFIED[/bold red]") + console.print() + + +# ============================================================================= +# INTROSPECTION +# ============================================================================= + +def _show_proof_introspection() -> None: + """Show available proof packs and example commands when proof is run with no args.""" + packs = _discover_proof_packs() + console.print() + header("SEEDGO PROOF") + console.print() + + if not packs: + warning("No proof packs found.") + console.print("[dim]Add handler .py files to handlers/*_proof/ directories.[/dim]") + console.print() + return + + console.print("[yellow]Available Proof Packs:[/yellow]") + console.print() + for name, pack_path in packs.items(): + handler_files = _discover_proof_handlers(pack_path) + target = _resolve_target_pack(name) + target_status = "[green]target found[/green]" if target else "[red]no target[/red]" + console.print( + f" [cyan]{name}[/cyan] " + f"({len(handler_files)} proof{'s' if len(handler_files) != 1 else ''}, " + f"{target_status})" + ) + console.print() + + console.print("[yellow]Next:[/yellow] Pick a pack to run proofs") + first_pack = next(iter(packs)) + console.print(f" [green]drone @seedgo proof {first_pack}[/green] [dim]# Run all proofs[/dim]") + console.print(f" [green]drone @seedgo proof {first_pack} --json[/green] [dim]# JSON output[/dim]") + console.print() + + +def print_introspection() -> None: + """Display module info and connected handlers.""" + _show_proof_introspection() + + +# ============================================================================= +# HELP +# ============================================================================= + +def print_help() -> None: + """Print help information.""" + console.print() + console.print("[bold cyan]Seedgo Proof Module[/bold cyan]") + console.print("Self-check orchestrator — verifies standards pack integrity") + console.print() + + console.print("[yellow]COMMANDS:[/yellow]") + console.print(" [green]drone @seedgo proof[/green] [dim]Show available proof packs[/dim]") + console.print(" [green]drone @seedgo proof aipass[/green] [dim]Run all proofs for aipass pack[/dim]") + console.print(" [green]drone @seedgo proof aipass --json[/green] [dim]JSON output[/dim]") + console.print(" [green]drone @seedgo proof --help[/green] [dim]This help message[/dim]") + console.print() + + console.print("[yellow]PROOF HANDLER INTERFACE:[/yellow]") + console.print(" Each handler .py file must define:") + console.print(" [cyan]scan(pack_dir: Path) -> dict[/cyan]") + console.print(" Return keys: passed (bool), issues (list), summary (str)") + console.print() + + console.print("[yellow]REFERENCE:[/yellow]") + console.print(" Proof pack = handlers/*_proof/ directory") + console.print(" Target = handlers/*_standards/ directory (auto-resolved)") + console.print(" Verdict: CERTIFIED (all pass) or NOT CERTIFIED (any fail/error)") + console.print() + + console.print("[dim]Commands: proof, seedgo_proof, --help[/dim]") + console.print() + + +# ============================================================================= +# COMMAND HANDLER +# ============================================================================= + +def handle_command(command: str, args: List[str]) -> bool: + """Handle 'proof' command with pack-aware routing. + + Args: + command: Command name + args: Additional arguments + [] -> show proof introspection (available packs) + ["aipass"] -> run aipass proof pack + ["aipass", "--json"] -> JSON output + ["--help"] -> help + + Returns: + True if handled, False if not this module's command + """ + if command not in ("proof", "seedgo_proof"): + return False + + # No args -> show proof introspection (available packs) + if not args: + print_introspection() + return True + + # --help -> help + if args[0] in ["--help", "-h", "help"]: + print_help() + return True + + # Parse pack name and flags + pack_name: str | None = None + json_output = False + + for arg in args: + if arg == "--json": + json_output = True + elif not arg.startswith("-"): + if pack_name is None: + pack_name = arg + + # Validate pack name + packs = _discover_proof_packs() + if pack_name is None or pack_name not in packs: + available = ", ".join(packs.keys()) if packs else "(none)" + error( + f"Unknown proof pack: '{pack_name}'", + suggestion=f"Available packs: {available}. Usage: drone @seedgo proof {next(iter(packs), '')}" + ) + return True + + pack_dir = packs[pack_name] + + # Log proof start + json_handler.log_operation( + "proof_started", + {"pack": pack_name} + ) + + # Run the proof pack + results = _run_proof_pack(pack_name, pack_dir) + + # Output + if json_output: + console.print_json(json.dumps(results, indent=2, default=str)) + else: + _display_proof_results(pack_name, results) + + # Log completion + json_handler.log_operation( + "proof_completed", + { + "pack": pack_name, + "passed": results["passed"], + "failed": results["failed"], + "errors": results["errors"], + "certified": results["certified"], + } + ) + + return True