fix(system): S61-S62 — API cleanup, drone path routing, naming checker, .backup→.recovery rename (#142)
- API: 18-item P0/P1/P2 cleanup (debug prints→logger, help gate fix, URL injection fix, key masking, unused imports) - Drone: passport walk-up replaces hardcoded src/aipass/<branch> pattern, 13 naming bypasses removed - Seedgo: naming checker fixed (__dunder__ skip + column-0 scope detection) — eliminates 71 false positives system-wide - Spawn: .backup→.recovery rename (pre-change snapshots no longer conflict with backup branch namespace) - .gitignore: .recovery/ added - HERALD.md: Updated through S62 (backup audit, .recovery rename, night shift launch) - README.md: Stats updated (141 PRs, 2900+ tests) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
ae459853f0
commit
6772cacce2
@@ -38,6 +38,7 @@ tools/
|
|||||||
docs.local/
|
docs.local/
|
||||||
.archive/
|
.archive/
|
||||||
.backup/
|
.backup/
|
||||||
|
.recovery/
|
||||||
.seed/
|
.seed/
|
||||||
.spawn/
|
.spawn/
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> The living record. What happened, what's changing, what matters.
|
> The living record. What happened, what's changing, what matters.
|
||||||
|
|
||||||
**Last updated:** 2026-03-28 | **Session:** 58 | **PRs merged:** 137
|
**Last updated:** 2026-03-29 | **Session:** 62 | **PRs merged:** 141
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -10,11 +10,24 @@
|
|||||||
|
|
||||||
- **15 branches** operational
|
- **15 branches** operational
|
||||||
- **100% seedgo compliance** across all 15 branches, all 33 standards
|
- **100% seedgo compliance** across all 15 branches, all 33 standards
|
||||||
- **2,000+ tests** system-wide
|
- **2,900+ tests** system-wide
|
||||||
- **137 PRs** merged since inception
|
- **141 PRs** merged since inception
|
||||||
|
- **Backup rebuild in progress** — 4-phase autonomous night shift running now
|
||||||
|
|
||||||
## Recent Sessions
|
## Recent Sessions
|
||||||
|
|
||||||
|
### S62 — Backup Deep Audit + Night Shift Launch (2026-03-29)
|
||||||
|
Full backup branch investigation with 8 parallel agents (inventory, CLI, ignore patterns, Google Drive, live test, routing, tests, diff). Found: snapshot broken by JSON corruption (versioned works fine), 388GB legacy data from before ignore patterns were fixed, Google Drive auth duplicates API branch. Diff system is clean and stays. Renamed `.backup` to `.recovery` system-wide (79 directories) so backup branch owns the `.backup` namespace. Drone adapter pattern investigated — identical boilerplate across branches, documented for future redesign. Backup proposed a 4-phase plan (cleanup, JSON fix, Google Drive migration to API, test coverage), Patrick approved, and backup is now running autonomously through all phases overnight.
|
||||||
|
|
||||||
|
### S61 — Branch Audit Deep-Dive: API + Drone (2026-03-29)
|
||||||
|
API dispatched with 18-item P0/P1/P2 cleanup list — all fixed (186 tests, 100% seedgo). Drone audit verified all 4 architecture fixes from boardroom consensus (self-routing removed, adapter hack deleted, dual help paths unified, @ enforcement working). Naming checker false positives traced to seedgo — 71 bypasses across 11 branches eliminated by fixing `__dunder__` skip and local variable scope detection. Drone path routing fixed with passport walk-up (replaces hardcoded `src/aipass/<branch>` pattern). Access control investigation revealed no registry-scoped auth exists — DPLAN-0083 created. 4 new DPLANs: git workflow, VS Code reload bug, flow audit, access control.
|
||||||
|
|
||||||
|
### S60 — System Verification Wave (2026-03-29)
|
||||||
|
Prax queue spam eliminated (144k log entries per 4 hours). 9 stale plans closed. 15 branch audit DPLANs reorganized into dedicated directory. TTS Listen summaries added to all DPLANs (pure plaintext for Piper). 15-agent verification wave fact-checked every branch audit against live seedgo + pytest. System: 2,905 tests, 100% seedgo across all 15 branches.
|
||||||
|
|
||||||
|
### S59 — Full System Walkthrough (2026-03-28)
|
||||||
|
11 agents audited all 15 branches (2,378 tests at the time). Docker install verified — found and fixed registry format mismatch and seedgo CLI entry point bugs. README rewritten with verified claims. HERALD.md created. Dispatched 8 branches for fixes. PR #140 merged.
|
||||||
|
|
||||||
### S58 — Night Shift: 100% Compliance (2026-03-28)
|
### S58 — Night Shift: 100% Compliance (2026-03-28)
|
||||||
The big one. Every branch, every standard, 100%. Seven agents deployed overnight to fix the final 8 branches that were stuck at 99%. Commons was the hardest — test_quality at 68%, unused functions, architecture gaps, deep nesting. All fixed. Daemon needed plugin architecture bypasses. Memory and spawn needed test gaps filled. PR #137 (167 files, +12,843 lines).
|
The big one. Every branch, every standard, 100%. Seven agents deployed overnight to fix the final 8 branches that were stuck at 99%. Commons was the hardest — test_quality at 68%, unused functions, architecture gaps, deep nesting. All fixed. Daemon needed plugin architecture bypasses. Memory and spawn needed test gaps filled. PR #137 (167 files, +12,843 lines).
|
||||||
|
|
||||||
@@ -43,17 +56,22 @@ First autonomous night shift. PR #118 (75 files). Persistent git branches (citiz
|
|||||||
|
|
||||||
| DPLAN | Subject | Status |
|
| DPLAN | Subject | Status |
|
||||||
|-------|---------|--------|
|
|-------|---------|--------|
|
||||||
| 0031 | Drone audit + git workflow improvements | Open — master key concept, PR review gate |
|
| 0029 | API branch audit | Complete — 186 tests, 100% seedgo, all P0/P1/P2 items fixed |
|
||||||
| 0033 | Commons audit | Silent catch done, orphan cleanup remaining |
|
| 0034 | Backup branch audit | In progress — 4-phase rebuild running overnight |
|
||||||
| 0034 | Backup audit | Silent catch done, scope/filtering remaining |
|
| 0035 | Spawn branch audit | Template overhaul complete, .backup→.recovery rename done |
|
||||||
| 0035 | Spawn audit | Template overhaul complete, nesting deferred |
|
| 0036 | AI Mail audit | Silent catch done, nesting + reply-while-locked bug remaining |
|
||||||
| 0036 | AI Mail audit | Silent catch done, nesting + reply-while-locked bug |
|
| 0053 | Drone branch audit | Architecture fixes complete, adapter redesign documented |
|
||||||
| 0049 | API compliance | 100% achieved |
|
| 0080 | Devpulse git workflow | Design captured, not built yet |
|
||||||
|
| 0082 | Flow branch audit | Created, not started |
|
||||||
|
| 0083 | Access control design | Investigation complete, design pending |
|
||||||
|
|
||||||
## Key Milestones
|
## Key Milestones
|
||||||
|
|
||||||
| Date | Milestone |
|
| Date | Milestone |
|
||||||
|------|-----------|
|
|------|-----------|
|
||||||
|
| 2026-03-29 | Backup rebuild launched — 4-phase autonomous night shift |
|
||||||
|
| 2026-03-29 | .backup→.recovery rename — 79 dirs, namespace clarity |
|
||||||
|
| 2026-03-29 | Branch audit deep-dives — API complete, Drone complete, Backup in progress |
|
||||||
| 2026-03-28 | 100% seedgo compliance — all 15 branches, all 33 standards |
|
| 2026-03-28 | 100% seedgo compliance — all 15 branches, all 33 standards |
|
||||||
| 2026-03-25 | Spawn template overhaul — registry regen, 41-file template |
|
| 2026-03-25 | Spawn template overhaul — registry regen, 41-file template |
|
||||||
| 2026-03-24 | First autonomous night shift — 6 branches dispatched, all returned |
|
| 2026-03-24 | First autonomous night shift — 6 branches dispatched, all returned |
|
||||||
@@ -76,9 +94,9 @@ First autonomous night shift. PR #118 (75 files). Persistent git branches (citiz
|
|||||||
```
|
```
|
||||||
Branches: 15
|
Branches: 15
|
||||||
Standards: 33 (was 34, consolidated in S57)
|
Standards: 33 (was 34, consolidated in S57)
|
||||||
Tests: 2,000+
|
Tests: 2,900+
|
||||||
PRs merged: 137
|
PRs merged: 141
|
||||||
Sessions: 58
|
Sessions: 62
|
||||||
Compliance: 100%
|
Compliance: 100%
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ Git workflow is atomic via `drone @git pr` — one command handles: lock acquisi
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Beta. 15 branches. 137 PRs merged. 2,000+ tests. 100% compliance across 33 standards. See [HERALD.md](HERALD.md) for detailed progress and session history.
|
Beta. 15 branches. 141 PRs merged. 2,900+ tests. 100% compliance across 33 standards. See [HERALD.md](HERALD.md) for detailed progress and session history.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,11 @@
|
|||||||
"file": "apps/modules/google_client.py",
|
"file": "apps/modules/google_client.py",
|
||||||
"standard": "unused_function",
|
"standard": "unused_function",
|
||||||
"reason": "Public API exports for cross-branch consumption (get_drive_service, authenticate_google, validate_google, reauth_google). Tested in test_google_client.py. Backup migration pending."
|
"reason": "Public API exports for cross-branch consumption (get_drive_service, authenticate_google, validate_google, reauth_google). Tested in test_google_client.py. Backup migration pending."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "apps/modules/openrouter_client.py",
|
||||||
|
"standard": "modules",
|
||||||
|
"reason": "make_call() is a CLI orchestrator — parses args then delegates to client.get_response(). All business logic lives in handlers/openrouter/client.py."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"notes": {
|
"notes": {
|
||||||
|
|||||||
@@ -157,8 +157,15 @@ def print_help():
|
|||||||
table.add_row("reauth google", "Re-authenticate Google OAuth2")
|
table.add_row("reauth google", "Re-authenticate Google OAuth2")
|
||||||
table.add_row("test", "Test OpenRouter connection status")
|
table.add_row("test", "Test OpenRouter connection status")
|
||||||
table.add_row("models", "List available models from provider")
|
table.add_row("models", "List available models from provider")
|
||||||
|
table.add_row("status", "Check OpenRouter client status")
|
||||||
|
table.add_row("call", "Make API call to model")
|
||||||
|
table.add_row("list-providers", "List available API providers")
|
||||||
|
table.add_row("init", "Initialize .env template")
|
||||||
table.add_row("track", "Track API usage metrics")
|
table.add_row("track", "Track API usage metrics")
|
||||||
table.add_row("stats", "Display API usage statistics")
|
table.add_row("stats", "Display API usage statistics")
|
||||||
|
table.add_row("session", "Show session usage data")
|
||||||
|
table.add_row("caller-usage", "Show usage by caller module")
|
||||||
|
table.add_row("cleanup", "Clean up old usage data")
|
||||||
|
|
||||||
console.print(table)
|
console.print(table)
|
||||||
console.print()
|
console.print()
|
||||||
@@ -193,7 +200,7 @@ def print_help():
|
|||||||
console.print("─" * 70)
|
console.print("─" * 70)
|
||||||
console.print()
|
console.print()
|
||||||
|
|
||||||
console.print("[dim]Commands: get-key, validate, test, models, track, stats, help, --help[/dim]")
|
console.print("[dim]Commands: get-key, validate, test, models, status, call, list-providers, init, track, stats, session, caller-usage, cleanup[/dim]")
|
||||||
console.print()
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -42,9 +42,9 @@ def _guard_branch_access():
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
if os.environ.get("AIPASS_DEBUG_GUARD"):
|
if os.environ.get("AIPASS_DEBUG_GUARD"):
|
||||||
import sys
|
from aipass.prax import logger as _guard_logger
|
||||||
print(f"[GUARD DEBUG] caller_file = {caller_file}", file=sys.stderr)
|
_guard_logger.info(f"[GUARD] caller_file = {caller_file}")
|
||||||
print(f"[GUARD DEBUG] import_line = {import_line}", file=sys.stderr)
|
_guard_logger.info(f"[GUARD] import_line = {import_line}")
|
||||||
|
|
||||||
if caller_file is None:
|
if caller_file is None:
|
||||||
stack = inspect.stack()
|
stack = inspect.stack()
|
||||||
|
|||||||
@@ -17,10 +17,9 @@ Functions:
|
|||||||
|
|
||||||
# Infrastructure
|
# Infrastructure
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
|
||||||
|
|
||||||
# Standard library
|
# Standard library
|
||||||
from typing import Optional, Dict
|
from typing import Optional
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
from aipass.prax import logger
|
from aipass.prax import logger
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import json
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
import sys
|
|
||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
@@ -121,7 +120,7 @@ def ensure_json_exists(module_name: str, json_type: str) -> bool:
|
|||||||
if validate_json_structure(data, json_type):
|
if validate_json_structure(data, json_type):
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
pass # Corrupted - regenerating
|
logger.warning(f"Corrupted JSON structure at {json_path}, regenerating")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Unreadable JSON at {json_path}, regenerating: {e}")
|
logger.warning(f"Unreadable JSON at {json_path}, regenerating: {e}")
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
OpenRouter Client Handler
|
OpenRouter Client Handler
|
||||||
|
|
||||||
Business logic for OpenRouter API client creation and request execution.
|
Business logic for OpenRouter API client creation and request execution.
|
||||||
Extracted from archive.temp/openrouter.py following AIPASS standards.
|
OpenRouter API client creation and request execution.
|
||||||
|
|
||||||
Functions:
|
Functions:
|
||||||
- get_response() - Main API call with tracking integration
|
- get_response() - Main API call with tracking integration
|
||||||
@@ -34,8 +34,6 @@ Standards:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# INFRASTRUCTURE IMPORT PATTERN
|
# INFRASTRUCTURE IMPORT PATTERN
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# Standard library imports
|
# Standard library imports
|
||||||
import time
|
import time
|
||||||
@@ -193,6 +191,7 @@ def make_api_request(client: OpenAI, messages: List[Dict], model: str, retries:
|
|||||||
OpenAI response object or None on failure
|
OpenAI response object or None on failure
|
||||||
"""
|
"""
|
||||||
if not client or not messages or not model:
|
if not client or not messages or not model:
|
||||||
|
logger.warning(f"make_api_request() called with missing params — client={bool(client)}, messages={bool(messages)}, model={bool(model)}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
api_params = {
|
api_params = {
|
||||||
@@ -242,9 +241,11 @@ def extract_response(response: Any) -> Optional[Dict[str, Any]]:
|
|||||||
try:
|
try:
|
||||||
# Validate response structure
|
# Validate response structure
|
||||||
if not hasattr(response, 'choices') or not response.choices:
|
if not hasattr(response, 'choices') or not response.choices:
|
||||||
|
logger.warning("Response missing 'choices' or choices is empty")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not hasattr(response.choices[0], 'message'):
|
if not hasattr(response.choices[0], 'message'):
|
||||||
|
logger.warning("Response choice missing 'message' attribute")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Extract content
|
# Extract content
|
||||||
|
|||||||
@@ -25,12 +25,11 @@ COMPLIANT STANDARDS:
|
|||||||
- Under 300 lines
|
- Under 300 lines
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, Any, Optional, Tuple
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
from aipass.prax import logger
|
from aipass.prax import logger
|
||||||
|
|
||||||
@@ -102,7 +101,7 @@ def get_default_caller_config() -> Dict[str, Any]:
|
|||||||
Dict with default OpenRouter configuration (model must be set by caller)
|
Dict with default OpenRouter configuration (model must be set by caller)
|
||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
"skill_name": "openrouter",
|
"module_name": "openrouter",
|
||||||
"timestamp": datetime.now().isoformat(),
|
"timestamp": datetime.now().isoformat(),
|
||||||
"config": {
|
"config": {
|
||||||
"ai_model": "", # Caller must set their own model
|
"ai_model": "", # Caller must set their own model
|
||||||
@@ -121,7 +120,7 @@ def get_default_caller_data() -> Dict[str, Any]:
|
|||||||
Dict with initial usage tracking data
|
Dict with initial usage tracking data
|
||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
"skill_name": "openrouter",
|
"module_name": "openrouter",
|
||||||
"timestamp": datetime.now().isoformat(),
|
"timestamp": datetime.now().isoformat(),
|
||||||
"data": {
|
"data": {
|
||||||
"total_requests": 0,
|
"total_requests": 0,
|
||||||
@@ -141,7 +140,7 @@ def get_default_caller_log() -> Dict[str, Any]:
|
|||||||
Dict with empty log structure
|
Dict with empty log structure
|
||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
"skill_name": "openrouter",
|
"module_name": "openrouter",
|
||||||
"timestamp": datetime.now().isoformat(),
|
"timestamp": datetime.now().isoformat(),
|
||||||
"logs": []
|
"logs": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ Business logic for aggregating usage statistics:
|
|||||||
- Cost, token, and latency aggregation
|
- Cost, token, and latency aggregation
|
||||||
- Model usage tracking and breakdown
|
- Model usage tracking and breakdown
|
||||||
|
|
||||||
Extracted from legacy archive (api_usage.py).
|
|
||||||
Functions: get_caller_usage(), get_session_summary()
|
Functions: get_caller_usage(), get_session_summary()
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,9 @@ Business logic for tracking API usage from OpenRouter:
|
|||||||
- Store generation tracking data with newest-first ordering
|
- Store generation tracking data with newest-first ordering
|
||||||
- Handle HTTP requests with proper error handling
|
- Handle HTTP requests with proper error handling
|
||||||
|
|
||||||
Extracted from legacy archive (api_usage.py).
|
Functions: track_usage(), get_generation_metrics(), store_usage_data()
|
||||||
Functions: track_usage(), _get_generation_metrics(), _store_usage_data()
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Standard library imports
|
# Standard library imports
|
||||||
@@ -147,7 +145,8 @@ def get_generation_metrics(generation_id: str, api_key: str) -> Optional[Dict[st
|
|||||||
|
|
||||||
# Query the generation endpoint
|
# Query the generation endpoint
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
f"{GENERATION_ENDPOINT}?id={generation_id}",
|
GENERATION_ENDPOINT,
|
||||||
|
params={"id": generation_id},
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=30
|
timeout=30
|
||||||
)
|
)
|
||||||
@@ -158,7 +157,7 @@ def get_generation_metrics(generation_id: str, api_key: str) -> Optional[Dict[st
|
|||||||
|
|
||||||
# Validate response structure
|
# Validate response structure
|
||||||
if not data or "data" not in data:
|
if not data or "data" not in data:
|
||||||
# Invalid response structure from OpenRouter
|
logger.warning(f"[{MODULE_NAME}] Invalid response structure from OpenRouter for generation {generation_id}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Extract metrics from response
|
# Extract metrics from response
|
||||||
@@ -176,7 +175,7 @@ def get_generation_metrics(generation_id: str, api_key: str) -> Optional[Dict[st
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# OpenRouter API returned non-200 status
|
logger.warning(f"[{MODULE_NAME}] OpenRouter API returned status {response.status_code} for generation {generation_id}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
except requests.exceptions.Timeout as e:
|
except requests.exceptions.Timeout as e:
|
||||||
|
|||||||
@@ -20,10 +20,9 @@ from pathlib import Path
|
|||||||
|
|
||||||
from typing import List
|
from typing import List
|
||||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||||
from aipass.cli.apps.modules import console, header, success, error, warning, section
|
from aipass.cli.apps.modules import console, header, success, error
|
||||||
from aipass.api.apps.handlers.json import json_handler
|
from aipass.api.apps.handlers.json import json_handler
|
||||||
from aipass.api.apps.handlers.auth import keys, env
|
from aipass.api.apps.handlers.auth import keys, env
|
||||||
from aipass.api.apps.handlers.config import provider
|
|
||||||
|
|
||||||
|
|
||||||
def print_introspection():
|
def print_introspection():
|
||||||
@@ -110,7 +109,8 @@ def get_key(args: List[str]):
|
|||||||
|
|
||||||
if api_key:
|
if api_key:
|
||||||
success(f"API key retrieved for {provider_name}")
|
success(f"API key retrieved for {provider_name}")
|
||||||
console.print(f" Key (first 20 chars): {api_key[:20]}...")
|
masked = api_key[:6] + "****" + api_key[-4:] if len(api_key) > 10 else "****"
|
||||||
|
console.print(f" Key: {masked}")
|
||||||
else:
|
else:
|
||||||
error(f"Failed to retrieve API key for {provider_name}")
|
error(f"Failed to retrieve API key for {provider_name}")
|
||||||
|
|
||||||
|
|||||||
@@ -124,11 +124,6 @@ def handle_command(command: str, args: List[str]) -> bool:
|
|||||||
Returns:
|
Returns:
|
||||||
True if command was handled, False to pass through.
|
True if command was handled, False to pass through.
|
||||||
"""
|
"""
|
||||||
# Help gate
|
|
||||||
if args and args[0] in ("--help", "-h", "help"):
|
|
||||||
print_help()
|
|
||||||
return True
|
|
||||||
|
|
||||||
# NO-ARGS GATE (seedgo standard)
|
# NO-ARGS GATE (seedgo standard)
|
||||||
if not args:
|
if not args:
|
||||||
if command == "google":
|
if command == "google":
|
||||||
@@ -140,6 +135,12 @@ def handle_command(command: str, args: List[str]) -> bool:
|
|||||||
if args[0] != "google":
|
if args[0] != "google":
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Help gate — only fires after confirming this is a google command
|
||||||
|
remaining = args[1:] if len(args) > 1 else []
|
||||||
|
if remaining and remaining[0] in ("--help", "-h", "help"):
|
||||||
|
print_help()
|
||||||
|
return True
|
||||||
|
|
||||||
if command == "validate":
|
if command == "validate":
|
||||||
_cmd_validate()
|
_cmd_validate()
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -17,11 +17,10 @@ Orchestrates LLM API client operations:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from typing import List
|
from typing import List
|
||||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||||
from aipass.cli.apps.modules import console, header, success, error, warning, section
|
from aipass.cli.apps.modules import console, header, success, error
|
||||||
from aipass.api.apps.handlers.json import json_handler
|
from aipass.api.apps.handlers.json import json_handler
|
||||||
from aipass.api.apps.handlers.auth import keys
|
from aipass.api.apps.handlers.auth import keys
|
||||||
from aipass.api.apps.handlers.openrouter import client, models
|
from aipass.api.apps.handlers.openrouter import client, models
|
||||||
@@ -188,7 +187,42 @@ def make_call(args: List[str]):
|
|||||||
header("OpenRouter API Call")
|
header("OpenRouter API Call")
|
||||||
console.print()
|
console.print()
|
||||||
|
|
||||||
warning("API call workflow - TODO")
|
if not args:
|
||||||
|
error("Prompt required", suggestion="drone @api call \"your prompt\" --model MODEL")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Parse args: first non-flag arg is prompt, --model MODEL is optional
|
||||||
|
prompt = None
|
||||||
|
model = None
|
||||||
|
i = 0
|
||||||
|
while i < len(args):
|
||||||
|
if args[i] == "--model" and i + 1 < len(args):
|
||||||
|
model = args[i + 1]
|
||||||
|
i += 2
|
||||||
|
elif prompt is None:
|
||||||
|
prompt = args[i]
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if not prompt:
|
||||||
|
error("Prompt required", suggestion="drone @api call \"your prompt\" --model MODEL")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not model:
|
||||||
|
error("Model required", suggestion="drone @api call \"your prompt\" --model anthropic/claude-3.5-sonnet")
|
||||||
|
return
|
||||||
|
|
||||||
|
console.print(f"[dim]Calling {model}...[/dim]")
|
||||||
|
|
||||||
|
response = client.get_response(prompt, caller="cli", model=model)
|
||||||
|
|
||||||
|
if response:
|
||||||
|
success(f"Response received ({len(response['content'])} chars)")
|
||||||
|
console.print()
|
||||||
|
console.print(response["content"])
|
||||||
|
else:
|
||||||
|
error("API call failed")
|
||||||
|
|
||||||
|
|
||||||
def list_models(args: List[str] | None = None):
|
def list_models(args: List[str] | None = None):
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from typing import List
|
from typing import List
|
||||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||||
from aipass.cli.apps.modules import console, header, success, error, warning, section
|
from aipass.cli.apps.modules import console, header, success, error, warning
|
||||||
from aipass.api.apps.handlers.json import json_handler
|
from aipass.api.apps.handlers.json import json_handler
|
||||||
from aipass.api.apps.handlers.usage import tracking, aggregation, cleanup
|
from aipass.api.apps.handlers.usage import tracking, aggregation, cleanup
|
||||||
|
|
||||||
@@ -174,7 +174,22 @@ def track_usage(args: List[str]):
|
|||||||
header("Track API Usage")
|
header("Track API Usage")
|
||||||
console.print()
|
console.print()
|
||||||
|
|
||||||
warning("Usage tracking workflow - TODO")
|
if not args:
|
||||||
|
error("Generation ID required", suggestion="drone @api track <generation_id> [caller]")
|
||||||
|
return
|
||||||
|
|
||||||
|
generation_id = args[0]
|
||||||
|
caller = args[1] if len(args) > 1 else "manual"
|
||||||
|
|
||||||
|
console.print(f"[dim]Tracking generation {generation_id}...[/dim]")
|
||||||
|
|
||||||
|
result = tracking.track_usage(generation_id, caller)
|
||||||
|
|
||||||
|
if result.get("success"):
|
||||||
|
metrics = result.get("metrics", {})
|
||||||
|
success(f"Tracked: {metrics.get('tokens_prompt', 0)} prompt + {metrics.get('tokens_completion', 0)} completion tokens, ${metrics.get('total_cost', 0):.6f}")
|
||||||
|
else:
|
||||||
|
error(f"Tracking failed: {result.get('error', 'unknown')}")
|
||||||
|
|
||||||
|
|
||||||
def show_stats():
|
def show_stats():
|
||||||
|
|||||||
@@ -137,10 +137,10 @@ def test_handle_command_routes_reauth_google(
|
|||||||
def test_handle_command_help_gate(
|
def test_handle_command_help_gate(
|
||||||
_warn, _err, _succ, _hdr, _json, _retry, _factory, _auth, mock_console
|
_warn, _err, _succ, _hdr, _json, _retry, _factory, _auth, mock_console
|
||||||
):
|
):
|
||||||
"""handle_command prints help when args=['--help'] and returns True."""
|
"""handle_command prints help when args=['google', '--help'] and returns True."""
|
||||||
from aipass.api.apps.modules import google_client
|
from aipass.api.apps.modules import google_client
|
||||||
|
|
||||||
result = google_client.handle_command("validate", ["--help"])
|
result = google_client.handle_command("validate", ["google", "--help"])
|
||||||
|
|
||||||
assert result is True
|
assert result is True
|
||||||
# print_help calls console.print with the argparse output
|
# print_help calls console.print with the argparse output
|
||||||
|
|||||||
@@ -596,17 +596,17 @@ def test_list_models_formats_free_pricing(mock_console, mock_header, mock_keys,
|
|||||||
# =============================================
|
# =============================================
|
||||||
|
|
||||||
|
|
||||||
@patch(f"{_MOD}.warning")
|
@patch(f"{_MOD}.error")
|
||||||
@patch(f"{_MOD}.header")
|
@patch(f"{_MOD}.header")
|
||||||
@patch(f"{_MOD}.console")
|
@patch(f"{_MOD}.console")
|
||||||
def test_make_call_shows_todo_warning(mock_console, mock_header, mock_warning):
|
def test_make_call_no_model_shows_error(mock_console, mock_header, mock_error):
|
||||||
"""make_call currently shows a TODO warning."""
|
"""make_call without --model shows error."""
|
||||||
from aipass.api.apps.modules import openrouter_client
|
from aipass.api.apps.modules import openrouter_client
|
||||||
|
|
||||||
openrouter_client.make_call(["What is AI?"])
|
openrouter_client.make_call(["What is AI?"])
|
||||||
|
|
||||||
mock_warning.assert_called_once()
|
mock_error.assert_called_once()
|
||||||
assert "TODO" in mock_warning.call_args[0][0]
|
assert "Model required" in mock_error.call_args[0][0]
|
||||||
|
|
||||||
|
|
||||||
# =============================================
|
# =============================================
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ def test_create_caller_config_defaults(tmp_path: Path):
|
|||||||
assert config["config"]["ai_max_tokens"] == 4000
|
assert config["config"]["ai_max_tokens"] == 4000
|
||||||
assert config["config"]["enabled"] is True
|
assert config["config"]["enabled"] is True
|
||||||
assert config["config"]["ai_model"] == ""
|
assert config["config"]["ai_model"] == ""
|
||||||
assert config["skill_name"] == "openrouter"
|
assert config["module_name"] == "openrouter"
|
||||||
|
|
||||||
|
|
||||||
def test_create_caller_config_data_defaults(tmp_path: Path):
|
def test_create_caller_config_data_defaults(tmp_path: Path):
|
||||||
@@ -87,7 +87,7 @@ def test_create_caller_config_log_defaults(tmp_path: Path):
|
|||||||
log = read_json(json_folder / "openrouter_skill_log.json")
|
log = read_json(json_folder / "openrouter_skill_log.json")
|
||||||
assert log is not None
|
assert log is not None
|
||||||
assert log["logs"] == []
|
assert log["logs"] == []
|
||||||
assert log["skill_name"] == "openrouter"
|
assert log["module_name"] == "openrouter"
|
||||||
|
|
||||||
|
|
||||||
def test_create_caller_config_creates_folder(tmp_path: Path):
|
def test_create_caller_config_creates_folder(tmp_path: Path):
|
||||||
@@ -187,7 +187,7 @@ def test_get_default_caller_config_structure():
|
|||||||
"""Default config should have expected structure."""
|
"""Default config should have expected structure."""
|
||||||
config = get_default_caller_config()
|
config = get_default_caller_config()
|
||||||
|
|
||||||
assert "skill_name" in config
|
assert "module_name" in config
|
||||||
assert "timestamp" in config
|
assert "timestamp" in config
|
||||||
assert "config" in config
|
assert "config" in config
|
||||||
assert isinstance(config["config"], dict)
|
assert isinstance(config["config"], dict)
|
||||||
|
|||||||
@@ -44,85 +44,6 @@
|
|||||||
29
|
29
|
||||||
],
|
],
|
||||||
"reason": "Scanner needs discovery_handler's get_entry_point() and parse_help_for_commands() to discover commands from branch help output. Justified — scanner's core function depends on discovery's parsing."
|
"reason": "Scanner needs discovery_handler's get_entry_point() and parse_help_for_commands() to discover commands from branch help output. Justified — scanner's core function depends on discovery's parsing."
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "apps/modules/router.py",
|
|
||||||
"standard": "naming",
|
|
||||||
"reason": "branch_path, branch_name, command are local variables inside route_command(), not module-level constants. False positive from naming checker."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "apps/modules/scan.py",
|
|
||||||
"standard": "naming",
|
|
||||||
"pattern": "__all__",
|
|
||||||
"reason": "__all__ is a standard Python convention (PEP 8) — lowercase dunder is correct."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "apps/modules/commands.py",
|
|
||||||
"standard": "naming",
|
|
||||||
"pattern": "__all__",
|
|
||||||
"reason": "__all__ is a standard Python convention (PEP 8) — lowercase dunder is correct."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "apps/modules/config.py",
|
|
||||||
"standard": "naming",
|
|
||||||
"pattern": "__all__",
|
|
||||||
"reason": "__all__ is a standard Python convention (PEP 8) — lowercase dunder is correct."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "apps/modules/module_registry.py",
|
|
||||||
"standard": "naming",
|
|
||||||
"pattern": "__all__",
|
|
||||||
"reason": "__all__ is a standard Python convention (PEP 8) — lowercase dunder is correct."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "apps/modules/registry.py",
|
|
||||||
"standard": "naming",
|
|
||||||
"pattern": "__all__",
|
|
||||||
"reason": "__all__ is a standard Python convention (PEP 8) — lowercase dunder is correct."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "apps/modules/__init__.py",
|
|
||||||
"standard": "naming",
|
|
||||||
"pattern": "__all__",
|
|
||||||
"reason": "__all__ is a standard Python convention (PEP 8) — lowercase dunder is correct."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "apps/handlers/json/json_handler.py",
|
|
||||||
"standard": "naming",
|
|
||||||
"pattern": "__all__",
|
|
||||||
"reason": "__all__ is a standard Python convention (PEP 8) — lowercase dunder is correct."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "apps/handlers/scanning/__init__.py",
|
|
||||||
"standard": "naming",
|
|
||||||
"pattern": "__all__",
|
|
||||||
"reason": "__all__ is a standard Python convention (PEP 8) — lowercase dunder is correct."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "__init__.py",
|
|
||||||
"standard": "naming",
|
|
||||||
"pattern": "__all__",
|
|
||||||
"reason": "__all__ is a standard Python convention (PEP 8) — lowercase dunder is correct."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "apps/handlers/router_handler.py",
|
|
||||||
"standard": "naming",
|
|
||||||
"reason": "cmd_args, executable, args are local variables inside execute_branch_command(), not module-level constants. False positive."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "apps/handlers/executor.py",
|
|
||||||
"standard": "naming",
|
|
||||||
"reason": "run_env, cwd, shell are local variables inside run_command(), not module-level constants. False positive."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "apps/handlers/registry_handler.py",
|
|
||||||
"standard": "naming",
|
|
||||||
"reason": "filtered is a local variable inside get_all_branches(), not a module-level constant. False positive."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "apps/handlers/command_registry/ops.py",
|
|
||||||
"standard": "naming",
|
|
||||||
"reason": "module_name is a local variable, not a module-level constant. False positive."
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"notes": {
|
"notes": {
|
||||||
@@ -138,6 +59,7 @@
|
|||||||
"lines": "Optional - specific line numbers to bypass",
|
"lines": "Optional - specific line numbers to bypass",
|
||||||
"pattern": "Optional - pattern to match (e.g. 'if __name__')",
|
"pattern": "Optional - pattern to match (e.g. 'if __name__')",
|
||||||
"reason": "Required - why this bypass exists"
|
"reason": "Required - why this bypass exists"
|
||||||
}
|
},
|
||||||
|
"removed_s61": "13 naming bypasses removed (9 __all__ + 4 local variable false positives). Seedgo dispatched to fix naming_check.py. If audit fails on naming after this, the checker fix didn't land."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,14 +32,31 @@ _COMMANDS = ("pr", "status", "sync", "lock", "unlock")
|
|||||||
|
|
||||||
|
|
||||||
def _detect_branch_dir() -> tuple[str, Path] | None:
|
def _detect_branch_dir() -> tuple[str, Path] | None:
|
||||||
"""Detect caller's branch from CWD. Returns (branch_name, branch_dir) or None."""
|
"""Detect caller's branch from CWD via passport lookup.
|
||||||
cwd = Path.cwd()
|
|
||||||
parts = cwd.parts
|
Walks up from CWD looking for ``.trinity/passport.json`` and extracts
|
||||||
for i, part in enumerate(parts):
|
the branch name + directory. Works for any registered branch regardless
|
||||||
if part == "aipass" and i > 0 and parts[i - 1] == "src" and i + 1 < len(parts):
|
of where it lives on disk (commons, skills, aipass sub-dirs, etc.).
|
||||||
branch_name = parts[i + 1]
|
"""
|
||||||
branch_dir = Path(*parts[: i + 2])
|
current = Path.cwd().resolve()
|
||||||
return branch_name, branch_dir
|
for _ in range(10):
|
||||||
|
passport = current / ".trinity" / "passport.json"
|
||||||
|
if passport.exists():
|
||||||
|
try:
|
||||||
|
with open(passport, "r", encoding="utf-8") as fh:
|
||||||
|
data = json.load(fh)
|
||||||
|
name = data.get("branch_info", {}).get("branch_name")
|
||||||
|
if not name:
|
||||||
|
name = data.get("identity", {}).get("name")
|
||||||
|
if name:
|
||||||
|
return name, current
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Failed to read passport at %s: %s", passport, exc)
|
||||||
|
return None
|
||||||
|
parent = current.parent
|
||||||
|
if parent == current:
|
||||||
|
break
|
||||||
|
current = parent
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -585,24 +585,52 @@ class TestGitModuleRouting:
|
|||||||
class TestDetectBranchDir:
|
class TestDetectBranchDir:
|
||||||
"""Branch directory detection tests."""
|
"""Branch directory detection tests."""
|
||||||
|
|
||||||
def test_detects_branch_from_path(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_detects_branch_from_passport(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
"""Detects branch name and dir from a path like .../src/aipass/drone/..."""
|
"""Detects branch name and dir by walking up to .trinity/passport.json."""
|
||||||
# Simulate CWD being inside a branch
|
# Create a fake branch directory with passport
|
||||||
fake_path = Path("/home/user/Projects/AIPass/src/aipass/drone/apps/modules")
|
branch_dir = tmp_path / "mybranch"
|
||||||
monkeypatch.chdir("/") # Just need a valid dir for the test
|
trinity = branch_dir / ".trinity"
|
||||||
with patch("aipass.drone.apps.modules.git_module.Path.cwd", return_value=fake_path):
|
trinity.mkdir(parents=True)
|
||||||
detected = _detect_branch_dir()
|
passport = trinity / "passport.json"
|
||||||
|
passport.write_text(json.dumps({
|
||||||
|
"branch_info": {"branch_name": "mybranch"},
|
||||||
|
}))
|
||||||
|
|
||||||
|
# CWD is inside a subdirectory of the branch
|
||||||
|
sub_dir = branch_dir / "apps" / "modules"
|
||||||
|
sub_dir.mkdir(parents=True)
|
||||||
|
monkeypatch.chdir(sub_dir)
|
||||||
|
|
||||||
|
detected = _detect_branch_dir()
|
||||||
assert detected is not None
|
assert detected is not None
|
||||||
name, bdir = detected
|
name, bdir = detected
|
||||||
assert name == "drone"
|
assert name == "mybranch"
|
||||||
|
assert bdir == branch_dir.resolve()
|
||||||
|
|
||||||
def test_returns_none_for_unrecognized_path(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_returns_none_for_unrecognized_path(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
"""Returns None when CWD is not under src/aipass/."""
|
"""Returns None when CWD has no .trinity/passport.json above it."""
|
||||||
monkeypatch.chdir(tmp_path)
|
monkeypatch.chdir(tmp_path)
|
||||||
detected = _detect_branch_dir()
|
detected = _detect_branch_dir()
|
||||||
assert detected is None
|
assert detected is None
|
||||||
|
|
||||||
|
def test_detects_non_aipass_branch(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""Detects branches outside src/aipass/ (e.g. commons, skills)."""
|
||||||
|
branch_dir = tmp_path / "src" / "commons"
|
||||||
|
trinity = branch_dir / ".trinity"
|
||||||
|
trinity.mkdir(parents=True)
|
||||||
|
passport = trinity / "passport.json"
|
||||||
|
passport.write_text(json.dumps({
|
||||||
|
"branch_info": {"branch_name": "commons"},
|
||||||
|
}))
|
||||||
|
|
||||||
|
monkeypatch.chdir(branch_dir)
|
||||||
|
|
||||||
|
detected = _detect_branch_dir()
|
||||||
|
assert detected is not None
|
||||||
|
name, bdir = detected
|
||||||
|
assert name == "commons"
|
||||||
|
assert bdir == branch_dir.resolve()
|
||||||
|
|
||||||
|
|
||||||
class TestGitModuleHelp:
|
class TestGitModuleHelp:
|
||||||
"""Help and introspection tests."""
|
"""Help and introspection tests."""
|
||||||
|
|||||||
@@ -150,5 +150,6 @@
|
|||||||
"DPLAN-0059_seedgo_test_quality_standard_deep_coverage_en_2026-03-24.md": "2026-03-28T19:52:31.066363",
|
"DPLAN-0059_seedgo_test_quality_standard_deep_coverage_en_2026-03-24.md": "2026-03-28T19:52:31.066363",
|
||||||
"DPLAN-0060_custom_function_scanner_seedgo_module_for_bra_2026-03-24.md": "2026-03-28T19:52:49.501398",
|
"DPLAN-0060_custom_function_scanner_seedgo_module_for_bra_2026-03-24.md": "2026-03-28T19:52:49.501398",
|
||||||
"DPLAN-0061_expand_test_quality_checker_to_all_standard_c_2026-03-24.md": "2026-03-28T19:53:13.017860",
|
"DPLAN-0061_expand_test_quality_checker_to_all_standard_c_2026-03-24.md": "2026-03-28T19:53:13.017860",
|
||||||
"FPLAN-0138_s59_walkthrough_fixes_2026-03-28.md": "2026-03-28T19:53:32.097935"
|
"FPLAN-0138_s59_walkthrough_fixes_2026-03-28.md": "2026-03-28T19:53:32.097935",
|
||||||
|
"FPLAN-0139_dplan_0029_branch_audit_cleanup_fix_all_remai_2026-03-28.md": "2026-03-28T22:57:07.425893"
|
||||||
}
|
}
|
||||||
@@ -41,6 +41,13 @@
|
|||||||
"subject": "Monitor Phase 2: caller attribution via drone CALLER markers",
|
"subject": "Monitor Phase 2: caller attribution via drone CALLER markers",
|
||||||
"date_closed": "2026-03-19",
|
"date_closed": "2026-03-19",
|
||||||
"location": "prax"
|
"location": "prax"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"plan_id": "FPLAN-0071",
|
||||||
|
"type": "FPLAN",
|
||||||
|
"subject": "Self-audit compliance sprint",
|
||||||
|
"date_closed": "2026-03-28",
|
||||||
|
"location": "prax"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,9 @@ Validates module compliance with AIPass naming standards.
|
|||||||
Checks file naming, function naming, variable naming, constant naming.
|
Checks file naming, function naming, variable naming, constant naming.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, Optional
|
||||||
from aipass.prax import logger
|
from aipass.prax import logger
|
||||||
from aipass.seedgo.apps.handlers.json import json_handler
|
from aipass.seedgo.apps.handlers.json import json_handler
|
||||||
|
|
||||||
@@ -90,7 +89,6 @@ def check_module(module_path: str, bypass_rules: list | None = None) -> Dict:
|
|||||||
try:
|
try:
|
||||||
with open(path, 'r', encoding='utf-8') as f:
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
lines = content.split('\n')
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.info("Cannot read %s: %s", path, e)
|
logger.info("Cannot read %s: %s", path, e)
|
||||||
return {
|
return {
|
||||||
@@ -236,49 +234,6 @@ def check_function_naming(content: str) -> Optional[Dict]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _iter_module_level_lines(lines: List[str]) -> List[str]:
|
|
||||||
result = []
|
|
||||||
in_function_or_class = False
|
|
||||||
in_multiline_string = False
|
|
||||||
multiline_delimiter = None
|
|
||||||
current_indentation = 0
|
|
||||||
|
|
||||||
for line in lines:
|
|
||||||
stripped = line.strip()
|
|
||||||
|
|
||||||
if '"""' in stripped or "'''" in stripped:
|
|
||||||
delimiter = '"""' if '"""' in stripped else "'''"
|
|
||||||
count = stripped.count(delimiter)
|
|
||||||
|
|
||||||
if count % 2 == 1 and not in_multiline_string:
|
|
||||||
in_multiline_string = True
|
|
||||||
multiline_delimiter = delimiter
|
|
||||||
elif count % 2 == 1 and in_multiline_string and multiline_delimiter == delimiter:
|
|
||||||
in_multiline_string = False
|
|
||||||
multiline_delimiter = None
|
|
||||||
|
|
||||||
if in_multiline_string:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if (stripped.startswith('def ') or
|
|
||||||
stripped.startswith('class ') or
|
|
||||||
stripped.startswith('async def ') or
|
|
||||||
stripped.startswith('if __name__')):
|
|
||||||
in_function_or_class = True
|
|
||||||
current_indentation = len(line) - len(line.lstrip())
|
|
||||||
continue
|
|
||||||
|
|
||||||
if in_function_or_class and not line.startswith(' ') and not line.startswith('\t') and stripped:
|
|
||||||
in_function_or_class = False
|
|
||||||
current_indentation = 0
|
|
||||||
|
|
||||||
if in_function_or_class:
|
|
||||||
continue
|
|
||||||
|
|
||||||
result.append(stripped)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def check_constant_naming(content: str) -> Optional[Dict]:
|
def check_constant_naming(content: str) -> Optional[Dict]:
|
||||||
"""
|
"""
|
||||||
@@ -286,8 +241,9 @@ def check_constant_naming(content: str) -> Optional[Dict]:
|
|||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
- UPPER_CASE for module-level constants
|
- UPPER_CASE for module-level constants
|
||||||
- Assigned outside of functions/classes
|
- Assigned outside of functions/classes (column 0 only)
|
||||||
- Excludes module imports (from X import Y as Z)
|
- Excludes module imports (from X import Y as Z)
|
||||||
|
- Excludes __dunder__ variables (PEP 8 convention: always lowercase)
|
||||||
"""
|
"""
|
||||||
# First pass: collect imported names to exclude from constant checking
|
# First pass: collect imported names to exclude from constant checking
|
||||||
imported_names = set()
|
imported_names = set()
|
||||||
@@ -306,13 +262,34 @@ def check_constant_naming(content: str) -> Optional[Dict]:
|
|||||||
# Direct import like "console"
|
# Direct import like "console"
|
||||||
imported_names.add(item.strip())
|
imported_names.add(item.strip())
|
||||||
|
|
||||||
# Second pass: find module-level assignments (not inside functions/classes)
|
# Second pass: find TRUE module-level assignments
|
||||||
module_lines = _iter_module_level_lines(content.split('\n'))
|
# Only check lines at column 0 (no indentation) — this eliminates local
|
||||||
|
# variables inside functions/classes, which are always indented.
|
||||||
|
# The old _iter_module_level_lines approach had a bug: multi-line function
|
||||||
|
# signatures with closing ) at column 0 tricked it into thinking the
|
||||||
|
# function body had ended.
|
||||||
constants = []
|
constants = []
|
||||||
bad_constants = []
|
bad_constants = []
|
||||||
|
in_multiline_string = False
|
||||||
|
|
||||||
for stripped in module_lines:
|
for line in content.split('\n'):
|
||||||
# Find assignments at module level
|
stripped = line.strip()
|
||||||
|
|
||||||
|
# Track multiline strings
|
||||||
|
for delimiter in ('"""', "'''"):
|
||||||
|
if delimiter in stripped:
|
||||||
|
count = stripped.count(delimiter)
|
||||||
|
if count % 2 == 1:
|
||||||
|
in_multiline_string = not in_multiline_string
|
||||||
|
|
||||||
|
if in_multiline_string:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Only consider lines with zero indentation (true module-level)
|
||||||
|
if line and (line[0] == ' ' or line[0] == '\t'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Find assignments
|
||||||
if '=' not in stripped or stripped.startswith('#'):
|
if '=' not in stripped or stripped.startswith('#'):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -324,6 +301,10 @@ def check_constant_naming(content: str) -> Optional[Dict]:
|
|||||||
const_name = match.group(1)
|
const_name = match.group(1)
|
||||||
assigned_value = match.group(2)
|
assigned_value = match.group(2)
|
||||||
|
|
||||||
|
# Skip __dunder__ variables (__all__, __version__, etc.)
|
||||||
|
if const_name.startswith('__') and const_name.endswith('__'):
|
||||||
|
continue
|
||||||
|
|
||||||
# Skip if this is an imported name (like logger, console)
|
# Skip if this is an imported name (like logger, console)
|
||||||
if const_name in imported_names:
|
if const_name in imported_names:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ def deep_merge(template_data: Any, existing_data: Any) -> Any:
|
|||||||
def backup_json(file_path: Path) -> Path:
|
def backup_json(file_path: Path) -> Path:
|
||||||
"""Create a timestamped backup of a JSON file before modifying.
|
"""Create a timestamped backup of a JSON file before modifying.
|
||||||
|
|
||||||
The backup is placed in a ``.backup/`` directory alongside the file,
|
The backup is placed in a ``.recovery/`` directory alongside the file,
|
||||||
named with a timestamp suffix.
|
named with a timestamp suffix.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -116,7 +116,7 @@ def backup_json(file_path: Path) -> Path:
|
|||||||
raise FileNotFoundError(f"Cannot backup — file not found: {file_path}")
|
raise FileNotFoundError(f"Cannot backup — file not found: {file_path}")
|
||||||
|
|
||||||
# Create backup directory alongside the file
|
# Create backup directory alongside the file
|
||||||
backup_dir = file_path.parent / ".backup"
|
backup_dir = file_path.parent / ".recovery"
|
||||||
backup_dir.mkdir(parents=True, exist_ok=True)
|
backup_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Timestamped backup name
|
# Timestamped backup name
|
||||||
|
|||||||
@@ -615,7 +615,7 @@ class TestBackupJson:
|
|||||||
"""Tests for backup_json()."""
|
"""Tests for backup_json()."""
|
||||||
|
|
||||||
def test_creates_backup_file(self, tmp_path):
|
def test_creates_backup_file(self, tmp_path):
|
||||||
"""Backup should create a copy in .backup/ directory."""
|
"""Backup should create a copy in .recovery/ directory."""
|
||||||
from aipass.spawn.apps.handlers.json_ops import backup_json
|
from aipass.spawn.apps.handlers.json_ops import backup_json
|
||||||
|
|
||||||
source = tmp_path / "data.json"
|
source = tmp_path / "data.json"
|
||||||
@@ -624,7 +624,7 @@ class TestBackupJson:
|
|||||||
backup_path = backup_json(source)
|
backup_path = backup_json(source)
|
||||||
|
|
||||||
assert backup_path.exists()
|
assert backup_path.exists()
|
||||||
assert ".backup" in str(backup_path)
|
assert ".recovery" in str(backup_path)
|
||||||
assert "data.json" in backup_path.name
|
assert "data.json" in backup_path.name
|
||||||
assert ".backup" in backup_path.name
|
assert ".backup" in backup_path.name
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user