203 lines
10 KiB
Python
203 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import csv
|
|
import hashlib
|
|
import json
|
|
import py_compile
|
|
import re
|
|
import sys
|
|
import xml.etree.ElementTree as ET
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
ERRORS: list[str] = []
|
|
WARNINGS: list[str] = []
|
|
PASSES: list[str] = []
|
|
|
|
|
|
def ok(message: str) -> None: PASSES.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', 'RELEASE_NOTES_V12_5.md', 'SOURCE_BASIS_V12_5.md',
|
|
'docs/v12_5/00_EXECUTIVE_CRITICAL_REVIEW.md',
|
|
'docs/v12_5/01_SOURCE_COVERAGE_MATRIX.csv',
|
|
'docs/v12_5/02_ROLE_BASED_BRUTAL_AUDIT.md',
|
|
'docs/v12_5/03_TARGET_ARCHITECTURE_SOLID_REFACTORING.md',
|
|
'docs/v12_5/04_ALGORITHM_MODEL_EVALUATION.md',
|
|
'docs/v12_5/05_DATA_DB_INTEGRITY.md',
|
|
'docs/v12_5/06_BE_FE_HANGFIRE_IMPLEMENTATION.md',
|
|
'docs/v12_5/07_QA_SECURITY_OPERATIONS.md',
|
|
'docs/v12_5/08_PROCESS_SIMPLIFICATION_VIBE_CODING.md',
|
|
'docs/v12_5/09_ROADMAP_20D_90D_36M.md',
|
|
'docs/v12_5/10_DETAILED_WBS_MASTER.csv',
|
|
'docs/v12_5/11_TECH_DEBT_REGISTER.csv',
|
|
'docs/v12_5/12_TRACEABILITY_MATRIX.csv',
|
|
'docs/v12_5/13_DECISION_LOG.csv',
|
|
'docs/v12_5/14_PACKAGE_CONTENTS.md',
|
|
'docs/v12_5/15_JOB_CATALOGUE.csv',
|
|
'docs/v12_5/16_RACI_MATRIX.csv',
|
|
'docs/v12_5/17_RISK_REGISTER.csv',
|
|
'docs/v12_5/18_VALIDATION_MATRIX.csv',
|
|
'docs/v12_5/K-ArtSell_Aegis_v12_5_실행보증_통합고도화_제안서.docx',
|
|
'db/migrations/0017_execution_assurance.sql',
|
|
'contracts/schedules/execution-assurance.v1.json',
|
|
'contracts/model-governance/evidence-classification.v1.json',
|
|
'src/KArtSell.Modules.ModelOperations/Domain/ModelOperationExecutionBoundary.cs',
|
|
'tests/KArtSell.ModelOperations.UnitTests/ModelOperationExecutionBoundaryTests.cs',
|
|
]
|
|
for name in required:
|
|
if not (ROOT / name).exists(): fail(f'missing required file: {name}')
|
|
if not ERRORS: ok(f'required v12.5 files present: {len(required)}')
|
|
|
|
# Current request direct attachment preservation.
|
|
coverage_path = ROOT / 'docs/v12_5/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_request_20260802'
|
|
attachments = [p for p in sorted(attachment_dir.iterdir()) if p.is_file()]
|
|
if len(attachments) != 3:
|
|
fail(f'expected exactly three direct current-request attachments, found {len(attachments)}')
|
|
for path in attachments:
|
|
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(attachments): fail('source coverage row count mismatch')
|
|
if not ERRORS: ok('three direct attachments preserved byte-for-byte')
|
|
|
|
# Parse structured 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 MSBuild 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')
|
|
|
|
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')
|
|
|
|
# WBS/debt/trace dimensions.
|
|
def read_dicts(rel: str):
|
|
with (ROOT / rel).open(encoding='utf-8-sig', newline='') as f: return list(csv.DictReader(f))
|
|
|
|
wbs = read_dicts('docs/v12_5/10_DETAILED_WBS_MASTER.csv') if (ROOT/'docs/v12_5/10_DETAILED_WBS_MASTER.csv').exists() else []
|
|
if len(wbs) != 336: fail(f'v12.5 WBS must contain 336 rows, found {len(wbs)}')
|
|
ids = [r.get('WBS_ID','') for r in wbs]
|
|
if len(ids) != len(set(ids)): fail('duplicate WBS_ID')
|
|
if sum(1 for r in wbs if r.get('WBS_ID','').startswith('AEG-V12-5-')) != 48: fail('v12.5 WBS delta must contain 48 rows')
|
|
if any(not r.get('Acceptance_Evidence','').strip() or not r.get('Primary_Owner','').strip() for r in wbs): fail('WBS blank acceptance/owner')
|
|
ok('WBS 336 rows, unique IDs, 48-row delta')
|
|
|
|
debt = read_dicts('docs/v12_5/11_TECH_DEBT_REGISTER.csv') if (ROOT/'docs/v12_5/11_TECH_DEBT_REGISTER.csv').exists() else []
|
|
if len(debt) != 68: fail(f'tech debt must contain 68 rows, found {len(debt)}')
|
|
if len({r.get('ID') for r in debt}) != len(debt): fail('duplicate tech debt ID')
|
|
ok('technical debt register 68 rows')
|
|
|
|
trace = read_dicts('docs/v12_5/12_TRACEABILITY_MATRIX.csv') if (ROOT/'docs/v12_5/12_TRACEABILITY_MATRIX.csv').exists() else []
|
|
for i in range(1, 13):
|
|
if not any(r.get('Requirement_ID') == f'REQ-EXA-{i:03d}' for r in trace): fail(f'missing trace requirement REQ-EXA-{i:03d}')
|
|
ok('execution-assurance trace requirements complete')
|
|
|
|
# Fail-closed configuration and code markers.
|
|
settings = json.loads((ROOT/'src/KArtSell.Host/appsettings.json').read_text(encoding='utf-8'))
|
|
if settings.get('ModelOperations', {}).get('DispatcherEnabled') is not False: fail('ModelOperations dispatcher must default OFF')
|
|
program = (ROOT/'src/KArtSell.Host/Program.cs').read_text(encoding='utf-8')
|
|
for marker in ['modelOperationsDispatcherEnabled', '!modelOperationsDispatcherEnabled || x.ShadowEvaluation',
|
|
'RegisterModelOperationsSchedules(modelOperationsDispatcherEnabled', '!x.AutomaticOrder', '!x.KisOrderAdapter']:
|
|
if marker not in program: fail(f'missing fail-closed marker: {marker}')
|
|
scheduler = (ROOT/'src/KArtSell.Modules.ModelOperations/Scheduling/ModelOperationsScheduler.cs').read_text(encoding='utf-8')
|
|
for marker in ['if (!dispatcherEnabled)', 'RemoveIfExists', 'DispatcherJobId']:
|
|
if marker not in scheduler: fail(f'missing scheduler safety marker: {marker}')
|
|
contract = (ROOT/'contracts/schedules/execution-assurance.v1.json')
|
|
if contract.exists():
|
|
c = json.loads(contract.read_text(encoding='utf-8'))
|
|
if c.get('dispatcher', {}).get('enabledByDefault') is not False: fail('schedule contract must default dispatcher OFF')
|
|
if len(c.get('operations', [])) != 6: fail('execution-assurance contract must contain J25-J30')
|
|
ok('fail-closed model-operations boundary complete')
|
|
|
|
registry = (ROOT/'src/KArtSell.Modules.ModelOperations/Domain/ModelOperationDefinition.cs').read_text(encoding='utf-8')
|
|
codes = re.findall(r'new\("(J\d+)"', registry)
|
|
if len(codes) != 16 or len(set(codes)) != 16: fail(f'model operation registry expected 16 unique operations, found {len(codes)}/{len(set(codes))}')
|
|
for code in [f'J{i}' for i in range(25,31)]:
|
|
if code not in codes: fail(f'missing registry operation {code}')
|
|
ok('model operation registry J10-J30 approved set complete')
|
|
|
|
migration = (ROOT/'db/migrations/0017_execution_assurance.sql').read_text(encoding='utf-8')
|
|
for marker in ['V12_5_REAPPROVAL_REQUIRED', 'model_operation_artifact', 'release_evidence_bundle',
|
|
'source_contract_snapshot', 'execution_assurance_decision', "'J30'"]:
|
|
if marker not in migration: fail(f'migration 0017 missing marker: {marker}')
|
|
ok('migration 0017 execution-assurance structures complete')
|
|
|
|
# Prohibited patterns in active source.
|
|
source_files = [p for p in [*active_files('*.cs'), *active_files('*.sql')] if 'tests' not in p.relative_to(ROOT).parts]
|
|
patterns = {'AllowAnonymous()':'anonymous endpoint','IGenericRepository':'generic repository','DateTime.Now':'direct wall clock','DateTime.UtcNow':'direct wall clock'}
|
|
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}: {token} in {hits}')
|
|
select_star = [str(p.relative_to(ROOT)) for p in source_files if re.search(r'\bselect\s+\*', p.read_text(encoding='utf-8'), flags=re.I)]
|
|
if select_star: fail(f'SELECT * prohibited: {select_star}')
|
|
ok('prohibited-pattern scan complete')
|
|
|
|
# Domain framework dependency scan and C# structural smoke.
|
|
for path in active_files('*.cs'):
|
|
rel = path.relative_to(ROOT)
|
|
text = path.read_text(encoding='utf-8')
|
|
if 'Domain' in rel.parts:
|
|
for token in ['using Dapper', 'using Npgsql', 'using FastEndpoints', 'using Hangfire', 'HttpContext', 'DbConnection']:
|
|
if token in text: fail(f'domain framework dependency {token}: {rel}')
|
|
scrub = re.sub(r'""".*?"""', '', text, flags=re.S)
|
|
scrub = re.sub(r'"(?:\\.|[^"\\])*"', '', scrub)
|
|
if scrub.count('{') != scrub.count('}'): fail(f'C# brace imbalance: {rel}')
|
|
ok('domain dependency and C# structural smoke complete')
|
|
|
|
# Package status honesty.
|
|
claims = []
|
|
for path in [ROOT/'README.md', ROOT/'RELEASE_NOTES_V12_5.md', ROOT/'docs/v12_5/00_EXECUTIVE_CRITICAL_REVIEW.md']:
|
|
text = path.read_text(encoding='utf-8')
|
|
for forbidden in ['PRODUCTION_READY', 'BUILD_VALIDATED / DB_REHEARSED', 'SHADOW_VALIDATED']:
|
|
if forbidden in text: claims.append(f'{path.name}:{forbidden}')
|
|
if claims: fail(f'unsupported readiness claim: {claims}')
|
|
else: ok('readiness claims remain evidence-bounded')
|
|
|
|
# Toolchain warnings are expected in this container/package.
|
|
if not (ROOT/'frontend/pnpm-lock.yaml').exists(): warn('pnpm-lock.yaml is still absent; frozen frontend validation remains blocked')
|
|
warn('.NET 10 restore/build/test is not executed by static validator')
|
|
warn('PostgreSQL DbUp fresh/upgrade/re-run/failure rehearsal is not executed by static validator')
|
|
warn('252-trading-day Shadow, PBO/DSR, security and DR evidence remain unmet')
|
|
|
|
print(f'PASS={len(PASSES)} WARN={len(WARNINGS)} FAIL={len(ERRORS)}')
|
|
for m in PASSES: print(f'PASS: {m}')
|
|
for m in WARNINGS: print(f'WARN: {m}')
|
|
for m in ERRORS: print(f'FAIL: {m}')
|
|
sys.exit(1 if ERRORS else 0)
|