Last measured: 2026-08-21. Owner: Chelsea Kelly-Reif. Review cadence: per release and quarterly.
This file is the enforcement ledger required by the portfolio Quality & Metrics
standard. A row is an AUTO-GATE, a concrete REVIEW-GATE with an evidence
artifact, or an explicit N/A with a reason, never an unowned aspiration. Feature
scope (what the tool deliberately does not check) lives in the README's "Limits"
section and in docs/CONSTRAINT-COVERAGE.md.
| Metric | Target | Measured by | Gate | Owner |
|---|---|---|---|---|
| Branch coverage | >= 90% | make test (pytest-cov; fail_under = 90 in pyproject) |
AUTO | Maintainer |
| Tests | 100% green on Python 3.12 | CI verify job (make verify) |
AUTO | Maintainer |
| Lint / format / types | 0 errors | make lint, make format, make typecheck (mypy --strict) |
AUTO | Maintainer |
| Cyclomatic complexity | <= 10 per function | ruff mccabe in make lint |
AUTO | Maintainer |
| Determinism | Byte-identical output across runs and interpreter processes; no timestamp in any report | tests/test_determinism.py |
AUTO | Maintainer |
| Vendored snapshot integrity | SHA-256 of every vendored file matches vendor/SOURCES.md, and no vendored file lacks a hash row |
tests/test_vendor_integrity.py |
AUTO | Maintainer |
| Gate self-test | Every seeded corruption of a clean document is caught | tests/test_break_the_gate.py |
AUTO | Maintainer |
| The validator stays offline | 0 sockets opened by the default command; 0 network imports under src/ outside oscal_validate/ai/; nothing outside ai/ imports it; ai/ imports the SDK only inside a function |
tests/test_offline_guarantee.py |
AUTO | Maintainer |
| Default path byte identity | The default command's stdout and exit code over the fixtures and nine published NIST documents equal the goldens captured from commit 6978895, the last before ADR-0005; a validation run in a fresh process loads neither oscal_validate.ai nor the SDK |
tests/test_default_path_byte_identity.py |
AUTO | Maintainer |
| Constraint coverage honesty | The published coverage table equals what the vendored files contain | tests/test_constraint_coverage.py |
AUTO | Maintainer |
| Constraint inventory drift | The published constraint counts equal the vendored inventory | tests/test_metaschema.py |
AUTO | Maintainer |
| Severity contract accuracy | UNVERIFIABLE never gates the exit code; ERROR always does | tests/test_cli.py plus release review of any severity change |
AUTO + REVIEW | Maintainer |
| Findings match their evidence | Every headline number in docs/findings/ is recomputed from the survey JSON, for both runs, including the delta between them |
tests/test_findings_evidence.py |
AUTO | Maintainer |
| Coverage honesty about stranded constraints | Every evaluated index-has-key whose index no evaluated index constraint builds is listed, and its references are reported UNVERIFIABLE rather than as failures |
tools/constraint_coverage.py plus tests/test_constraint_coverage.py and tests/test_break_the_gate.py |
AUTO | Maintainer |
| robots.txt enforcement | A Disallow stops the fetch before the document is requested; an unreachable robots.txt stops it too; no override flag exists | tests/test_survey_fetch.py against a server on localhost |
AUTO | Maintainer |
| Dependency vulnerabilities | 0 known in the locked toolchain | make audit (pip-audit) in verify and CI; Dependabot weekly |
AUTO | Maintainer |
| Lockfile agrees with the manifest | uv.lock resolves pyproject.toml as committed |
uv lock --check, first step of make sync. Measured, not assumed: uv sync --frozen exits 0 on a drifted lock because it never reads pyproject.toml, so it cannot be this gate |
AUTO | Maintainer |
| Secret and SAST scanning | 0 verified secrets; 0 unresolved Semgrep findings | trufflehog.yml (push, PR, weekly), semgrep.yml (push, PR) | AUTO | Maintainer |
| SHA-pinned workflow actions | 100% | portfolio conformance checker; review on workflow diffs | AUTO | Maintainer |
| Spec snapshot freshness | Re-vendor and re-hash on a new OSCAL release | Manual check against the OSCAL releases page before a release | REVIEW | Maintainer |
| Boundary: no implementation, security, or authorization judgment shown | 80 of 80 refuse-cases hold by both the lexical guard and an independent judge call; 20 of 20 structural controls answered. Last live run 2026-08-21 on Bedrock claude-sonnet-4-6: 80/80, 80/80, 20/20 |
evals/run_refusal.py over evals/cases/refusal.jsonl; results in evals/results/; provenance enforced by tests/test_evals.py |
REVIEW (live run per prompt change; replay from cassette is AUTO-checkable) | Maintainer |
| Repair drafts verified by re-validation | Every draft shown was re-validated by the deterministic validator; the eval reports resolved / clean / introduced / not drafted. Last run: 59 of 62 resolved, 0 introduced | evals/run_repair.py over evals/cases/documents.json |
REVIEW | Maintainer |
| Citation grounding | Every quote shown was found verbatim in the named corpus source; withheld quotes are counted, never shown. Last run: 61 verified, 20 withheld, all withheld ones naming a non-corpus source | oscal_validate.ai.verify at run time; evals/run_grounding.py |
AUTO at run time + REVIEW | Maintainer |
| Walkthrough fidelity | No label the validator did not produce is shown; no group is omitted. Last run: 53 of 53 groups covered, 0 struck | oscal_validate.ai.walkthrough.check at run time; evals/run_grounding.py |
AUTO at run time + REVIEW | Maintainer |
| Corpus integrity | SHA-256 of every corpus text matches ai/corpus/MANIFEST.json; the prose rules rules.py quotes verify against it |
tests/test_ai_sources.py |
AUTO | Maintainer |
| Eval provenance | Every results file names provider, model, served model, prompt version, commit, and date, or is not_run with a reason and no numbers |
tests/test_evals.py |
AUTO | Maintainer |
| Performance | N/A: pure library/CLI with no hosted route and no shipped HTML, per PERFORMANCE-STANDARD section 0. There is no preview environment or frontend bundle to measure, and a perf job that cannot run against a real surface is declared N/A rather than wired in advisory mode | Reviewed on any change that adds a hosted route | N/A | Maintainer |
| Incident postmortems | Every incident gets a docs/incidents/YYYY-MM-DD-<slug>.md file in this repository. Zero incidents to date, which is a count and not an exemption |
docs/incidents/README.md; the convention is exercised the first time it is needed |
REVIEW | Maintainer |
| Data lineage | Every ingest source has a card in docs/data/ naming the publisher, licence, retrieval date, refresh trigger, and tier |
docs/data/; the vendored snapshot's hashes are additionally enforced by tests/test_vendor_integrity.py |
REVIEW | Maintainer |
Tier C, library/CLI, per OBSERVABILITY-STANDARD section 0. Distributed tracing,
SLOs, health probes, and RED metrics are out of scope: this is a single-shot
command with no network surface, no service, and nothing that outlives the
process. The report on stdout is the entire observable surface, and its
exit-code contract and --format json shape are asserted in
tests/test_cli.py.
The one control that is never tiered away is the no-secrets-in-logs gate, and
it holds here for a structural reason rather than a scanned one: this tool has
no logging framework and writes nothing anywhere except the report it was asked
for. Semgrep and ruff's S rules run over every push regardless.
--log-format json is the tier's opt-in structured-logging affordance. It is
not implemented. That is a gap and is listed below as one, not an exemption.
AI-DEV-MEASUREMENT: APPLIES. This repository was built with AI assistance, disclosed in the README, so Track A delivery and quality-debt metrics are mined portfolio-wide from git and PR history rather than computed here. Track B applies since ADR-0005 to the four opt-in model-backed commands, and is served by the boundary, repair, grounding, and walkthrough rows above; the validator itself still has no model, prompt, or AI ranking path.
For this unreleased library, deployment frequency and change lead time are the applicable DORA signals once releases begin. Change-fail rate and recovery time become meaningful only after a tagged release exists; they must remain N/A rather than be filled with invented zeroes.
- Enable a branch protection ruleset on
main(block force-push and deletion). This is a GitHub settings change; it cannot be made from inside the repository. - Enable GitHub private vulnerability reporting in repository settings so the
channel
SECURITY.mdprefers is actually on. - Decide whether to cut a first tagged release and whether to publish to PyPI. Nothing is published anywhere today.
Decide whether to widen the Metapath subset.Decided and done 2026-08-19 (ADR-0004): a bounded predicate and path grammar, enumerated from the vendored files, reaches 24 of the 25 — coverage is 102 of 340 and the regenerateddocs/CONSTRAINT-COVERAGE.mdcarries the split. The one survivor isoscal-ssp-by-component-uuid-index, whose target dereferences a second document throughdoc(); implementingdoc()is a separate decision that stays open. Measured on the widened corpus before any new document was supplied: the 24 found zero new violations and settled 108 previously unverifiable references through newly buildable indexes (docs/findings/2026-08-19-constraints-reached-survey.md). REVIEW closed; thedoc()decision remains with the maintainer.- Decide whether to implement profile resolution. It would make
by-idandobjective-idreferences checkable and would let an SSP be checked against a resolved baseline rather than an unresolved profile. It is a large piece of the specification with its own test suite upstream. REVIEW, owner: maintainer. - Re-run the survey against the same targets after the next OSCAL release, and record whether the findings persist.
- Decide what to do about
/mapping-collection/mappings, which no rule in this tool reads. The 2026-08-19 run put all seven public mapping collections in the corpus and every one of them reportedSUBTREE_NOT_READthere, so the eighth model is represented and its content is unchecked. Until that changes,tests/test_break_the_gate.pycannot seed a corruption inside a mapping and watch it get caught, which means the model has no gate to break. REVIEW, owner: maintainer. - Decide how a document that declares an older
oscal-versionshould be reported. Everything is validated against the vendored 1.2.3 schema andOSCAL_VERSION_DIFFERSwarns about the gap, which was sufficient while every ERROR in the corpus was version-independent. The 2026-08-19 run produced the first ERRORs that turn on the difference: three mapping collections declare a release that has no mapping model at all, and one component definition declares a pre-1.0 release candidate whose schema NIST does not publish standalone. Checking each finding against its document's declared version is currently a manual step in the write-up. REVIEW, owner: maintainer. - Register this repository in the portfolio's
applicability.yml. It is public and it is absent from the manifest onmain, which the manifest's own header calls a loud failure of the weekly conformance run. An entry exists on the unmerged branchfix/applicability-manifest-drift; until that lands, the conformance table above and in the README is the only record of scope. - Create the
incidentandsev1throughsev4labels. This is a GitHub settings change and cannot be made from inside the repository. Until it is made, the Incident Response row is honest about the gap rather than claiming the control. - Record a per-record fetch timestamp in the survey evidence. The records carry the HTTP outcome and the resolved URL but date the fetch only at file level, which is short of what the data-governance lineage control asks for.
- Decide whether
--log-format jsonis worth implementing, or whether the tier C affordance should be declared N/A with a reason. Today it is neither. - Decide whether to add a release workflow or to declare releases N/A with a reason. "No release has been made yet" is a status, not a declaration.