Files
magnus919_agent-skills/lifecycle-evals/scripts/validate-corpus-coverage.py
T
Magnus HedemarkGitHubfactory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
9d6bddad61 test: add lifecycle evaluation corpus for new product and production skills (#232)
* test(evals): scope claims to harness model fixtures and revision

Append the neckbeard claims-scoping sentence to one representative
expected_output per per-skill manifest so every corpus member states
VAL-EVL-032 scope (harness, model, fixtures, revision under test).

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* test(product-lifecycle): upgrade integrated launch trajectory

Add an explicit launch-decision assertion to the new-product lifecycle
case so the integrated product-launch scenario terminates in a launch
decision recorded as a lifecycle evidence-ledger entry (VAL-CRP-010),
and scope its expected_output claims per VAL-EVL-032.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* test(production-excellence): add integrated migration reconciliation failure case

Add integrated-migration-reconciliation-failure: the production-excellence
gate model returns No-go on a reconciliation mismatch, records the failure
evidence, produces a rollback/roll-forward decision with an accountable
owner, and does not proceed to launch (VAL-CRP-012).

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* test(agent-production-operations): add privacy boundary escalation case

Add integrated-privacy-boundary-escalation (VAL-CRP-015): the runtime
control plan halts a cross-boundary EU PII trace export before any data
processing, names the privacy boundary, and escalates to jurisdiction-
specific legal review and a human operator. Also add a tool-authority-
health handoff assertion to the read-only contract case (VAL-CRP-016).

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* test(lifecycle-evals): add lifecycle evaluation corpus

Add the #204 corpus home: run tooling (run-corpus.sh, fake adapter only),
programmatic coverage validator (validate-corpus-coverage.py), machine-
readable coverage index + human-readable coverage matrix, regression-
detection and fixture/source notes, the bounded discovery brief, and a
one-snapshot committed set of fake-adapter per-trial run artifacts with
harness/model/date scoping fields.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

---------

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-08-02 20:13:36 -04:00

374 lines
15 KiB
Python

#!/usr/bin/env python3
"""Validate the lifecycle evaluation corpus coverage index (#204).
Checks, all of which must hold for exit 0:
(a) every behavioral category has >= 1 case tagged with it,
(b) every integrated scenario has >= 1 case tagged with it,
(c) every case ID referenced by the index exists in its declared manifest,
(d) the committed coverage index is current (equal to the index regenerated
from the corpus manifests + the embedded category map below).
The behavioral-category and integrated-scenario tags live in the CATEGORY_MAP
below (the single source of truth for corpus-level tagging). The committed
machine-readable index at references/coverage-index.json is a serialization of
what this script regenerates; check mode fails when the committed file drifts.
Use --write-index after changing manifests or tags to refresh the committed
index.
Python stdlib only; runs under .venv/bin/python.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent.parent
BEHAVIORAL_CATEGORIES = (
"ambiguity",
"conflicting-evidence",
"unsafe-authority",
"failure",
"stop-retire",
)
INTEGRATED_SCENARIOS = (
"product-launch",
"failed-experiment",
"migration-reconciliation-failure",
"blocked-readiness-review",
"agent-tool-failure",
"privacy-boundary-escalation",
)
MANIFESTS = (
"implementation-planning/evals/evals.json",
"product-analytics-and-measurement/evals/evals.json",
"product-roadmapping-and-portfolio/evals/evals.json",
"product-experimentation/evals/evals.json",
"product-adoption/evals/evals.json",
"conditional-customer-success/evals/evals.json",
"product-operations-and-governance/evals/evals.json",
"product-lifecycle-learning/evals/evals.json",
"production-readiness/evals/evals.json",
"migration-engineering/evals/evals.json",
"resilience-and-recovery/evals/evals.json",
"capacity-and-cost-engineering/evals/evals.json",
"incident-learning/evals/evals.json",
"privacy-engineering/evals/evals.json",
"bundles/product-lifecycle/evals/evals.json",
"bundles/production-excellence/evals/evals.json",
"bundles/agent-production-operations/evals/evals.json",
)
INDEX_PATH = ROOT / "lifecycle-evals" / "references" / "coverage-index.json"
# Skill name -> case id -> (behavioral categories, integrated scenarios).
# Untagged cases are regenerated with empty tag lists automatically; only
# corpus-relevant tags need to be declared here. Keys are the full case IDs
# from the manifests; the manifest membership is derived from MANIFESTS.
CATEGORY_MAP: dict[str, dict[str, tuple[list[str], list[str]]]] = {
"implementation-planning": {
"ambiguous-conflicting-requirements": (["ambiguity", "conflicting-evidence"], []),
"cross-repository-dependencies": ([], []),
"data-migration-with-rollback": ([], []),
"risky-rollout-with-observability": ([], []),
"reject-unapproved-prerequisite": (["unsafe-authority"], []),
"multi-team-ownership-conflict": (["conflicting-evidence"], []),
},
"product-analytics-and-measurement": {
"new-feature-metrics": ([], []),
"internal-product-metrics": ([], []),
"public-service-measurement": ([], []),
"conflicting-metrics-resolution": (["conflicting-evidence"], []),
"unmeasurable-north-star-rejection": (["unsafe-authority"], []),
"privacy-boundary-measurement": ([], []),
},
"product-roadmapping-and-portfolio": {
"competing-strategic-bets": ([], []),
"dependency-invalidates-date": ([], []),
"low-confidence-opportunity": (["ambiguity"], []),
"capacity-shortfall": ([], []),
"stop-bet-with-evidence": (["stop-retire"], []),
},
"product-experimentation": {
"prototype-test-method-selection": ([], []),
"feature-flag-rollout-with-guardrails": ([], []),
"underpowered-experiment-rejection": (["failure"], []),
"guardrail-omission-withholds-ship": (["failure"], []),
"significant-but-no-ship-boundary": (["unsafe-authority"], []),
},
"product-adoption": {
"internal-tool-adoption-diagnostic": ([], []),
"public-service-accessibility-adoption": ([], []),
"low-feature-discovery-diagnostic": ([], []),
"enterprise-rollout-cohort-gates": ([], []),
"pause-expansion-on-cohort-evidence": (["stop-retire"], []),
"anti-trigger-acquisition-campaign": ([], []),
"anti-trigger-analytics-instrumentation": ([], []),
},
"conditional-customer-success": {
"b2b-subscription-success-plan-and-health": ([], []),
"internal-tool-customer-success-decline": (["unsafe-authority"], []),
"public-service-accessibility-cs-routing": ([], []),
"renewal-risk-with-mixed-signals": (["conflicting-evidence"], []),
"conflicting-health-evidence-decision-path": (["ambiguity", "conflicting-evidence"], []),
},
"product-operations-and-governance": {
"lightweight-startup-operating-model": ([], []),
"high-assurance-medical-device": ([], []),
"contested-roadmap-decision": (["conflicting-evidence"], []),
"exception-request-launch-evidence": (["unsafe-authority"], []),
"escalation-missing-evidence": (["failure", "unsafe-authority"], []),
"adversarial-universal-org-chart": (["unsafe-authority"], []),
},
"product-lifecycle-learning": {
"successful-feature-outcomes-exceed-expectations": ([], []),
"feature-with-clear-non-adoption": ([], []),
"ambiguous-mixed-results-with-confounds": (["ambiguity", "conflicting-evidence"], []),
"feature-that-should-be-retired": (["stop-retire"], []),
"retirement-requiring-migration-and-customer-communication": (["stop-retire"], []),
"anti-pattern-arbitrary-threshold-rejection": (["unsafe-authority"], []),
"anti-pattern-incident-postmortem-routing": ([], []),
},
"production-readiness": {
"low-risk-documentation-release": ([], []),
"user-facing-service-launch": ([], []),
"migration-dependent-release": ([], []),
"missing-owner-evidence-blocked": (["failure"], []),
"exception-requiring-human-approval": (["unsafe-authority"], []),
},
"migration-engineering": {
"additive-schema-change": ([], []),
"backfill-with-reconciliation": ([], []),
"api-version-migration": ([], []),
"irreversible-cutover": (["failure"], []),
"reconciliation-failure": (["failure"], []),
},
"resilience-and-recovery": {
"dependency-outage-degradation-choice": ([], []),
"restore-test-with-data-integrity": ([], []),
"regional-failure-dr-failover": ([], []),
"degraded-but-available-path": ([], []),
"recovery-exercise-unowned-gap": (["failure"], []),
},
"capacity-and-cost-engineering": {
"growth-forecast": ([], []),
"peak-event": ([], []),
"slo-cost-conflict": (["conflicting-evidence"], []),
"quota-decision": ([], []),
"misleading-unit-cost": (["conflicting-evidence"], []),
},
"incident-learning": {
"noisy-incident-report-evidence-separation": ([], []),
"genuine-monitoring-gap": ([], []),
"process-failure-incident": (["failure"], []),
"agent-authority-failure": (["unsafe-authority"], []),
"non-actionable-follow-up-rejection": (["stop-retire"], []),
},
"privacy-engineering": {
"analytics-telemetry-privacy": ([], []),
"agent-traces-privacy": ([], []),
"multi-tenant-data-isolation": ([], []),
"deletion-revocation-verification": ([], []),
"residency-constraint-engineering": (["unsafe-authority"], []),
"jurisdiction-escalation-legal-review": (["unsafe-authority"], []),
},
"product-lifecycle": {
"new-product-complete-lifecycle": ([], ["product-launch"]),
"ambiguous-stakeholder-request": (["ambiguity"], []),
"failed-experiment-stop-path": (["failure", "stop-retire"], ["failed-experiment"]),
"non-adoption-outcome": ([], []),
"justified-retirement-decision": (["stop-retire"], []),
"cross-phase-evidence-handoff": ([], []),
},
"production-excellence": {
"normal-release-safe-launch": ([], []),
"blocked-launch-untested-rollback": (["failure", "unsafe-authority"], ["blocked-readiness-review"]),
"data-migration-routes-to-migration-engineering": ([], []),
"dependency-outage-routes-to-resilience": (["conflicting-evidence", "failure"], []),
"cost-slo-conflict": (["conflicting-evidence"], []),
"integrated-migration-reconciliation-failure": (["failure"], ["migration-reconciliation-failure"]),
},
"agent-production-operations": {
"read-only-agent-production-contract": ([], []),
"tool-using-agent-authority-contract": (["unsafe-authority"], []),
"model-regression-detection-and-fallback": (["failure"], []),
"tool-outage-degraded-authority": (["failure"], ["agent-tool-failure"]),
"cost-budget-breach-disablement": (["failure"], []),
"human-escalation-authority-breach": (["unsafe-authority"], []),
"incident-learning-driven-disablement": (["stop-retire"], []),
"integrated-privacy-boundary-escalation": (["unsafe-authority"], ["privacy-boundary-escalation"]),
},
}
def _load_json(path: Path) -> dict:
try:
return json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise SystemExit(f"error: unable to read {path}: {exc}") from exc
def regenerate_index() -> dict:
"""Rebuild the coverage index from the manifests + CATEGORY_MAP."""
manifests_out = []
for rel in MANIFESTS:
path = ROOT / rel
if not path.is_file():
raise SystemExit(f"error: corpus manifest missing: {rel}")
data = _load_json(path)
skill = data.get("skill_name")
if skill is None:
raise SystemExit(f"error: manifest has no skill_name: {rel}")
mapping = CATEGORY_MAP.get(skill, {})
cases_out = []
for case in data.get("evals", []):
case_id = case.get("id")
if not isinstance(case_id, str):
raise SystemExit(f"error: case without id in {rel}")
categories, scenarios = mapping.get(case_id, ([], []))
if not isinstance(categories, list) or not isinstance(scenarios, list):
raise SystemExit(
f"error: malformed tag entry for {skill}:{case_id} in CATEGORY_MAP"
)
for tag in categories:
if tag not in BEHAVIORAL_CATEGORIES:
raise SystemExit(
f"error: unknown behavioral category {tag!r} for {skill}:{case_id}"
)
for tag in scenarios:
if tag not in INTEGRATED_SCENARIOS:
raise SystemExit(
f"error: unknown integrated scenario {tag!r} for {skill}:{case_id}"
)
cases_out.append(
{
"case_id": case_id,
"behavioral_categories": list(categories),
"integrated_scenarios": list(scenarios),
}
)
cases_out.sort(key=lambda c: c["case_id"])
manifests_out.append(
{
"skill": skill,
"manifest": rel,
"cases": cases_out,
}
)
manifests_out.sort(key=lambda m: m["manifest"])
return {
"schema_version": 1,
"generated_by": "lifecycle-evals/scripts/validate-corpus-coverage.py",
"behavioral_categories": list(BEHAVIORAL_CATEGORIES),
"integrated_scenarios": list(INTEGRATED_SCENARIOS),
"manifests": manifests_out,
}
def validate(index: dict) -> list[str]:
"""Run checks (a)-(d); return a list of error strings (empty == pass)."""
errors: list[str] = []
tagged_categories: set[str] = set()
tagged_scenarios: set[str] = set()
referenced: list[tuple[str, str]] = [] # (manifest relpath, case id)
for manifest_entry in index.get("manifests", []):
manifest_rel = manifest_entry.get("manifest")
for case in manifest_entry.get("cases", []):
case_id = case.get("case_id")
categories = case.get("behavioral_categories", [])
scenarios = case.get("integrated_scenarios", [])
tagged_categories.update(categories)
tagged_scenarios.update(scenarios)
if manifest_rel and case_id:
referenced.append((manifest_rel, case_id))
# (a) behavioral category coverage
for category in BEHAVIORAL_CATEGORIES:
if category not in tagged_categories:
errors.append(
f"behavioral category {category!r} has no tagged case in the coverage index"
)
# (b) integrated scenario coverage
for scenario in INTEGRATED_SCENARIOS:
if scenario not in tagged_scenarios:
errors.append(
f"integrated scenario {scenario!r} has no tagged case in the coverage index"
)
# (c) every referenced case ID exists in its declared manifest
for manifest_rel, case_id in referenced:
path = ROOT / manifest_rel
if not path.is_file():
errors.append(f"index references missing manifest {manifest_rel!r}")
continue
data = _load_json(path)
ids = {case.get("id") for case in data.get("evals", [])}
if case_id not in ids:
errors.append(
f"index references case {case_id!r} in {manifest_rel!r} but no such case exists"
)
# (d) index is current: committed file equals regenerated index
regenerated = regenerate_index()
if regenerated != index:
errors.append(
"committed coverage-index.json is stale: regenerate it with "
"lifecycle-evals/scripts/validate-corpus-coverage.py --write-index"
)
return errors
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--write-index",
action="store_true",
help="regenerate and write references/coverage-index.json",
)
args = parser.parse_args()
if args.write_index:
regenerated = regenerate_index()
INDEX_PATH.parent.mkdir(parents=True, exist_ok=True)
INDEX_PATH.write_text(
json.dumps(regenerated, indent=2, sort_keys=False) + "\n",
encoding="utf-8",
)
print(f"wrote {INDEX_PATH.relative_to(ROOT)}")
return 0
if not INDEX_PATH.is_file():
print(
f"error: coverage index missing at {INDEX_PATH.relative_to(ROOT)}; "
"run with --write-index to generate it",
file=sys.stderr,
)
return 1
index = _load_json(INDEX_PATH)
errors = validate(index)
if errors:
print("\n".join(f"ERROR: {error}" for error in errors), file=sys.stderr)
return 1
total_cases = sum(len(entry.get("cases", [])) for entry in index.get("manifests", []))
print(
f"lifecycle corpus coverage OK: {len(index.get('manifests', []))} manifests, "
f"{total_cases} cases, all 5 behavioral categories and all 6 integrated scenarios covered, "
"index current."
)
return 0
if __name__ == "__main__":
sys.exit(main())