feat(seedgo): feat: seedgo proof system + checklist directory mode
Co-Authored-By: @seedgo <seedgo@aipass>
This commit is contained in:
@@ -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.
|
||||
@@ -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
|
||||
|
||||
<!-- TODO: Document the exact standards_query resolution code path
|
||||
and error handling behavior -->
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
<!-- TODO: Detail the AST inspection approach vs import-based validation
|
||||
once the scanner implementation is finalized -->
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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:
|
||||
|
||||
<!-- TODO: Document the full AMBIGUOUS_NAMES list once scanner is finalized -->
|
||||
|
||||
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
|
||||
|
||||
<!-- TODO: Document the exact regex/AST approach and threshold for
|
||||
pass/fail once scanner is implemented -->
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
@@ -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 "<module level>"
|
||||
|
||||
|
||||
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_<standard>( -- direct checker function call
|
||||
- <standard>_violations -- hardcoded violation key construction
|
||||
- == '<standard>' -- 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_<standard>( -- 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: <standard>_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: == '<standard>' -- 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,
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
<!-- TODO: Document the exact detection algorithm once scanner is built -->
|
||||
|
||||
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
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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, ...}
|
||||
```
|
||||
|
||||
<!-- TODO: Document return dict shape in detail once scanner is implemented -->
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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 <file>[/green] [dim]# Check file (aipass pack)[/dim]")
|
||||
console.print(" [green]drone @seedgo checklist <file>[/green] [dim]# Check single file[/dim]")
|
||||
console.print(" [green]drone @seedgo checklist <directory>[/green] [dim]# Check all .py files in directory[/dim]")
|
||||
console.print(" [green]drone @seedgo checklist --pack <pack> <file>[/green] [dim]# Check with specific pack[/dim]")
|
||||
console.print(" [green]drone @seedgo checklist --help[/green] [dim]# This help message[/dim]")
|
||||
console.print()
|
||||
|
||||
@@ -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 <pack>[/green] [dim]List proofs in pack[/dim]")
|
||||
console.print(" [green]drone @seedgo proof_query <pack> <proof>[/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_<name>_proof() returning Rich-formatted text.")
|
||||
console.print()
|
||||
|
||||
console.print("[dim]Commands: proof_query, --help[/dim]")
|
||||
console.print()
|
||||
@@ -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), '<pack>')}"
|
||||
)
|
||||
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
|
||||
Reference in New Issue
Block a user