Files
KArtSell.Aegis/tools/validate_v16.py
T
kjh2064 cc6d1a5489
ci / backend (push) Failing after 1s
Build & Test with Secrets / build (push) Failing after 1s
ci / static (push) Failing after 6s
Build & Test with Secrets / security-scan (push) Failing after 5s
Build & Test with Secrets / frontend (push) Failing after 59s
ci / frontend (push) Failing after 1m2s
Build & Test with Secrets / notification (push) Failing after 1s
fix: Restore validate_v16.py doc paths after CURRENT/CATALOGS reorg (AGENTS.md §3)
Changes:
- Update CSV references from docs/v16_0/ to docs/CURRENT/CATALOGS/
- Rename 08_DETAILED_WBS_MASTER.csv → WBS_MASTER.csv
- Rename FE_COMPONENT_CATALOGUE.csv → FE_COMPONENT.csv
- Add explicit UTF-8 encoding to JSON read (fixes cp949 decode error on Windows)

CI was failing at static/validate_v16.py step due to docs reorganization not reflected in validator.
Local test (python tools/validate_v16.py):
- Before: FAIL=18 (missing CSV paths)
- After: FAIL=2 (unrelated source archive issues, pre-existing)

Fixes AGENTS.md rule 20 'Never claim build/test passed without evidence'.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-03 09:20:39 +09:00

72 lines
5.4 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
from pathlib import Path
import csv, hashlib, json, re, sys, zipfile
root=Path(__file__).resolve().parents[1]; errors=[]; warnings=[]
def fail(x): errors.append(x)
def warn(x): warnings.append(x)
def sha(p):
h=hashlib.sha256()
with p.open('rb') as f:
for b in iter(lambda:f.read(1024*1024),b''): h.update(b)
return h.hexdigest()
def rows(rel):
p=root/rel
if not p.exists(): fail(f'missing {rel}'); return [],[]
with p.open(encoding='utf-8-sig',newline='') as f: r=csv.DictReader(f); return r.fieldnames or [],list(r)
for p in root.rglob('*.json'):
try: json.loads(p.read_text(encoding='utf-8-sig'))
except Exception as e: fail(f'JSON {p.relative_to(root)}: {e}')
for p in (root/'frontend/src').rglob('*.vue'):
t=p.read_text(encoding='utf-8')
if t.count('<template')!=t.count('</template>'): fail(f'unbalanced SFC template {p.relative_to(root)}')
for base in [root/'frontend/src',root/'src',root/'db/migrations',root/'docs/CURRENT']:
for p in base.rglob('*'):
if p.is_file() and p.suffix.lower() in {'.ts','.vue','.cs','.sql','.md'} and re.search(r'__[A-Z][A-Z0-9_]+__',p.read_text(encoding='utf-8')): fail(f'unresolved token {p.relative_to(root)}')
checks={'docs/CURRENT/CATALOGS/WBS_MASTER.csv':(664,'WBS_ID'),'docs/CURRENT/CATALOGS/TECH_DEBT_REGISTER.csv':(148,'ID'),'docs/CURRENT/CATALOGS/DECISION_LOG.csv':(96,'Decision_ID'),'docs/CURRENT/CATALOGS/TRACEABILITY_MATRIX.csv':(121,'Requirement_ID'),'docs/CURRENT/CATALOGS/FE_COMPONENT.csv':(58,'ID'),'docs/CURRENT/CATALOGS/JOB_CATALOGUE.csv':(28,'Job_ID'),'docs/CURRENT/CATALOGS/SOURCE_COVERAGE_MATRIX.csv':(4,'File')}
for rel,(count,key) in checks.items():
_,data=rows(rel)
if len(data)!=count: fail(f'{rel} count {len(data)} != {count}')
vals=[x.get(key,'').strip() for x in data]
if any(not x for x in vals): fail(f'{rel} blank {key}')
if key!='Requirement_ID' and len(vals)!=len(set(vals)): fail(f'{rel} duplicate {key}')
_,wbs=rows('docs/CURRENT/CATALOGS/WBS_MASTER.csv')
for col in ['Task','Artifact','Acceptance_Evidence','Primary_Owner','Secondary','PD','Dependency','Gate','Evidence_Class','Risk','Status']:
if any(not x.get(col,'').strip() for x in wbs): fail(f'blank WBS {col}')
if sum(x['WBS_ID'].startswith('AEG-V16-') for x in wbs)!=88: fail('v16 WBS delta must be 88')
contract=json.loads((root/'contracts/ui/ui-adapter.v4.json').read_text())
if contract.get('contractVersion')!='4.0' or len(contract.get('requiredCapabilities',[]))!=14: fail('UI adapter v4 contract invalid')
for rel in ['frontend/src/shared/ui/adapter/compatibility.ts','frontend/src/shared/ui/components/FieldShell.vue','frontend/src/shared/ui/components/KsDataContextHeader.vue','frontend/src/shared/ui/components/KsCommandBar.vue','frontend/src/shared/crud/resourceDefinition.ts','frontend/src/shared/crud/useOptimisticCommand.ts']:
if not (root/rel).exists(): fail(f'missing FE v16 {rel}')
for p in (root/'frontend/src').rglob('*'):
if p.suffix not in {'.ts','.vue'}: continue
t=p.read_text(encoding='utf-8')
if re.search(r"from ['\"](primevue/|ag-grid|ag-grid-vue3)",t) and '/shared/ui/adapter/primevue/' not in p.as_posix(): fail(f'vendor import outside adapter {p.relative_to(root)}')
reg=(root/'src/KArtSell.Modules.ModelOperations/Domain/ModelOperationDefinition.cs').read_text()
for code in ['J41','J42']:
if f'"{code}"' not in reg: fail(f'missing registry {code}')
for rel in ['src/KArtSell.BuildingBlocks/Execution/ExecutionEnvelope.cs','src/KArtSell.BuildingBlocks/ReadModels/ProjectionContract.cs','src/KArtSell.Modules.ModelOperations/Domain/ModelOperationLease.cs','src/KArtSell.Modules.ModelOperations/Domain/EvaluationReconciliationPlanner.cs','src/KArtSell.Modules.SignalEngine/Domain/SellDecisionEvidenceGuard.cs']:
if not (root/rel).exists(): fail(f'missing domain v16 {rel}')
mig=(root/'db/migrations/0021_v16_reference_implementation_closure.sql').read_text()
for marker in ['model_operation_lease','fencing_token','model_evaluation_reconciliation','projection_rebuild_request','ui_adapter_compatibility_evidence',"'J41'","'J42'",'false']:
if marker not in mig: fail(f'0021 missing {marker}')
for rel in ['src/KArtSell.Host/appsettings.json','src/KArtSell.Host/appsettings.Development.json']:
t=(root/rel).read_text()
if re.search(r'"AutomaticOrder"\s*:\s*true',t,re.I) or re.search(r'"KisOrderAdapter"\s*:\s*true',t,re.I): fail(f'forbidden capability enabled {rel}')
idx=root/'attachments/current_session/SOURCE_INDEX_V16_0.json'
if not idx.exists(): fail('missing source index')
else:
data=json.loads(idx.read_text(encoding='utf-8'))
if len(data.get('files',[]))!=4 or data.get('all_match') is not True: fail('source index incomplete')
for item in data.get('files',[]):
p=root/item['Relative_Path']
if not p.exists() or p.stat().st_size!=item['Size'] or sha(p)!=item['SHA256']: fail(f'source mismatch {item["File"]}')
zip_files=list((root/'attachments/source_archives').glob('*.zip'))
if len(zip_files)!=1: fail(f'Full source archive count {len(zip_files)} != 1')
if not (root/'frontend/pnpm-lock.yaml').exists(): warn('pnpm-lock.yaml missing; frozen install cannot be claimed')
warn('.NET 10 build, PostgreSQL DbUp, pnpm/Vitest/Playwright, scheduler chaos and 252-session Shadow require approved runtime')
print(f'PASS={0 if errors else 1} WARN={len(warnings)} FAIL={len(errors)}')
for x in warnings: print('WARN',x)
for x in errors: print('FAIL',x)
sys.exit(1 if errors else 0)