40-validation/validate-artifact-manifests.py
#!/usr/bin/env python3
import argparse, json, sys
from pathlib import Path
from evidence_lifecycle import seal_evidence, write_evidence
REQUIRED_TOP = {"manifestVersion","artifact","realizes","implements","documentation","ownership","quality"}
REQUIRED_ARTIFACT = {"id","type","name","status"}
REQUIRED_REALIZES = {"capabilities","referenceBuildingBlocks","solutionBuildingBlocks","requirements"}
def validate_test_coverage(manifest, repository_root, errors):
use_cases = manifest.get("implements", {}).get("useCases", [])
coverage = manifest.get("quality", {}).get("testCoverage", {})
for use_case in use_cases:
use_case_id = use_case.get("id", "unknown")
entries = coverage.get(use_case_id, [])
if not entries:
errors.append(f"use case {use_case_id} has no human-readable testCoverage")
continue
categories = set()
for entry in entries:
category = entry.get("category")
purpose = entry.get("purpose", "").strip()
relative = entry.get("path")
if not category or not purpose or not relative:
errors.append(f"use case {use_case_id} has incomplete testCoverage entry: {entry}")
continue
categories.add(category)
if not (repository_root / relative).exists():
errors.append(f"use case {use_case_id} missing covered test path: {relative}")
required_categories = set(use_case.get("requiredTestCategories", []))
missing = required_categories - categories
if missing:
errors.append(f"use case {use_case_id} missing required test categories: {sorted(missing)}")
ALLOWED_CODE_AREA_CLASSES = {
"domain-logic", "security-critical", "application-logic", "adapter",
"transport", "declaration-only", "generated", "composition-root"
}
ALLOWED_VERIFICATION_STATUS = {
"directly-verified", "indirectly-verified", "not-applicable", "verification-gap"
}
def validate_verification_coverage(manifest, repository_root, errors):
for area in manifest.get("verification", {}).get("codeAreas", []):
relative = area.get("path")
classification = area.get("classification")
status = area.get("status")
prefix = f"code area {relative or 'unknown'}"
if not relative or not (repository_root / relative).exists():
errors.append(f"{prefix} path does not exist")
if classification not in ALLOWED_CODE_AREA_CLASSES:
errors.append(f"{prefix} has invalid classification: {classification}")
if status not in ALLOWED_VERIFICATION_STATUS:
errors.append(f"{prefix} has invalid verification status: {status}")
evidence = area.get("evidence", [])
if status in {"directly-verified", "indirectly-verified"} and not evidence:
errors.append(f"{prefix} requires verification evidence")
for item in evidence:
p = item.get("path")
if not item.get("category") or not item.get("purpose", "").strip() or not p:
errors.append(f"{prefix} has incomplete evidence: {item}")
elif not (repository_root / p).exists():
errors.append(f"{prefix} evidence path does not exist: {p}")
if classification == "declaration-only" and status == "directly-verified":
errors.append(f"{prefix} must not require an artificial direct test")
if classification in {"domain-logic", "security-critical", "application-logic"} and status == "not-applicable":
errors.append(f"{prefix} critical logic cannot be not-applicable")
if status in {"indirectly-verified", "not-applicable", "verification-gap"} and not area.get("reason"):
errors.append(f"{prefix} status {status} requires a reason")
if status == "verification-gap" and not area.get("severity"):
errors.append(f"{prefix} verification gap requires severity")
def validate_quality_model(manifest, repository_root, errors):
quality = manifest.get("qualityModel")
if quality is None:
return
goals = quality.get("goals", [])
if not goals:
errors.append("qualityModel requires at least one goal")
goal_ids=set()
for goal in goals:
gid=goal.get("id")
if not gid or gid in goal_ids:
errors.append(f"invalid or duplicate quality goal id: {gid}")
goal_ids.add(gid)
if not goal.get("attribute") or not goal.get("statement", "").strip():
errors.append(f"quality goal {gid} requires attribute and statement")
for evidence in quality.get("evidence", []):
if not (repository_root / evidence).exists():
errors.append(f"quality evidence path does not exist: {evidence}")
for gap in quality.get("gaps", []):
if not gap.get("id") or not gap.get("severity") or not gap.get("statement", "").strip():
errors.append(f"incomplete quality gap: {gap}")
def fail(msg):
print(f"ERROR: {msg}", file=sys.stderr)
return 1
def validate_human_readable_contracts(manifest, repository_root, errors):
documentation = manifest.get("documentation", {})
for relative in documentation.get("humanReadableContracts", []):
path = repository_root / relative
if not path.exists():
errors.append(f"missing human-readable contract: {relative}")
for use_case in manifest.get("implements", {}).get("useCases", []):
for field in ("code", "documentation"):
relative = use_case.get(field)
if not relative or not (repository_root / relative).exists():
errors.append(f"use case {use_case.get('id','unknown')} missing {field}: {relative}")
for relative in use_case.get("tests", []):
if not (repository_root / relative).exists():
errors.append(f"use case {use_case.get('id','unknown')} missing test: {relative}")
def main():
ap=argparse.ArgumentParser()
ap.add_argument('repository', type=Path)
ap.add_argument('--solution-architecture', type=Path)
ap.add_argument('--enterprise-architecture', type=Path)
ap.add_argument('--evidence-output', type=Path)
ap.add_argument('--repository-roots', type=Path)
ap.add_argument('--work-item', default='manual')
ap.add_argument('--chain', default='RC-0002')
args=ap.parse_args()
files=sorted(args.repository.rglob('artifact-manifest.json'))
if not files:
return fail('no artifact-manifest.json found')
ids=set(); errors=[]
for path in files:
try: data=json.loads(path.read_text(encoding='utf-8'))
except Exception as exc:
errors.append(f'{path}: invalid JSON: {exc}'); continue
missing=REQUIRED_TOP-data.keys()
if missing: errors.append(f'{path}: missing top fields {sorted(missing)}'); continue
if data.get('manifestVersion')!='1': errors.append(f'{path}: unsupported manifestVersion')
if REQUIRED_ARTIFACT-data['artifact'].keys(): errors.append(f'{path}: incomplete artifact')
if REQUIRED_REALIZES-data['realizes'].keys(): errors.append(f'{path}: incomplete realizes')
aid=data['artifact'].get('id')
if aid in ids: errors.append(f'{path}: duplicate artifact.id {aid}')
ids.add(aid)
module=args.repository/data['ownership'].get('modulePath','')
if not module.exists(): errors.append(f'{path}: modulePath does not exist: {module}')
for docroot,key in [(args.solution_architecture,'solutionArchitecture'),(args.enterprise_architecture,'enterpriseArchitecture')]:
if docroot:
for rel in data.get('documentation',{}).get(key,[]):
if not (docroot/rel).exists(): errors.append(f'{path}: missing documentation reference {key}:{rel}')
for rel in data.get('quality',{}).get('tests',[]):
if not (args.repository/rel).exists(): errors.append(f'{path}: missing test path {rel}')
validate_human_readable_contracts(data, args.repository, errors)
validate_test_coverage(data, args.repository, errors)
validate_verification_coverage(data, args.repository, errors)
validate_quality_model(data, args.repository, errors)
if errors:
for e in errors: print('ERROR:',e,file=sys.stderr)
return 1
output=f'OK: validated {len(files)} artifact manifests'
print(output)
if args.evidence_output:
if not args.repository_roots:
return fail('--repository-roots is required with --evidence-output')
payload=seal_evidence(
{
'evidenceType':'artifact-manifest-validation',
'validator':'engineering-tools:40-validation/validate-artifact-manifests.py',
'repository':'engineering-platform',
'result':'passed',
'exitCode':0,
'output':output,
},
args.repository_roots,
'engineering-tools:40-validation/validate-artifact-manifests.py',
[
'engineering-platform:10-backend/internal/artifacts/artifact-manifest.json',
'runtime:test/installations/engineering-platform/web/src/platform-shell/artifact-manifest.json',
],
work_item=args.work_item, chain=args.chain,
)
write_evidence(args.evidence_output,payload)
return 0
if __name__=='__main__': raise SystemExit(main())