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())