net-kingdom published the first source-owned declaration (116643f). It passes emission-review. Record it as feedback, and file its session-scoped-silence incompatibility as demand/EmissionActivityScope.md. T02 is done, and T04 is in progress until activity-core answers. The adoption brief sent owners the 0.1.0 draft digest, because the candidate promotion changed the standard's text after the digest was taken. The export manifest also hard-coded status "draft". The manifest now derives status and version from the standard, a test pins that, and the brief names the candidate digest b08b4d95fc4b0bd3. The wire schema is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 121421@bnt-lap001 Assistant-Session: 36f8657c-ebfa-4a2e-9ba0-bff06738f233
209 lines
9.5 KiB
Python
209 lines
9.5 KiB
Python
"""Reproducible projections, portable contract exports, and read measurements."""
|
|
|
|
from collections import Counter
|
|
from dataclasses import replace
|
|
from datetime import datetime, timezone
|
|
import hashlib
|
|
import io
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import shutil
|
|
import statistics
|
|
import subprocess
|
|
import tarfile
|
|
import tempfile
|
|
import time
|
|
|
|
from . import generation
|
|
|
|
|
|
def scope_inventory(context) -> dict:
|
|
return {"artifact_count": len(context.infospace.artifacts),
|
|
"kinds": dict(sorted(Counter(a.kind for a in context.infospace.artifacts).items())),
|
|
"standards": sorted(a.id for a in context.infospace.artifacts if a.kind == "standard"),
|
|
"models": sorted(a.id for a in context.infospace.artifacts if a.kind == "model")}
|
|
|
|
|
|
def check_generated(context) -> dict:
|
|
"""Render only into a disposable copy; never repair the source as a check."""
|
|
with tempfile.TemporaryDirectory(prefix="canon-freshness-") as temporary:
|
|
target = Path(temporary) / "infospace"
|
|
shutil.copytree(context.infospace_root, target)
|
|
copied = replace(context, infospace_root=target)
|
|
files = {}
|
|
for render in (generation.generate_indexes, generation.generate_tree,
|
|
generation.generate_agent_briefs):
|
|
for item in render(copied)["files"]:
|
|
path = Path(item["path"])
|
|
files[str(path.relative_to(target))] = path.read_bytes()
|
|
stale = []
|
|
for relative, expected in files.items():
|
|
original = context.infospace_root / relative
|
|
if not original.is_file() or original.read_bytes() != expected:
|
|
stale.append(relative)
|
|
return {"ok": not stale, "checked": len(files), "stale": sorted(stale)}
|
|
|
|
|
|
def source_evidence(root: Path) -> dict:
|
|
digest = hashlib.sha256()
|
|
# Include source and generated corpus; exclude reports to avoid self-hashing.
|
|
for path in sorted(root.rglob("*")):
|
|
if path.is_file() and "validation" not in path.relative_to(root).parts:
|
|
digest.update(str(path.relative_to(root)).encode() + b"\0" + path.read_bytes() + b"\0")
|
|
def git(*args):
|
|
try:
|
|
result = subprocess.run(["git", "-C", str(root.parent), *args],
|
|
capture_output=True, text=True, check=False)
|
|
except FileNotFoundError:
|
|
return None
|
|
return result.stdout.strip() if result.returncode == 0 else None
|
|
status = git("status", "--porcelain")
|
|
return {"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
"revision": git("rev-parse", "HEAD"),
|
|
"dirty": bool(status) if status is not None else None,
|
|
"corpus_sha256": digest.hexdigest(),
|
|
"digest_scope": "all infospace files except validation reports"}
|
|
|
|
|
|
def export_emission_bundle(root: Path, destination: Path) -> dict:
|
|
"""Content-addressed deterministic tar; a changed contract gets a new name."""
|
|
paths = ["schemas/emission-cadence.schema.yaml",
|
|
"standards/emission-cadence/InfoTechCanonEmissionCadenceStandard.md",
|
|
"standards/emission-cadence/examples/qonto-assistant.yaml"]
|
|
files = {path: (root / path).read_bytes() for path in paths}
|
|
# Status and version come from the standard itself, so a promotion cannot
|
|
# leave the manifest describing an earlier maturity.
|
|
standard = files[paths[1]].decode()
|
|
header = re.match(r"---\n(.*?)\n---", standard, re.S)
|
|
fields = dict(re.findall(r"^(status|version):\s*(\S+)", header.group(1) if header else "", re.M))
|
|
manifest = {"contract": "emission-cadence/0.1", "status": fields.get("status"),
|
|
"document_version": fields.get("version"),
|
|
"files": {name: hashlib.sha256(data).hexdigest() for name, data in files.items()},
|
|
"semantic_checks": ["unique source_id in sources"],
|
|
"adoption_evidence": ("Stable requires two source-owned declarations from independent owners "
|
|
"and one observer result from a third party; example does not count.")}
|
|
files["manifest.json"] = (json.dumps(manifest, sort_keys=True, indent=2) + "\n").encode()
|
|
output = io.BytesIO()
|
|
with tarfile.open(fileobj=output, mode="w", format=tarfile.USTAR_FORMAT) as archive:
|
|
for name, data in sorted(files.items()):
|
|
info = tarfile.TarInfo(name)
|
|
info.size = len(data)
|
|
info.mode = 0o644
|
|
archive.addfile(info, io.BytesIO(data))
|
|
data = output.getvalue()
|
|
digest = hashlib.sha256(data).hexdigest()
|
|
destination.mkdir(parents=True, exist_ok=True)
|
|
path = destination / f"emission-cadence-0.1-{digest}.tar"
|
|
try:
|
|
with path.open("xb") as stream:
|
|
stream.write(data)
|
|
except FileExistsError:
|
|
if path.read_bytes() != data:
|
|
raise ValueError(f"Refusing to overwrite different bundle contents: {path}")
|
|
return {"ok": True, "path": str(path), "sha256": digest, "manifest": manifest}
|
|
|
|
|
|
def benchmark_reads(root: Path, runs: int = 10) -> dict:
|
|
from .service import inspect_canon
|
|
samples = []
|
|
for _ in range(runs):
|
|
started = time.perf_counter()
|
|
inspect_canon(root)
|
|
samples.append((time.perf_counter() - started) * 1000)
|
|
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,
|
|
}
|