orchestrata_ultimate.verification.spec_completeness
Reference › orchestrata_ultimate › verification › spec_completeness
Answers "how much of this spec did the generated project implement?", for the QA volk. The entry point is evaluate_spec_completeness(project_dir, task_summary). It is re-exported by the package root.
The flow: _find_spec_checklist takes the first three-digit number in the task summary and looks for a spec directory with that prefix containing checklists/requirements.md; with no match the function returns an empty dict. Requirements are parsed into Prolog facts by the openspec requirements parser and into (id, description) tuples by _parse_req_tuples. implementation_scanner then matches them against AST evidence. _run_prolog_evaluation loads spec_completeness.pl, retracts earlier facts for the spec (so repeated calls in one process do not inflate the denominator), asserts the new facts and queries the coverage predicates. When Prolog is unavailable a pure Python fallback computes the same counts.
The result holds spec_coverage (for example "47/202 (23.3%)"), spec_complete, gaps (unresolved requirements are labelled as such), scan_degraded and scan_diagnostics. When scan_degraded is true an evidence source could not be read, so the gaps describe missing source and the coverage is a floor.
At a glance
| Source | src/orchestrata_ultimate/verification/spec_completeness.py |
| Lines of code | 306 |
| Classes | 0 |
| Functions | 4 |
| Imports (internal) | orchestrata_ultimate.core.infra_config, orchestrata_ultimate.core.paths, orchestrata_ultimate.core.prolog_adapter, orchestrata_ultimate.openspec.requirements_parser, orchestrata_ultimate.verification.implementation_scanner |
| Imported by | orchestrata_ultimate.verification |
| Tests that import it | tests/generated_recovery/test_verification_spec_completeness.py, tests/verification/test_spec_completeness.py, tests/verification/test_spec_completeness_extended.py |
Class diagram
No classes; see Functions.
Dependency diagram
API
Functions
| Function | Signature | Summary |
|---|---|---|
_find_spec_checklist |
_find_spec_checklist(task_summary: str) -> tuple[str, str] |
Locate the spec checklist from the task summary. |
_parse_req_tuples |
_parse_req_tuples(checklist_path: str) -> list[tuple[str, str]] |
Parse requirements as (req_id, description) tuples. |
_run_prolog_evaluation |
_run_prolog_evaluation(spec_id: str, req_facts: list[str], impl_facts: list[str]) -> dict |
Run Prolog evaluation of spec completeness. |
evaluate_spec_completeness |
evaluate_spec_completeness(project_dir: str, task_summary: str) -> dict |
Evaluate spec completeness for a generated project. |
Source
"""Spec completeness evaluation via Prolog.
Parses a spec's requirements checklist, scans generated code for
implementation evidence, asserts both as Prolog facts, and queries
the ``spec_completeness.pl`` rules to determine coverage and gaps.
This module is called by the QA volk after all review gates pass,
before declaring review complete.
Migrated from spike/spec_completeness.py to production package.
"""
import os
import re
from orchestrata_ultimate.core.infra_config import PROLOG_RULES_DIR
from orchestrata_ultimate.core.paths import resolve_project_root
from orchestrata_ultimate.openspec.requirements_parser import parse_requirements
from orchestrata_ultimate.verification.implementation_scanner import (
match_requirements_to_evidence,
)
# Matches REQ lines to extract (req_id, description) tuples
_REQ_RE = re.compile(
r"^-\s*\[[ x]\]\s*\*\*(?P<req_id>REQ-\d{3}-\d{3})\*\*:" r"\s*(?P<desc>.+)$"
)
# Bug fix (found while touching this file 2026-08-12): this counted SIX
# os.path.dirname() hops off __file__ -- verification -> orchestrata_ultimate
# -> src -> repo root -> workspace -> home -- landing two levels above the
# repo at `~/specs`, which never exists. `_find_spec_checklist` therefore
# always failed its `os.path.isdir(_SPECS_DIR)` check and every real call to
# `evaluate_spec_completeness` silently returned `{}`, in every environment,
# since this module was written. Four hops (verification -> orchestrata_ultimate
# -> src -> repo root) fixed it for the source tree; since the proper install
# (dd919d55c) the module sits in site-packages, where four hops is .venv/lib,
# so the root now comes from resolve_project_root (the nearest pyproject.toml).
_SPECS_DIR = str(resolve_project_root() / "specs")
def _find_spec_checklist(
task_summary: str,
) -> tuple[str, str]:
"""Locate the spec checklist from the task summary.
Searches the task summary for spec references (e.g. ``014``,
``015-plugin-sdk``) and resolves to the checklist path on disk.
Args:
task_summary: The task summary text from the CXP packet.
Returns:
Tuple of ``(spec_id, checklist_path)`` or ``("", "")``
if no spec checklist is found.
"""
if not os.path.isdir(_SPECS_DIR):
return "", ""
# Try to find a 3-digit spec number in the task summary
match = re.search(r"(\d{3})", task_summary)
if not match:
return "", ""
spec_num = match.group(1)
for dirname in os.listdir(_SPECS_DIR):
if dirname.startswith(spec_num):
checklist = os.path.join(
_SPECS_DIR,
dirname,
"checklists",
"requirements.md",
)
if os.path.isfile(checklist):
return spec_num, checklist
return "", ""
def _parse_req_tuples(
checklist_path: str,
) -> list[tuple[str, str]]:
"""Parse requirements as (req_id, description) tuples.
Args:
checklist_path: Absolute path to requirements.md.
Returns:
List of ``(req_id, description)`` tuples.
"""
tuples: list[tuple[str, str]] = []
with open(checklist_path, encoding="utf-8") as fh:
for line in fh:
stripped = line.strip()
req_match = _REQ_RE.match(stripped)
if req_match:
tuples.append(
(
req_match.group("req_id"),
req_match.group("desc").strip(),
)
)
return tuples
def _run_prolog_evaluation(
spec_id: str,
req_facts: list[str],
impl_facts: list[str],
) -> dict:
"""Run Prolog evaluation of spec completeness.
Loads ``spec_completeness.pl``, asserts requirement and
implementation facts, then queries coverage predicates.
Args:
spec_id: Spec number (e.g. ``"014"``).
req_facts: List of ``spec_requirement/3`` fact strings.
impl_facts: List of ``implemented/2`` fact strings.
Returns:
Dict with ``spec_complete``, ``implemented_count``,
``total_count``, and ``gaps`` list.
"""
from orchestrata_ultimate.core.prolog_adapter import (
PROLOG_AVAILABLE,
PrologEngine,
)
if not PROLOG_AVAILABLE:
return {}
# janus-swi via the adapter: rules come from spec_completeness.pl (consulted)
# and the requirement/implementation facts asserted below are ground.
prolog = PrologEngine()
rules_path = os.path.join(
PROLOG_RULES_DIR,
"spec_completeness.pl",
)
if os.path.isfile(rules_path):
prolog.consult(rules_path)
# Clear this spec's facts before asserting. spec_requirement/3 and
# implemented/2 are dynamic and the engine outlives the call, so
# without this a second evaluation of the same spec in one process
# sees BOTH generations. Measured: evaluating one 1-requirement spec
# twice reported total_count=2 -- coverage denominators grew with the
# number of times the process had been asked, which is not a property
# of the spec.
for predicate in (
f"spec_completeness:spec_requirement('{spec_id}', _, _)",
f"spec_completeness:implemented('{spec_id}', _)",
):
try:
prolog.retractall(predicate)
except Exception:
continue
# Assert requirement facts (module-qualified for SWI-Prolog)
for fact in req_facts:
try:
prolog.assertz(f"spec_completeness:{fact}")
except Exception:
continue
# Assert implementation facts (module-qualified for SWI-Prolog)
for fact in impl_facts:
try:
prolog.assertz(f"spec_completeness:{fact}")
except Exception:
continue
# Query spec_complete
complete = bool(list(prolog.query(f"spec_complete('{spec_id}')")))
# Query implementation_coverage
impl_count = 0
total_count = 0
coverage_results = list(prolog.query(f"implementation_coverage('{spec_id}', I, T)"))
if coverage_results:
impl_count = int(coverage_results[0]["I"])
total_count = int(coverage_results[0]["T"])
# Query missing requirements individually to avoid
# janus-swi PrologError on compound terms like gap(R, S).
# Instead of gap_report/2 which returns findall(gap(...)),
# we query missing_requirement/3 and collect results in Python.
gaps: list[str] = []
missing_results = list(
prolog.query(f"missing_requirement('{spec_id}', ReqId, Desc)")
)
for result in missing_results:
req_id = str(result.get("ReqId", ""))
desc = str(result.get("Desc", ""))
gaps.append(f"{req_id}: {desc}")
return {
"spec_complete": complete,
"implemented_count": impl_count,
"total_count": total_count,
"gaps": gaps,
}
def evaluate_spec_completeness(
project_dir: str,
task_summary: str,
) -> dict:
"""Evaluate spec completeness for a generated project.
This is the main entry point called by the QA volk. It:
1. Finds the spec checklist from the task summary.
2. Parses requirements into Prolog facts.
3. Scans the project for implementation evidence.
4. Runs Prolog evaluation.
5. Returns a coverage report dict.
Args:
project_dir: Root directory of the generated project.
task_summary: Task summary string (used to find spec).
Returns:
Dict with keys ``spec_coverage`` (str like ``"47/202"``),
``spec_complete`` (bool), ``gaps`` (list of str),
``scan_degraded`` (bool) and ``scan_diagnostics`` (list of str),
or empty dict if no spec checklist is found.
``scan_degraded`` is the one that must not be ignored: when it is
true an evidence source could not be read, so ``gaps`` describes
the missing SOURCE rather than missing work, and the coverage
number is a floor rather than a measurement.
"""
spec_id, checklist_path = _find_spec_checklist(task_summary)
if not spec_id:
return {}
# Parse requirements
req_facts = parse_requirements(spec_id, checklist_path)
if not req_facts:
return {}
req_tuples = _parse_req_tuples(checklist_path)
# Scan implementation (AST-derived evidence) and match against requirements.
# `unresolved` (a description with no identifier-shaped token at all) is
# kept distinct from `gaps` (a named identifier with no matching evidence)
# per the implementation-scanner honesty rule; both still count toward
# "not implemented" in the coverage number below, which is unchanged in
# kind -- only the gap text now says which is which.
match_report = match_requirements_to_evidence(
spec_id,
req_tuples,
project_dir,
)
impl_facts = match_report.implemented
gaps = match_report.gaps
unresolved = match_report.unresolved
# A DEGRADED scan could not read some evidence source, so its gaps are
# not findings -- they are the shape of the missing source. Reporting
# them as ordinary gaps is precisely how commit 29bbf088 went
# unnoticed: it deleted all non-Python evidence collection, and every
# JS/TS-implemented requirement started reading as a clean gap.
scan_diagnostics = list(match_report.diagnostics)
# Try Prolog evaluation first
prolog_result = _run_prolog_evaluation(
spec_id,
req_facts,
impl_facts,
)
unresolved_strs = [
f"{rid}: {desc} (unresolved — no structural identifier found)"
for rid, desc in unresolved
]
if prolog_result:
impl_n = prolog_result["implemented_count"]
total_n = prolog_result["total_count"]
pct = (impl_n / total_n * 100) if total_n > 0 else 0.0
gap_strs = prolog_result["gaps"]
# Enrich gaps with req IDs from Python-side matching
if not gap_strs and (gaps or unresolved):
gap_strs = [f"{rid}: {desc}" for rid, desc in gaps] + unresolved_strs
return {
"spec_coverage": (f"{impl_n}/{total_n} ({pct:.1f}%)"),
"spec_complete": prolog_result["spec_complete"],
"gaps": gap_strs,
"scan_degraded": bool(scan_diagnostics),
"scan_diagnostics": scan_diagnostics,
}
# Python fallback when Prolog is unavailable
total = len(req_tuples)
implemented = total - len(gaps) - len(unresolved)
pct = (implemented / total * 100) if total > 0 else 0.0
gap_strs = [f"{rid}: {desc}" for rid, desc in gaps] + unresolved_strs
return {
"spec_coverage": (f"{implemented}/{total} ({pct:.1f}%)"),
"spec_complete": len(gaps) == 0 and len(unresolved) == 0,
"gaps": gap_strs,
"scan_degraded": bool(scan_diagnostics),
"scan_diagnostics": scan_diagnostics,
}