Measure the concept-declaration gap (INFO-WP-0027-T01)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 3s

Adds maintenance.concept_candidates() and the concept-coverage CLI command,
which measure concepts an artifact defines in prose against the concepts it
declares. Extraction covers the bold form, the numbered-heading form that hid
itc-org:Authority, and the concept-table form the kernel map uses; preserved
source under assimilation, seeds and incoming is excluded. Candidates are review
input, never ownership.

Baseline over 31 live artifacts: 113 concepts declared against 690 defined,
leaving 637 defined but undeclared, about 16 percent coverage. The workplan's
519 counted the bold form alone.

Two corrections to the workplan's framing, applied there. Thirteen artifacts
declare nothing rather than twelve: kernel/itc-core defines 57 concepts across
two forms and declares none, and it is the artifact every other artifact imports
from, so it goes first in T02. The gap also reaches further than obscure terms —
Actor is undeclared in the organization model although SecurityCanon imports it
from there by name against a pinned hash.

Three tests cover the extractor, one asserting that Authority appears in the
organization model's undeclared list, so the blind spot that produced finding
F-1 now has a regression test. make check passes with 49 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 3588@bnt-lap001
Assistant-Session: 24b80f66-e5a7-4e61-99fe-2d422e6d17da
This commit is contained in:
tegwick 2026-09-20 23:06:09 +02:00
parent d5704f1c4d
commit 2a59d3f77c
5 changed files with 305 additions and 10 deletions

View file

@ -122,6 +122,11 @@ def build_parser() -> argparse.ArgumentParser:
freshness.set_defaults(handler=_check_generated)
inventory = sub.add_parser("scope-inventory", help="Derive scope counts from the artifact registry")
inventory.set_defaults(handler=_scope_inventory)
concepts = sub.add_parser(
"concept-coverage",
help="Measure declared concepts against candidates defined in artifact prose")
concepts.add_argument("--artifact", help="Limit the report to one artifact id")
concepts.set_defaults(handler=_concept_coverage)
bundle = sub.add_parser("export-emission-contract", help="Export a content-addressed contract tar")
bundle.add_argument("destination")
bundle.set_defaults(handler=_export_emission)
@ -245,6 +250,17 @@ def _scope_inventory(args):
return dict(scope_inventory(load_context(_root(args))), ok=True)
def _concept_coverage(args):
from .maintenance import concept_candidates
from .service import load_context
report = concept_candidates(load_context(_root(args)))
if getattr(args, "artifact", None):
report = dict(report, artifacts=[item for item in report["artifacts"]
if item["artifact"] == args.artifact])
return dict(report, ok=True)
def _export_emission(args):
from .maintenance import export_emission_bundle
from .paths import infospace_root

View file

@ -7,6 +7,7 @@ import hashlib
import io
import json
from pathlib import Path
import re
import shutil
import statistics
import subprocess
@ -106,3 +107,96 @@ def benchmark_reads(root: Path, runs: int = 10) -> dict:
return {"ok": True, "operation": "inspect", "runs": runs,
"median_ms": statistics.median(samples), "max_ms": max(samples),
"cache": "none; benchmark does not establish a production latency requirement"}
CONCEPT_BOLD = re.compile(
r"^(?:A |An |The )?\*\*(?P<name>[A-Z][A-Za-z0-9 /\-]{2,60}?)\*\*"
r"\s*(?:—|-|is|are|defines|denotes|identifies|represents)\b"
)
CONCEPT_HEADING = re.compile(r"^#{2,4}\s+\d+(?:\.\d+)*\.?\s+(?P<name>[A-Z][A-Za-z0-9 /\-]{2,60})\s*$")
CONCEPT_TABLE_HEADER = re.compile(r"^\|\s*(?:Concept|Term|Name|Entity)\s*\|", re.IGNORECASE)
CONCEPT_TABLE_ROW = re.compile(r"^\|\s*`?(?P<name>[A-Z][A-Za-z0-9 /\-]{2,60}?)`?\s*\|")
SOURCE_ONLY_PREFIXES = ("assimilation/", "seeds/", "incoming/")
def _heading_is_defined(lines: list[str], start: int, name: str) -> bool:
"""A numbered heading names a concept only if a definition follows it."""
pattern = re.compile(r"^(?:A |An |The )?\*\*" + re.escape(name) + r"\*\*")
return any(pattern.match(lines[offset].strip()) for offset in range(start + 1, min(start + 6, len(lines))))
def _extract_concepts(text: str) -> dict[str, list[str]]:
lines = text.splitlines()
found: dict[str, set[str]] = {"bold": set(), "heading": set(), "table": set()}
in_concept_table = False
for number, raw in enumerate(lines):
line = raw.strip()
if CONCEPT_TABLE_HEADER.match(line):
in_concept_table = True
continue
if in_concept_table:
if not line.startswith("|"):
in_concept_table = False
elif not set(line) <= set("|- :"):
match = CONCEPT_TABLE_ROW.match(line)
if match:
found["table"].add(match.group("name").strip())
continue
match = CONCEPT_BOLD.match(line)
if match:
found["bold"].add(match.group("name").strip())
continue
match = CONCEPT_HEADING.match(line)
if match and _heading_is_defined(lines, number, match.group("name").strip()):
found["heading"].add(match.group("name").strip())
return {form: sorted(names) for form, names in found.items()}
def concept_candidates(context) -> dict:
"""Measure declared concepts against candidates defined in artifact prose.
Candidates are review input, never ownership. A concept becomes owned by
being declared, not by being matched here.
"""
ownership = generation.concept_ownership(context)
declared_by_owner: dict[str, set[str]] = {}
for item in ownership["concepts"]:
declared_by_owner.setdefault(item["owner"], set()).add(
generation._normalize_concept(item["concept"])
)
artifacts = []
for artifact in sorted(context.infospace.artifacts, key=lambda item: item.id):
if artifact.path.startswith(SOURCE_ONLY_PREFIXES) or not artifact.path.endswith(".md"):
continue
path = context.infospace_root / artifact.path
if not path.exists():
continue
forms = _extract_concepts(path.read_text(encoding="utf-8"))
declared = declared_by_owner.get(artifact.id, set())
candidates = sorted({name for names in forms.values() for name in names})
undeclared = [name for name in candidates
if generation._normalize_concept(name) not in declared]
artifacts.append({
"artifact": artifact.id,
"path": artifact.path,
"declared_count": len(declared),
"declares_frontmatter": bool(
generation._frontmatter(path).get("owned_concepts")
),
"candidate_count": len(candidates),
"undeclared_count": len(undeclared),
"by_form": {form: len(names) for form, names in forms.items()},
"undeclared": undeclared,
})
silent = [item["artifact"] for item in artifacts
if not item["declares_frontmatter"] and item["candidate_count"]]
return {
"artifact_count": len(artifacts),
"declared_total": sum(item["declared_count"] for item in artifacts),
"candidate_total": sum(item["candidate_count"] for item in artifacts),
"undeclared_total": sum(item["undeclared_count"] for item in artifacts),
"silent_artifacts": sorted(silent),
"artifacts": artifacts,
}