40-validation/validate-gap-mvp-completion.py
#!/usr/bin/env python3
"""Validate GAP MVP completion and maintenance handover."""
from __future__ import annotations
import argparse, json, os, re
from pathlib import Path
from evidence_lifecycle import seal_evidence, validate_evidence
VALIDATOR_REF="engineering-tools:40-validation/validate-gap-mvp-completion.py"
DOC_REF="engineering-tools:40-validation/GAP_MVP_COMPLETION_MAINTENANCE_HANDOVER.md"
ROADMAP_REF="gap-000:20-runtime-state/roadmap/ROADMAP.md"
GAP_MANIFEST_REF="gap-000:manifest.json"
DISCOVERY_EVIDENCE="runtime:test/reports/validation/mandatory-discovery-closure.json"
LANDSCAPE_EVIDENCE="runtime:test/reports/validation/full-repository-landscape.json"
def read_json(path:Path)->dict:
value=json.loads(path.read_text(encoding="utf-8"))
if not isinstance(value,dict): raise ValueError(f"JSON object required: {path}")
return value
def atomic_json(path:Path,data:dict)->None:
path.parent.mkdir(parents=True,exist_ok=True)
tmp=path.with_suffix(path.suffix+'.tmp')
tmp.write_text(json.dumps(data,indent=2,ensure_ascii=False)+'\n',encoding='utf-8')
os.replace(tmp,path)
def parse_roadmap(path:Path):
text=path.read_text(encoding='utf-8')
items=[]
for m in re.finditer(r'^\|\s*(KT-\d{4})\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|$',text,re.M):
items.append({'id':m.group(1),'title':m.group(2).strip(),'status':m.group(3).strip()})
phases=[]
for m in re.finditer(r'^## (Phase \d+[^\n]*)[\s\S]*?\*\*Status:\*\*\s*([^\n]+)',text,re.M):
phases.append({'phase':m.group(1).strip(),'statusLine':m.group(2).strip()})
optional=[]
start=text.find('## Optionaler Backlog')
if start>=0:
for m in re.finditer(r'^\|\s*(OPT-[^|]+?)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|$',text[start:],re.M):
if m.group(1).strip()!='ID': optional.append({'id':m.group(1).strip(),'classification':m.group(3).strip(),'roadmapImpact':m.group(4).strip(),'status':m.group(5).strip()})
return text,items,phases,optional
def evaluate(gap_root:Path,runtime_root:Path,roots_container:Path)->tuple[dict,list[str]]:
errors=[]
manifest=read_json(gap_root/'manifest.json')
text,items,phases,optional=parse_roadmap(gap_root/'20-runtime-state/roadmap/ROADMAP.md')
roadmap=manifest.get('roadmap',{})
if len(items)!=24: errors.append(f'expected 24 mandatory work items, found {len(items)}')
incomplete=[x['id'] for x in items if x['status']!='complete']
if incomplete: errors.append('incomplete mandatory work items: '+', '.join(incomplete))
if len(phases)!=5: errors.append(f'expected 5 phases, found {len(phases)}')
bad_phases=[x['phase'] for x in phases if not x['statusLine'].startswith('complete')]
if bad_phases: errors.append('incomplete phases: '+', '.join(bad_phases))
expected={'mandatoryTotal':24,'mandatoryCompleted':24,'mandatoryRemaining':0,'progressPercent':100,'nextMandatoryWorkItem':None}
for k,v in expected.items():
if roadmap.get(k)!=v: errors.append(f'roadmap {k} expected {v!r}, got {roadmap.get(k)!r}')
if manifest.get('stateMachine',{}).get('currentState')!='MAINTENANCE_READY': errors.append('GAP state is not MAINTENANCE_READY')
if manifest.get('activeWorkItem') is not None: errors.append('activeWorkItem must be null after MVP completion')
if manifest.get('runtimeState',{}).get('productiveKnowledgeTransfers',{}).get('active') is not None: errors.append('productive active work item must be null')
for entry in optional:
if entry.get('roadmapImpact')!='none': errors.append(f"optional backlog changes mandatory scope: {entry['id']}")
evidence_checks=[]
for name,rel in [('discovery', 'test/reports/validation/mandatory-discovery-closure.json'),('landscape','test/reports/validation/full-repository-landscape.json')]:
p=runtime_root/rel
if not p.is_file(): errors.append(f'missing {name} evidence'); continue
d=read_json(p)
if d.get('status')!='PASSED' or d.get('freshness')!='fresh': errors.append(f'{name} evidence not PASSED/fresh')
stale=validate_evidence(roots_container,p)
if stale: errors.extend(f'{name} evidence: {x}' for x in stale)
evidence_checks.append({'name':name,'status':d.get('status'),'freshness':d.get('freshness'),'provenanceErrors':stale})
result={'validationId':'gap-mvp-completion-maintenance-handover','status':'PASSED' if not errors else 'FAILED','baseline':roadmap.get('baseline'),'baselineVersion':str(roadmap.get('baselineVersion')),'mandatoryTotal':len(items),'mandatoryCompleted':sum(x['status']=='complete' for x in items),'mandatoryRemaining':sum(x['status']!='complete' for x in items),'progressPercent':100 if items and all(x['status']=='complete' for x in items) else roadmap.get('progressPercent'),'phasesTotal':len(phases),'phasesComplete':sum(x['statusLine'].startswith('complete') for x in phases),'completionBlockers':errors,'optionalBacklogCount':len(optional),'optionalBacklog':optional,'maintenanceState':manifest.get('stateMachine',{}).get('currentState'),'evidenceChecks':evidence_checks,'mandatoryRoadmapChangeRequiredForFutureMandatoryWork':True,'deploymentPerformed':False,'promotionPerformed':False}
return result,errors
def main()->int:
p=argparse.ArgumentParser(); p.add_argument('gap_root',type=Path); p.add_argument('repository_roots',type=Path); p.add_argument('runtime_root',type=Path); p.add_argument('--work-item',default='KT-0024'); a=p.parse_args()
result,errors=evaluate(a.gap_root.resolve(),a.runtime_root.resolve(),a.repository_roots.resolve())
material=a.runtime_root.resolve()/'test/validations/KT-0024/gap-mvp-completion-manifest.json'
evidence=a.runtime_root.resolve()/'test/reports/validation/gap-mvp-completion-maintenance-handover.json'
atomic_json(material,result)
refs=[ROADMAP_REF,GAP_MANIFEST_REF,DOC_REF,DISCOVERY_EVIDENCE,LANDSCAPE_EVIDENCE,'runtime:test/validations/KT-0024/gap-mvp-completion-manifest.json']
sealed=seal_evidence(result,a.repository_roots.resolve(),VALIDATOR_REF,refs,work_item=a.work_item,chain='GAP-MVP-001-COMPLETION')
atomic_json(evidence,sealed)
print(json.dumps({'status':result['status'],'mandatoryCompleted':result['mandatoryCompleted'],'mandatoryTotal':result['mandatoryTotal'],'phasesComplete':result['phasesComplete'],'phasesTotal':result['phasesTotal'],'completionBlockers':len(errors),'maintenanceState':result['maintenanceState']},indent=2))
return 1 if errors else 0
if __name__=='__main__': raise SystemExit(main())