#!/usr/bin/env python3 from __future__ import annotations import csv import hashlib import json import py_compile import re import shutil import sys import xml.etree.ElementTree as ET from pathlib import Path ROOT = Path(__file__).resolve().parents[1] ERRORS: list[str] = [] WARNINGS: list[str] = [] CHECKS: list[str] = [] def ok(message: str) -> None: CHECKS.append(message) def fail(message: str) -> None: ERRORS.append(message) def warn(message: str) -> None: WARNINGS.append(message) def active_files(pattern: str): for path in ROOT.rglob(pattern): rel = path.relative_to(ROOT) if rel.parts[:1] == ('attachments',): continue if rel.parts[:2] == ('research', 'original'): continue if any(part in {'bin', 'obj', 'node_modules', '__pycache__', '.git'} for part in rel.parts): continue yield path required = [ 'README.md', 'AGENTS.md', 'global.json', 'KArtSell.sln', 'docs/v12_1/00_EXECUTIVE_EXECUTION_PROPOSAL.md', 'docs/v12_1/01_SOURCE_COVERAGE_MATRIX.csv', 'docs/v12_1/06_SLICE_CATALOGUE.csv', 'docs/v12_1/07_DETAILED_WBS_MASTER.csv', 'docs/v12_1/08_TECH_DEBT_REGISTER.csv', 'docs/v12_1/09_TRACEABILITY_MATRIX.csv', 'docs/v12_1/10_DECISION_LOG.csv', 'db/migrations/0013_execution_readiness_building_blocks.sql', 'research/hardening/test_policy_contract.py', ] for name in required: if not (ROOT/name).exists(): fail(f'missing required file: {name}') if not ERRORS: ok(f'required files present: {len(required)}') # All current-session attachments must be byte-for-byte represented by source coverage. coverage_path = ROOT/'docs/v12_1/01_SOURCE_COVERAGE_MATRIX.csv' coverage: dict[str, str] = {} if coverage_path.exists(): with coverage_path.open(encoding='utf-8-sig', newline='') as f: for row in csv.DictReader(f): coverage[row['Attachment']] = row['SHA256'] attachment_dir = ROOT/'attachments/current_session' for path in sorted(attachment_dir.glob('*')): if not path.is_file(): continue actual=hashlib.sha256(path.read_bytes()).hexdigest() if coverage.get(path.name) != actual: fail(f'attachment coverage/hash mismatch: {path.name}') if len(coverage) != len([p for p in attachment_dir.glob('*') if p.is_file()]): fail('source coverage row count differs from current-session attachment count') else: ok(f'attachment preservation verified: {len(coverage)} files') for path in active_files('*.json'): try: json.loads(path.read_text(encoding='utf-8')) except Exception as exc: fail(f'invalid JSON {path.relative_to(ROOT)}: {exc}') ok('JSON parse complete') for path in [*active_files('*.csproj'), *active_files('*.props')]: try: ET.parse(path) except Exception as exc: fail(f'invalid XML {path.relative_to(ROOT)}: {exc}') ok('MSBuild XML parse complete') for path in active_files('*.csv'): try: with path.open(encoding='utf-8-sig', newline='') as f: rows=list(csv.reader(f)) if not rows: fail(f'empty CSV {path.relative_to(ROOT)}'); continue width=len(rows[0]) bad=[i+1 for i,row in enumerate(rows) if len(row)!=width] if bad: fail(f'ragged CSV {path.relative_to(ROOT)} rows {bad[:10]}') except Exception as exc: fail(f'invalid CSV {path.relative_to(ROOT)}: {exc}') ok('CSV structure complete') # WBS structural and traceability checks. wbs_path=ROOT/'docs/v12_1/07_DETAILED_WBS_MASTER.csv' if wbs_path.exists(): with wbs_path.open(encoding='utf-8-sig', newline='') as f: wbs=list(csv.DictReader(f)) ids=[r['WBS_ID'] for r in wbs] if len(ids)!=len(set(ids)): fail('duplicate WBS_ID') if len(wbs)<180: fail(f'detailed WBS is unexpectedly small: {len(wbs)}') slice_ids={r['Slice_ID'] for r in wbs if r['Slice_ID'].startswith('VS-')} expected={f'VS-{i:02d}' for i in range(26)} if slice_ids!=expected: fail(f'WBS slice coverage mismatch: missing={sorted(expected-slice_ids)} extra={sorted(slice_ids-expected)}') required_columns={'Requirement_ID','API_ID','DB_Migration_ID','Job_Event_ID','UI_ID','Test_ID','Acceptance_Evidence','Primary_Owner','Dependency','Gate','Evidence_Class'} if not required_columns.issubset(wbs[0]): fail('WBS required columns missing') if any(not r['Acceptance_Evidence'].strip() or not r['Primary_Owner'].strip() for r in wbs): fail('WBS has blank acceptance or owner') ok(f'WBS uniqueness/coverage complete: {len(wbs)} tasks, 26 slices') for path in active_files('*.py'): try: py_compile.compile(str(path), doraise=True) except Exception as exc: fail(f'Python compile failed {path.relative_to(ROOT)}: {exc}') ok('Python compile complete') for path in active_files('*.yaml'): text=path.read_text(encoding='utf-8') if '\t' in text: fail(f'YAML contains tab indentation: {path.relative_to(ROOT)}') if not text.strip(): fail(f'empty YAML: {path.relative_to(ROOT)}') ok('YAML basic checks complete') source_files=[p for p in [*active_files('*.cs'),*active_files('*.sql')] if 'tests' not in p.relative_to(ROOT).parts] patterns={ 'AllowAnonymous()':'anonymous module endpoint', 'IGenericRepository':'generic repository', 'DateTime.Now':'direct wall-clock use', 'DateTime.UtcNow':'direct wall-clock use', 'dynamic ':'dynamic typing in production C#', } for token,label in patterns.items(): hits=[str(p.relative_to(ROOT)) for p in source_files if token in p.read_text(encoding='utf-8')] if hits: fail(f'{label} pattern {token}: {hits}') select_star=[] for path in source_files: if re.search(r'\bselect\s+\*',path.read_text(encoding='utf-8'),flags=re.I): select_star.append(str(path.relative_to(ROOT))) if select_star: fail(f'SELECT * prohibited: {select_star}') ok('prohibited source pattern scan complete') for path in active_files('Endpoint.cs'): text=path.read_text(encoding='utf-8') if 'Roles(' not in text and 'Policies(' not in text: fail(f'endpoint lacks Roles/Policies: {path.relative_to(ROOT)}') ok('endpoint authorization declarations complete') for path in active_files('*.cs'): rel=path.relative_to(ROOT) if 'Domain' in rel.parts: text=path.read_text(encoding='utf-8') for token in ['using Dapper','using Npgsql','using FastEndpoints','using Hangfire','HttpContext','DbConnection']: if token in text: fail(f'domain framework dependency {token}: {rel}') ok('domain dependency scan complete') # Lightweight brace balance catches truncated generated source without pretending to compile C#. for path in active_files('*.cs'): text=path.read_text(encoding='utf-8') # Remove raw/normal string bodies sufficiently for a structural smoke check. scrub=re.sub(r'""".*?"""','',text,flags=re.S) scrub=re.sub(r'"(?:\\.|[^"\\])*"','',scrub) if scrub.count('{') != scrub.count('}'): fail(f'C# brace imbalance: {path.relative_to(ROOT)}') ok('C# structural smoke complete') migration_names=sorted(p.name for p in (ROOT/'db/migrations').glob('*.sql')) if not all(re.match(r'^\d{4}_[a-z0-9_]+\.sql$',n) for n in migration_names): fail('migration filename format must be NNNN_snake_case.sql') if len(migration_names)!=len(set(migration_names)): fail('duplicate migration filename') nums=[int(n[:4]) for n in migration_names] if nums != sorted(nums): fail('migration sequence is not ordered') ok(f'migration ordering complete: {len(migration_names)} scripts') for path in active_files('*'): if path.is_file() and path.name.lower() in {'testfile','temp','tmp'}: fail(f'placeholder file: {path.relative_to(ROOT)}') ok('placeholder scan complete') # Safety capability must be fail-closed in code and configuration. program=(ROOT/'src/KArtSell.Host/Program.cs').read_text(encoding='utf-8') for marker in ['!x.AutomaticOrder','!x.KisOrderAdapter','ValidateOnStart']: if marker not in program: fail(f'missing startup capability guard: {marker}') for cfg in [ROOT/'src/KArtSell.Host/appsettings.json',ROOT/'src/KArtSell.Host/appsettings.Development.json']: if cfg.exists(): data=json.loads(cfg.read_text(encoding='utf-8')) caps=data.get('Capabilities') if caps is not None and (caps.get('AutomaticOrder') is not False or caps.get('KisOrderAdapter') is not False): fail(f'order capability is not OFF: {cfg.relative_to(ROOT)}') ok('automatic-order and KIS capability guards complete') if not (ROOT/'frontend/pnpm-lock.yaml').exists(): warn('frontend/pnpm-lock.yaml missing; G0 must generate, review and commit it') if shutil.which('dotnet') is None: warn('dotnet SDK unavailable; restore/build/test not executed') if shutil.which('pnpm') is None: warn('pnpm unavailable; frozen install/typecheck/test/build not executed') if shutil.which('psql') is None: warn('PostgreSQL client unavailable; migration rehearsal not executed') print('K-ArtSell Aegis v12.1 static validation') print(f'root={ROOT}') for m in CHECKS: print(f'PASS: {m}') for m in WARNINGS: print(f'WARN: {m}') for m in ERRORS: print(f'FAIL: {m}') print(f'summary: pass={len(CHECKS)} warn={len(WARNINGS)} fail={len(ERRORS)}') sys.exit(1 if ERRORS else 0)