@asobacloud/nehanda 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSE.md +57 -0
  3. package/README.md +444 -2
  4. package/bin/agent.mjs +1260 -0
  5. package/bin/nehanda-ui.mjs +824 -0
  6. package/lib/anthropicHeaders.mjs +8 -0
  7. package/lib/app.mjs +313 -0
  8. package/lib/auth.mjs +255 -0
  9. package/lib/bashguard.mjs +159 -0
  10. package/lib/compact.mjs +306 -0
  11. package/lib/energy-middleware.mjs +318 -0
  12. package/lib/hookShapeOrder.mjs +30 -0
  13. package/lib/hookplane.mjs +228 -0
  14. package/lib/instructions.mjs +23 -0
  15. package/lib/jev_read.mjs +216 -0
  16. package/lib/jev_write.mjs +253 -0
  17. package/lib/laya.mjs +296 -0
  18. package/lib/laya_sidecar.mjs +198 -0
  19. package/lib/mcp-config.mjs +189 -0
  20. package/lib/mcp.mjs +169 -0
  21. package/lib/modelConfig.mjs +152 -0
  22. package/lib/modelDiscovery.mjs +205 -0
  23. package/lib/openaiCompat.mjs +147 -0
  24. package/lib/orchestrate.mjs +1163 -0
  25. package/lib/paths.mjs +20 -0
  26. package/lib/permissions.mjs +44 -0
  27. package/lib/scripts/__pycache__/laya_worker.cpython-314.pyc +0 -0
  28. package/lib/scripts/audit-code-integrity.py +437 -0
  29. package/lib/scripts/ensure-laya-env.sh +55 -0
  30. package/lib/scripts/js-safety-checker.cjs +308 -0
  31. package/lib/scripts/laya_worker.py +171 -0
  32. package/lib/scripts/odse-transform.py +97 -0
  33. package/lib/scripts/python-safety-checker.py +197 -0
  34. package/lib/scripts/shell-safety-checker.sh +306 -0
  35. package/lib/settings.mjs +147 -0
  36. package/lib/store.mjs +278 -0
  37. package/lib/team.mjs +69 -0
  38. package/lib/testgen.mjs +146 -0
  39. package/lib/tools/AuditCodeIntegrity.json +30 -0
  40. package/lib/tools/JsSafetyChecker.json +29 -0
  41. package/lib/tools/PythonSafetyChecker.json +29 -0
  42. package/lib/tools/ShellSafetyChecker.json +29 -0
  43. package/lib/tools.mjs +907 -0
  44. package/lib/transcript.mjs +223 -0
  45. package/lib/ui.mjs +286 -0
  46. package/lib/workflow.mjs +91 -0
  47. package/lib/worktree.mjs +56 -0
  48. package/package.json +50 -4
  49. package/schema.sql +181 -0
  50. package/scripts/postinstall.mjs +62 -0
  51. package/scripts/sdlc-acceptance.sh +477 -0
  52. package/scripts/verify-sdlc-hook-order.mjs +26 -0
  53. package/templates/test_e2e.sh +32 -0
  54. package/templates/test_file.sh +14 -0
  55. package/templates/test_hook.sh +27 -0
  56. package/templates/test_http.sh +17 -0
  57. package/templates/test_phase.sh +23 -0
  58. package/templates/test_process.sh +14 -0
  59. package/templates/test_tool.sh +24 -0
  60. package/templates/validate_test.sh +55 -0
package/lib/paths.mjs ADDED
@@ -0,0 +1,20 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+
5
+ export function onaHome() {
6
+ const h = process.env.ONAHOME || path.join(os.homedir(), '.ona')
7
+ fs.mkdirSync(h, { recursive: true, mode: 0o700 })
8
+ return h
9
+ }
10
+
11
+ export function defaultDbPath() {
12
+ return path.join(onaHome(), 'agent.db')
13
+ }
14
+
15
+ /** §2.8 — secrets outside AGENT_SDLC_DB */
16
+ export function secureAuthPath() {
17
+ const d = path.join(onaHome(), 'secure')
18
+ fs.mkdirSync(d, { recursive: true, mode: 0o700 })
19
+ return path.join(d, 'anthropic.json')
20
+ }
@@ -0,0 +1,44 @@
1
+ /** §5.12 — permission evaluation (deny > ask > allow > defaultMode). */
2
+ export function evaluatePermission(permissions, toolName, _toolInput, phase) {
3
+ const p = permissions || { defaultMode: 'default' }
4
+ const defaultMode = p.defaultMode || 'default'
5
+
6
+ if (matchesAny(p.deny, toolName)) return 'deny'
7
+ if (matchesAny(p.ask, toolName)) return 'ask'
8
+ if (matchesAny(p.allow, toolName)) return 'allow'
9
+
10
+ switch (defaultMode) {
11
+ case 'bypassPermissions': return 'allow'
12
+ case 'dontAsk': return 'deny'
13
+ case 'acceptEdits':
14
+ return (toolName === 'Read' || toolName === 'Write' || toolName === 'Edit') ? 'allow' : 'ask'
15
+ case 'plan':
16
+ return (toolName === 'Write' || toolName === 'Edit' || toolName === 'Bash' || toolName === 'NotebookEdit') ? 'deny' : 'ask'
17
+ case 'default':
18
+ default: {
19
+ // In implement/test phases the user already approved the plan — auto-allow all execution tools
20
+ if (phase === 'implement' || phase === 'test') return 'allow'
21
+
22
+ // Read-only/safe tools auto-allowed in all other modes
23
+ const AUTO_ALLOW = new Set(['Read', 'Glob', 'Grep', 'WebFetch', 'WebSearch',
24
+ 'ToolSearch', 'ListMcpResources', 'ReadMcpResource',
25
+ 'AskUserQuestion', 'Brief', 'TodoWrite', 'TaskOutput', 'TaskStop', 'Skill'])
26
+ return AUTO_ALLOW.has(toolName) ? 'allow' : 'ask'
27
+ }
28
+ }
29
+ }
30
+
31
+ function matchesAny(rules, toolName) {
32
+ if (!Array.isArray(rules)) return false
33
+ for (const r of rules) {
34
+ if (typeof r !== 'string' || !r.trim()) continue
35
+ if (ruleMatches(r.trim(), toolName)) return true
36
+ }
37
+ return false
38
+ }
39
+
40
+ function ruleMatches(rule, toolName) {
41
+ if (rule === toolName) return true
42
+ if (rule.endsWith('*')) return toolName.startsWith(rule.slice(0, -1))
43
+ return false
44
+ }
@@ -0,0 +1,437 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ audit-code-integrity.py — Ona Platform Manual Code Integrity Auditor
4
+
5
+ Audits repository files for:
6
+ 1. Lifecycle Symmetry & Teardown Parity (Shell/Python resources)
7
+ 2. Behavioral Test Authenticity (Mock Theater Detection)
8
+ 3. Architectural Naming & Contract Alignment
9
+ 4. Error Handling & Hygiene (Swallowed Exceptions & Root Clutter)
10
+ 5. DRY Invariants & Duplicative Functions
11
+
12
+ Outputs formatted terminal reports and saves structured JSON logs to .review/<M-D-YYYY-XXX>-audit-report.json
13
+ """
14
+
15
+ import argparse
16
+ import ast
17
+ import datetime
18
+ import json
19
+ import os
20
+ import re
21
+ import sys
22
+
23
+ WORKSPACE_ROOT = os.getcwd()
24
+
25
+
26
+ class CodeIntegrityAuditor:
27
+ def __init__(self, target_path=None, verbose=False, output_dir=None):
28
+ self.target_path = os.path.abspath(target_path or WORKSPACE_ROOT)
29
+ self.verbose = verbose
30
+ base_dir = os.path.dirname(self.target_path) if os.path.isfile(self.target_path) else self.target_path
31
+ self.output_dir = output_dir or os.path.join(base_dir, ".review")
32
+ self.findings = {
33
+ "lifecycle_parity": [],
34
+ "behavioral_tests": [],
35
+ "architectural_naming": [],
36
+ "error_handling": [],
37
+ "dry_violations": [],
38
+ }
39
+ self.authenticity_stats = {
40
+ "tier_1_live": 0,
41
+ "tier_2_contract": 0,
42
+ "tier_3_theater": 0,
43
+ "total_test_functions": 0,
44
+ }
45
+
46
+ def run(self):
47
+ if os.path.isfile(self.target_path):
48
+ files_to_scan = [self.target_path]
49
+ base_dir = os.path.dirname(self.target_path)
50
+ else:
51
+ files_to_scan = self._get_files_to_scan(self.target_path)
52
+ base_dir = self.target_path
53
+
54
+ for file_path in files_to_scan:
55
+ rel_path = os.path.relpath(file_path, base_dir) if file_path.startswith(base_dir) else file_path
56
+ if file_path.endswith(".sh"):
57
+ self._audit_shell_file(file_path, rel_path)
58
+ elif file_path.endswith(".py"):
59
+ self._audit_python_file(file_path, rel_path)
60
+
61
+ self._audit_duplicative_functions(files_to_scan, base_dir)
62
+
63
+ report_path = self._save_json_report()
64
+ return report_path
65
+
66
+ def _get_files_to_scan(self, root_dir):
67
+ files = []
68
+ skip_dirs = {"node_modules", "venv", ".venv", "/tmp/"}
69
+ for dirpath, dirnames, filenames in os.walk(root_dir):
70
+ dirnames[:] = [d for d in dirnames if d not in skip_dirs and not d.startswith(".")]
71
+ for f in filenames:
72
+ if f.endswith(".sh") or f.endswith(".py"):
73
+ files.append(os.path.join(dirpath, f))
74
+ return sorted(files)
75
+
76
+ def _audit_shell_file(self, file_path, rel_path):
77
+ try:
78
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
79
+ content = f.read()
80
+ lines = content.splitlines()
81
+ except Exception:
82
+ return
83
+
84
+ has_trap = "trap " in content or "cleanup()" in content
85
+
86
+ # 1. Resource Allocations vs Teardown Parity
87
+ creations = [
88
+ ("docker buildx create", "docker buildx rm", "buildx builder creation"),
89
+ ("docker context create", "docker context rm", "docker context creation"),
90
+ ("aws ec2 run-instances", "aws ec2 terminate-instances", "EC2 instance launch"),
91
+ ("aws ec2 authorize-security-group-ingress", "aws ec2 revoke-security-group-ingress", "security group ingress rule"),
92
+ ]
93
+
94
+ for create_cmd, remove_cmd, resource_desc in creations:
95
+ if create_cmd in content:
96
+ if not has_trap or remove_cmd not in content:
97
+ self.findings["lifecycle_parity"].append({
98
+ "file": rel_path,
99
+ "severity": "FAIL",
100
+ "rule": "LIFECYCLE_ASYMMETRY",
101
+ "message": f"Script contains '{create_cmd}' ({resource_desc}) but lacks matching '{remove_cmd}' in cleanup trap.",
102
+ })
103
+
104
+ # 2. Forced Teardown Checks (--force or -f)
105
+ for idx, line in enumerate(lines, 1):
106
+ if "docker buildx rm" in line and "--force" not in line and "-f" not in line:
107
+ self.findings["lifecycle_parity"].append({
108
+ "file": rel_path,
109
+ "line": idx,
110
+ "severity": "FAIL",
111
+ "rule": "BLOCKING_TEARDOWN",
112
+ "message": "Call to 'docker buildx rm' lacks '--force' / '-f' flag, which can hang on dead SSH hosts.",
113
+ })
114
+ if "docker context rm" in line and "2>/dev/null" not in line and "|| true" not in line:
115
+ self.findings["lifecycle_parity"].append({
116
+ "file": rel_path,
117
+ "line": idx,
118
+ "severity": "WARN",
119
+ "rule": "UNGUARDED_CONTEXT_RM",
120
+ "message": "Call to 'docker context rm' lacks error guard ('2>/dev/null || true').",
121
+ })
122
+
123
+ # 3. Global Ambient State Leak Check
124
+ if "docker buildx use" in content and "docker buildx use default" not in content:
125
+ self.findings["lifecycle_parity"].append({
126
+ "file": rel_path,
127
+ "severity": "WARN",
128
+ "rule": "AMBIENT_STATE_LEAK",
129
+ "message": "Script modifies active buildx builder ('docker buildx use') without restoring default builder in cleanup.",
130
+ })
131
+
132
+ def _audit_python_file(self, file_path, rel_path):
133
+ try:
134
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
135
+ content = f.read()
136
+ tree = ast.parse(content, filename=file_path)
137
+ except Exception:
138
+ return
139
+
140
+ is_test_file = (
141
+ "test_" in rel_path
142
+ or "_test" in rel_path
143
+ or "tests/" in rel_path
144
+ or re.search(r"^\s*def test_", content, re.MULTILINE) is not None
145
+ )
146
+
147
+ # Scan AST nodes
148
+ for node in ast.walk(tree):
149
+ # 1. Behavioral Test Authenticity (Mock Theater Detection)
150
+ if is_test_file and isinstance(node, ast.FunctionDef) and node.name.startswith("test_"):
151
+ self._check_mock_theater(node, rel_path, content)
152
+
153
+ # 2. Architectural Naming Verification
154
+ if isinstance(node, ast.Assign):
155
+ for target in node.targets:
156
+ if isinstance(target, ast.Name) and "LAMBDA" in target.id.upper():
157
+ if isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
158
+ val = node.value.value
159
+ if val and not val.startswith("ona-") and not val.startswith("/ona/") and not val.startswith("/aws/lambda/"):
160
+ self.findings["architectural_naming"].append({
161
+ "file": rel_path,
162
+ "line": node.lineno,
163
+ "severity": "FAIL",
164
+ "rule": "INVALID_LAMBDA_NAME",
165
+ "message": f"Lambda function variable '{target.id}' value '{val}' violates 'ona-{{service}}-{{stage}}' naming invariant.",
166
+ })
167
+
168
+ # 3. Swallowed Error Exceptions
169
+ if isinstance(node, ast.ExceptHandler):
170
+ if len(node.body) == 1 and isinstance(node.body[0], ast.Pass):
171
+ self.findings["error_handling"].append({
172
+ "file": rel_path,
173
+ "line": node.lineno,
174
+ "severity": "WARN",
175
+ "rule": "SWALLOWED_EXCEPTION",
176
+ "message": "Exception handler silently swallows errors with bare 'pass' without logging.",
177
+ })
178
+
179
+ def _check_mock_theater(self, func_node, rel_path, content):
180
+ self.authenticity_stats["total_test_functions"] += 1
181
+ func_name = func_node.name
182
+ target_stem = func_name.replace("test_", "")
183
+
184
+ allowed_external_prefixes = (
185
+ "boto3", "botocore", "requests", "urllib", "sys", "os", "subprocess",
186
+ "pgvector", "pymysql", "redis", "psycopg2", "aiohttp", "fitz", "pdfplumber",
187
+ "builtins", "datetime", "time", "json", "math", "numpy", "pandas", "sklearn", "scipy"
188
+ )
189
+
190
+ patched_targets = []
191
+ has_substantive_assert = False
192
+ has_any_assert = False
193
+ is_internal_mocked = False
194
+ is_live_aws = False
195
+
196
+ func_body_str = ast.unparse(func_node) if hasattr(ast, "unparse") else ""
197
+ if any(term in func_body_str for term in ["boto3.client", "boto3.resource", "requests.get", "requests.post", "urllib.request", "aws lambda", "aws e2e"]):
198
+ is_live_aws = True
199
+
200
+ # Walk nodes inside test function definition
201
+ for subnode in ast.walk(func_node):
202
+ # Inspect @patch(...) or with patch(...) or mocker.patch(...)
203
+ if isinstance(subnode, ast.Call):
204
+ func_str = ast.unparse(subnode.func) if hasattr(ast, "unparse") else ""
205
+ if "patch" in func_str and subnode.args:
206
+ arg0 = subnode.args[0]
207
+ if isinstance(arg0, ast.Constant) and isinstance(arg0.value, str):
208
+ target_str = arg0.value
209
+ patched_targets.append(target_str)
210
+
211
+ # Check if internal service function is mocked
212
+ if ("services." in target_str or "app." in target_str or target_str.startswith("app")) and not any(target_str.startswith(p) for p in allowed_external_prefixes):
213
+ is_internal_mocked = True
214
+ self.findings["behavioral_tests"].append({
215
+ "file": rel_path,
216
+ "line": subnode.lineno,
217
+ "severity": "WARN",
218
+ "rule": "INTERNAL_FUNCTION_MOCKED",
219
+ "message": f"Test '{func_name}' patches internal function/module '{target_str}'. Behavioral tests must call internal service logic directly and only mock external boundaries.",
220
+ })
221
+ elif target_stem and (target_stem in target_str or target_str.endswith(f".{target_stem}")):
222
+ is_internal_mocked = True
223
+ self.findings["behavioral_tests"].append({
224
+ "file": rel_path,
225
+ "line": subnode.lineno,
226
+ "severity": "WARN",
227
+ "rule": "MOCK_THEATER_TARGET_MOCKED",
228
+ "message": f"Test '{func_name}' mocks the primary function under test ('{target_stem}'). Behavioral tests must call the actual target function directly.",
229
+ })
230
+
231
+ # Inspect Assert statements
232
+ if isinstance(subnode, ast.Assert):
233
+ has_any_assert = True
234
+ assert_str = ast.unparse(subnode.test) if hasattr(ast, "unparse") else ""
235
+ # Substantive assertions check equality, subscripting, in-comparisons, or value bounds
236
+ if any(op in assert_str for op in ["==", "!=", " in ", ">", "<", "[", "]"]):
237
+ has_substantive_assert = True
238
+
239
+ # Inspect mock assertion calls like mock.assert_called_once()
240
+ if isinstance(subnode, ast.Expr) and isinstance(subnode.value, ast.Call):
241
+ call_str = ast.unparse(subnode.value.func) if hasattr(ast, "unparse") else ""
242
+ if "assert_called" in call_str:
243
+ has_any_assert = True
244
+
245
+ # Flag tests with zero substantive outcome assertions (Assertion Theater)
246
+ if not has_substantive_assert:
247
+ self.findings["behavioral_tests"].append({
248
+ "file": rel_path,
249
+ "line": func_node.lineno,
250
+ "severity": "WARN",
251
+ "rule": "ASSERTION_THEATER",
252
+ "message": f"Test '{func_name}' has no substantive outcome value assertions (e.g. data contract keys, non-zero ranges, schema checks). Testing mocks without outcome checks is Mock Theater.",
253
+ })
254
+
255
+ # Classify Tier
256
+ if is_live_aws:
257
+ self.authenticity_stats["tier_1_live"] += 1
258
+ elif has_substantive_assert and not is_internal_mocked:
259
+ self.authenticity_stats["tier_2_contract"] += 1
260
+ else:
261
+ self.authenticity_stats["tier_3_theater"] += 1
262
+
263
+ def _audit_duplicative_functions(self, files_to_scan, base_dir):
264
+ ignored_names = {
265
+ "main", "handler", "lambda_handler", "cleanup", "__init__", "setUp", "tearDown",
266
+ "run", "execute", "cleanup_on_failure", "require_cmd", "app"
267
+ }
268
+
269
+ func_names_seen = {}
270
+ ast_hashes_seen = {}
271
+
272
+ for file_path in files_to_scan:
273
+ if not file_path.endswith(".py"):
274
+ continue
275
+
276
+ rel_path = os.path.relpath(file_path, base_dir) if file_path.startswith(base_dir) else file_path
277
+
278
+ try:
279
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
280
+ content = f.read()
281
+ tree = ast.parse(content)
282
+ except Exception:
283
+ continue
284
+
285
+ for node in ast.walk(tree):
286
+ if isinstance(node, ast.FunctionDef):
287
+ fname = node.name
288
+ if fname.startswith("_") or fname in ignored_names or fname.startswith("test_"):
289
+ continue
290
+
291
+ # 1. Duplicate Function Name Check
292
+ if fname in func_names_seen:
293
+ first_rel, first_line = func_names_seen[fname]
294
+ if first_rel != rel_path:
295
+ self.findings["dry_violations"].append({
296
+ "file": rel_path,
297
+ "line": node.lineno,
298
+ "severity": "WARN",
299
+ "rule": "DUPLICATE_FUNCTION_NAME",
300
+ "message": f"Function '{fname}' is duplicated across files (first defined in '{first_rel}:L{first_line}'). Share logic via central package import instead of repeating function definitions.",
301
+ })
302
+ else:
303
+ func_names_seen[fname] = (rel_path, node.lineno)
304
+
305
+ # 2. AST Structural Code Clone Check (>= 2 statements)
306
+ if len(node.body) >= 2:
307
+ try:
308
+ node_copy = ast.parse(ast.unparse(node)) if hasattr(ast, "unparse") else node
309
+ ast_str = ast.dump(node_copy, annotate_fields=False, include_attributes=False)
310
+ norm_ast = re.sub(r"'[a-zA-Z0-9_]+'", "'VAR'", ast_str)
311
+
312
+ if norm_ast in ast_hashes_seen:
313
+ first_name, first_rel, first_line = ast_hashes_seen[norm_ast]
314
+ if first_rel != rel_path or first_name != fname:
315
+ self.findings["dry_violations"].append({
316
+ "file": rel_path,
317
+ "line": node.lineno,
318
+ "severity": "WARN",
319
+ "rule": "DUPLICATE_FUNCTION_LOGIC",
320
+ "message": f"Function '{fname}' has identical AST code logic structure as '{first_name}' in '{first_rel}:L{first_line}'. Refactor into shared module.",
321
+ })
322
+ else:
323
+ ast_hashes_seen[norm_ast] = (fname, rel_path, node.lineno)
324
+ except Exception:
325
+ pass
326
+
327
+ def _save_json_report(self):
328
+ os.makedirs(self.output_dir, exist_ok=True)
329
+ now = datetime.datetime.now()
330
+ date_str = f"{now.month}-{now.day}-{now.year}"
331
+
332
+ # Calculate next sequence number for today
333
+ existing = [f for f in os.listdir(self.output_dir) if f.startswith(date_str) and f.endswith("-audit-report.json")]
334
+ seq = len(existing) + 1
335
+ filename = f"{date_str}-{seq:03d}-audit-report.json"
336
+ report_file = os.path.join(self.output_dir, filename)
337
+
338
+ total_fails = sum(1 for cat in self.findings.values() for item in cat if item.get("severity") == "FAIL")
339
+ total_warns = sum(1 for cat in self.findings.values() for item in cat if item.get("severity") == "WARN")
340
+
341
+ payload = {
342
+ "timestamp": now.isoformat(),
343
+ "target": self.target_path,
344
+ "summary": {
345
+ "total_failures": total_fails,
346
+ "total_warnings": total_warns,
347
+ "status": "FAIL" if total_fails > 0 else "PASS",
348
+ "authenticity_stats": self.authenticity_stats,
349
+ },
350
+ "findings": self.findings,
351
+ }
352
+
353
+ with open(report_file, "w", encoding="utf-8") as f:
354
+ json.dump(payload, f, indent=2)
355
+
356
+ return report_file
357
+
358
+
359
+ def print_formatted_summary(auditor, report_path):
360
+ print("=" * 80)
361
+ print(" ONA PLATFORM CODE INTEGRITY AUDIT REPORT ")
362
+ print("=" * 80)
363
+
364
+ # Print Authenticity Breakdown Table
365
+ stats = auditor.authenticity_stats
366
+ total_tests = stats["total_test_functions"]
367
+ if total_tests > 0:
368
+ t1_pct = (stats["tier_1_live"] / total_tests) * 100
369
+ t2_pct = (stats["tier_2_contract"] / total_tests) * 100
370
+ t3_pct = (stats["tier_3_theater"] / total_tests) * 100
371
+ print("\nTEST SUITE AUTHENTICITY CLASSIFICATION:")
372
+ print(f" Tier 1 (Live AWS / E2E Integration) : {stats['tier_1_live']:3d} tests ({t1_pct:5.1f}%) [GOLD STANDARD]")
373
+ print(f" Tier 2 (Contract-Enforced Behavioral): {stats['tier_2_contract']:3d} tests ({t2_pct:5.1f}%) [VALID LOGIC]")
374
+ print(f" Tier 3 (Mock Theater / Superficial) : {stats['tier_3_theater']:3d} tests ({t3_pct:5.1f}%) [DECEPTIVE THEATER]")
375
+ print("-" * 80)
376
+
377
+ total_fails = 0
378
+ total_warns = 0
379
+
380
+ category_titles = {
381
+ "lifecycle_parity": "Lifecycle Parity & Resource Teardown",
382
+ "behavioral_tests": "Behavioral Test Authenticity (Anti-Mock Theater)",
383
+ "architectural_naming": "Architectural Naming & Contract Invariants",
384
+ "error_handling": "Error Handling & Exception Hygiene",
385
+ "dry_violations": "DRY Invariants & Duplicative Functions",
386
+ }
387
+
388
+ for cat_key, items in auditor.findings.items():
389
+ title = category_titles.get(cat_key, cat_key)
390
+ fails = [i for i in items if i.get("severity") == "FAIL"]
391
+ warns = [i for i in items if i.get("severity") == "WARN"]
392
+ total_fails += len(fails)
393
+ total_warns += len(warns)
394
+
395
+ if not items:
396
+ print(f"[PASS] {title}")
397
+ else:
398
+ status_badge = "[FAIL]" if fails else "[WARN]"
399
+ print(f"{status_badge} {title} ({len(fails)} Failures, {len(warns)} Warnings)")
400
+ for item in items:
401
+ line_info = f":L{item['line']}" if "line" in item else ""
402
+ sev = f"[{item['severity']}]"
403
+ print(f" • {item['file']}{line_info} {sev} {item['message']}")
404
+ print()
405
+
406
+ print("=" * 80)
407
+ summary_status = "FAIL" if total_fails > 0 else "PASS"
408
+ print(f"OVERALL STATUS: {summary_status} ({total_fails} Failures, {total_warns} Warnings)")
409
+ print(f"Detailed audit log saved to: {report_path}")
410
+ print("=" * 80)
411
+ return total_fails
412
+
413
+
414
+ def main():
415
+ parser = argparse.ArgumentParser(description="Ona Platform Manual Code Integrity Auditor")
416
+ parser.add_argument("--target", default=None, help="Target file or directory to scan (defaults to repository root)")
417
+ parser.add_argument("--verbose", action="store_true", help="Print verbose output")
418
+ parser.add_argument("--format", choices=["text", "json"], default="text", help="Output format")
419
+ parser.add_argument("--output-dir", default=None, help="Directory to save JSON report (defaults to .review/)")
420
+
421
+ args = parser.parse_args()
422
+
423
+ auditor = CodeIntegrityAuditor(target_path=args.target, verbose=args.verbose, output_dir=args.output_dir)
424
+ report_path = auditor.run()
425
+
426
+ if args.format == "json":
427
+ with open(report_path, "r") as f:
428
+ print(f.read())
429
+ total_fails = sum(1 for cat in auditor.findings.values() for item in cat if item.get("severity") == "FAIL")
430
+ sys.exit(1 if total_fails > 0 else 0)
431
+
432
+ fails = print_formatted_summary(auditor, report_path)
433
+ sys.exit(1 if fails > 0 else 0)
434
+
435
+
436
+ if __name__ == "__main__":
437
+ main()
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env bash
2
+ # ensure-laya-env.sh — REQUIRED provisioning for System-1 (Laya).
3
+ # Creates ~/.nehanda/venv and installs laya + torch. Exits non-zero on failure.
4
+ set -euo pipefail
5
+
6
+ NEHANDA_HOME="${NEHANDA_HOME:-$HOME/.nehanda}"
7
+ VENV_DIR="${NEHANDA_HOME}/venv"
8
+ PY="${VENV_DIR}/bin/python"
9
+ PIP="${VENV_DIR}/bin/pip"
10
+ MARKER="${NEHANDA_HOME}/.laya-ready"
11
+
12
+ mkdir -p "$NEHANDA_HOME"
13
+
14
+ if [[ ! -x "$PY" ]]; then
15
+ echo "[laya-env] creating venv at $VENV_DIR" >&2
16
+ python3 -m venv "$VENV_DIR"
17
+ fi
18
+
19
+ # shellcheck disable=SC1091
20
+ source "${VENV_DIR}/bin/activate"
21
+
22
+ "$PIP" install --upgrade pip --quiet
23
+
24
+ need_install=0
25
+ if ! "$PY" -c "import laya, torch" 2>/dev/null; then
26
+ need_install=1
27
+ fi
28
+
29
+ if [[ "$need_install" -eq 1 ]]; then
30
+ echo "[laya-env] installing laya + torch (~808 MB download — this is a one-time setup, please wait)" >&2
31
+ "$PIP" install 'laya' 'torch' --progress-bar on
32
+ fi
33
+
34
+ # Hard verify imports
35
+ "$PY" - <<'PY'
36
+ import sys
37
+ import laya
38
+ import torch
39
+ print(f"[laya-env] laya={getattr(laya, '__version__', 'ok')} torch={torch.__version__}", file=sys.stderr)
40
+ if sys.platform == "darwin":
41
+ if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
42
+ print("[laya-env] MPS available", file=sys.stderr)
43
+ else:
44
+ print("[laya-env] WARN: MPS not available; Laya will use CPU (slower)", file=sys.stderr)
45
+ PY
46
+
47
+ # Smoke: Router constructible (model weights download on first predict/load)
48
+ "$PY" - <<'PY'
49
+ from laya import Router
50
+ Router()
51
+ print("[laya-env] Router() constructible", file=sys.stderr)
52
+ PY
53
+
54
+ date -u +"%Y-%m-%dT%H:%M:%SZ" > "$MARKER"
55
+ echo "$PY"