Skip to content

Commit e18671f

Browse files
committed
Keep discovery publication compact with immutable external evidence
1 parent 4dc819b commit e18671f

3 files changed

Lines changed: 328 additions & 7 deletions

File tree

‎src/infermatrix_copilot/kb_service/init_feature_discovery.py‎

Lines changed: 139 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import os
1717
from pathlib import Path
1818
import re
19+
import tempfile
1920
import threading
2021
import unicodedata
2122

@@ -34,6 +35,7 @@
3435
MAX_ATTEMPTS = 4
3536
MAX_BASELINE_CHARS = 16000
3637
CATALOG_BOUNDARY_VERSION = "catalog-boundary-v1"
38+
COMPACT_REPORT_VERSION = "feature-discovery-compact-v1"
3739
RELATIONS = {"new", "implementation_supplement", "alias", "subcapability",
3840
"shared_component", "outdated", "unknown"}
3941
_SLUG = re.compile(r"[a-z0-9][a-z0-9-]{0,39}\Z")
@@ -110,6 +112,89 @@ def _json(value):
110112
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
111113

112114

115+
def _report_archive_root(state_dir):
116+
state_root = Path(state_dir).resolve()
117+
root = state_root / "init" / "artifacts" / "feature-discovery"
118+
if not root.resolve().is_relative_to(state_root):
119+
raise InitError("discovery report archive escapes its state directory")
120+
return root.resolve()
121+
122+
123+
def write_full_discovery_report(state_dir, report):
124+
"""Atomically create immutable full evidence outside publication files."""
125+
data = (json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n").encode()
126+
digest = hashlib.sha256(data).hexdigest()
127+
root = _report_archive_root(state_dir)
128+
root.mkdir(parents=True, exist_ok=True)
129+
path = root / (digest + ".json")
130+
with tempfile.NamedTemporaryFile(dir=root, delete=False) as handle:
131+
temporary = Path(handle.name)
132+
handle.write(data)
133+
handle.flush()
134+
os.fsync(handle.fileno())
135+
try:
136+
try:
137+
os.link(temporary, path) # Exclusive atomic creation, never replacement.
138+
except FileExistsError:
139+
pass
140+
if path.is_symlink() or path.read_bytes() != data:
141+
raise InitError("immutable discovery report artifact differs; restore its original bytes")
142+
finally:
143+
temporary.unlink()
144+
return {"path": str(path), "sha256": digest, "size_bytes": len(data)}
145+
146+
147+
def compact_discovery_report(report, artifact):
148+
keys = ("schema_version", "repo", "pin", "run_config", "complete", "done", "catalog_sha256",
149+
"feature_ids", "features", "owner_requests", "counts", "statistics", "scan_summary",
150+
"index_sha256", "limits", "historical_denominator", "facet_denominator", "pacing")
151+
compact = {key: deepcopy(report[key]) for key in keys if key in report}
152+
audit = report["catalog_boundary_audit"]
153+
compact["catalog_boundary_audit"] = {key: deepcopy(audit[key]) for key in ("identity", "done", "counts") if key in audit}
154+
compact["catalog_boundary_audit"]["candidate_count"] = len(audit.get("candidate_ids", []))
155+
compact.update(report_format=COMPACT_REPORT_VERSION, full_artifact=deepcopy(artifact),
156+
candidate_count=len(report.get("candidates", [])),
157+
unassociated_implementation_count=len(report.get("unassociated_implementation_paths", [])),
158+
unassociated_entry_lead_count=len(report.get("unassociated_entry_leads", [])))
159+
for key in ("failures", "scope_suggestions", "candidate_limit_task_ids", "invalid_candidates"):
160+
values = report.get(key, [])
161+
compact[key] = {"count": len(values), "examples": deepcopy(values[:5]),
162+
"complete_list_in_full_artifact": True}
163+
return compact
164+
165+
166+
def verify_full_discovery_report(state_dir, report, state):
167+
"""Check byte identity, containment and the genuine checkpoint binding."""
168+
try:
169+
if report.get("report_format") != COMPACT_REPORT_VERSION or state.get("report_format") != COMPACT_REPORT_VERSION:
170+
raise ValueError("compact report marker is missing")
171+
artifact = report["full_artifact"]
172+
if artifact != state.get("full_artifact") or set(artifact) != {"path", "sha256", "size_bytes"}:
173+
raise ValueError("report and checkpoint artifact bindings differ")
174+
digest, size = artifact["sha256"], artifact["size_bytes"]
175+
if not isinstance(digest, str) or not re.fullmatch(r"[0-9a-f]{64}", digest) \
176+
or type(size) is not int or size <= 0:
177+
raise ValueError("artifact hash or size is invalid")
178+
path = Path(artifact["path"])
179+
expected = _report_archive_root(state_dir) / (digest + ".json")
180+
if not path.is_absolute() or path != expected or path.is_symlink() or path.resolve() != expected:
181+
raise ValueError("artifact path escapes the immutable report archive")
182+
data = path.read_bytes()
183+
if len(data) != size or hashlib.sha256(data).hexdigest() != digest:
184+
raise ValueError("artifact bytes or hash differ")
185+
full = json.loads(data)
186+
if compact_discovery_report(full, artifact) != report:
187+
raise ValueError("compact report differs from its full artifact")
188+
for key in ("pin", "catalog_sha256", "index_sha256", "run_config"):
189+
if full.get(key) != state.get(key):
190+
raise ValueError("full report and checkpoint identity differ")
191+
if full.get("complete") is not True or full.get("done") is not True:
192+
raise ValueError("full report is unfinished")
193+
return full
194+
except (OSError, ValueError, TypeError, KeyError) as exc:
195+
raise InitError(f"discovery full report artifact could not be verified: {exc}") from exc
196+
197+
113198
def _hash(value):
114199
return hashlib.sha256(_json(value).encode()).hexdigest()
115200

@@ -765,13 +850,53 @@ def _chain(self):
765850
return super()._chain()
766851

767852
def _cache_reusable(self, previous):
768-
if previous.discovery.get("done") and self._validate_saved_archives(previous.discovery):
853+
audited = deepcopy(previous)
854+
if previous.discovery.get("done") and (self._validate_saved_archives(audited.discovery)
855+
or previous.discovery.get("catalog_boundary_audit", {}).get("done")
856+
and not audited.discovery.get("catalog_boundary_audit", {}).get("done")):
769857
raise InitError("completed discovery native archives are unavailable; restore the archive before reuse")
770858
if not previous.discovery.get("done") or self.retry_unfinished:
771859
return False
860+
if previous.discovery.get("report_format") == COMPACT_REPORT_VERSION:
861+
self._verify_report_record(previous)
862+
elif previous.status != "published":
863+
return False # Re-render saved unpublished results through the official stage.
772864
# Published historical catalogs remain immutable. Unpublished previews
773865
# must acquire the additive audit before publication, reusing their scan.
774-
return previous.status == "published" or self._boundary_record_ready(previous)
866+
return previous.status == "published" or self._boundary_record_ready(audited)
867+
868+
def _verify_report_record(self, record, files=None):
869+
from .init_stages import _dry_run_files
870+
from .init_support import load_prepared
871+
if files is None:
872+
if record.pr.get("prepared"):
873+
files = load_prepared(record.pr["prepared"])["files"]
874+
elif record.pr.get("dry_run_dir"):
875+
files = _dry_run_files(record)
876+
else:
877+
files = {path: self.rt.knowledge.show(record.kb_base_sha, path) for path in
878+
(record.discovery["report_path"], self._coverage_policy_path())}
879+
text = files.get(record.discovery["report_path"])
880+
catalog = files.get(self._coverage_policy_path())
881+
if catalog is None:
882+
catalog = self.rt.knowledge.show(record.kb_base_sha, self._coverage_policy_path())
883+
if not isinstance(text, str) or hashlib.sha256(text.encode()).hexdigest() != record.discovery.get("report_sha256") \
884+
or not isinstance(catalog, str) or hashlib.sha256(catalog.encode()).hexdigest() != record.discovery.get("catalog_sha256"):
885+
raise InitError("discovery checkpoint and compact catalog/report bytes differ")
886+
try:
887+
report = json.loads(text)
888+
except ValueError as exc:
889+
raise InitError("discovery compact report is not JSON") from exc
890+
if not isinstance(report, dict) or report.get("repo") != record.repo or report.get("pin") != record.pin:
891+
raise InitError("discovery compact report identity differs")
892+
return verify_full_discovery_report(self.rt.state_dir, report, record.discovery)
893+
894+
def _publish(self, changed):
895+
try:
896+
self._verify_report_record(self.record, changed)
897+
except InitError as exc:
898+
return self._blocked([str(exc)])
899+
return super()._publish(changed)
775900

776901
def _boundary_record_ready(self, previous):
777902
state = previous.discovery
@@ -811,6 +936,10 @@ def _resume_input_problems(self, previous, digest):
811936
if not self._boundary_record_ready(audited):
812937
return ["prepared discovery has no complete catalog boundary audit; preserve its publication and checkpoint "
813938
"and audit a copy before creating a new publication"]
939+
try:
940+
self._verify_report_record(previous)
941+
except InitError as exc:
942+
return [str(exc)]
814943
return []
815944

816945
def _init_identity(self):
@@ -1139,8 +1268,15 @@ def save():
11391268
"limits": "Declared inventory processed; not proof all repository capabilities were found. Tests not executed.",
11401269
"historical_denominator": len(seeds) * 7, "facet_denominator": len(features) * 7}
11411270
report_path = f"eval/feature-discovery/{self.lifecycle.repo}-{self.record.pin[:12]}.json"
1142-
report_text = json.dumps(report, ensure_ascii=False, indent=2) + "\n"
1271+
try:
1272+
if _report_archive_root(self.rt.state_dir).is_relative_to(Path(self.rt.knowledge.path).resolve()):
1273+
raise InitError("full discovery reports require a state directory outside the knowledge checkout")
1274+
artifact = write_full_discovery_report(self.rt.state_dir, report)
1275+
except (InitError, OSError) as exc:
1276+
return self._blocked([str(exc)])
1277+
report_text = json.dumps(compact_discovery_report(report, artifact), ensure_ascii=False, indent=2) + "\n"
11431278
state.update(done=True, pin=self.record.pin, catalog_sha256=catalog_sha, report_path=report_path,
1279+
report_format=COMPACT_REPORT_VERSION, full_artifact=artifact,
11441280
report_sha256=hashlib.sha256(report_text.encode()).hexdigest())
11451281
other = {path: (original_text, rendered), report_path: (self.rt.knowledge.show(self._base_sha, report_path), report_text)}
11461282

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Compact publication preserves full evidence in an immutable external file."""
2+
from copy import deepcopy
3+
import hashlib
4+
import json
5+
from pathlib import Path
6+
7+
import pytest
8+
9+
from infermatrix_copilot.kb_service.init_feature_discovery import (
10+
compact_discovery_report, write_full_discovery_report, verify_full_discovery_report,
11+
)
12+
from infermatrix_copilot.kb_service.init_support import InitError
13+
14+
15+
def full_report():
16+
return {'schema_version': 1, 'repo': 'library', 'pin': 'a' * 40,
17+
'complete': True, 'done': True, 'catalog_sha256': 'b' * 64,
18+
'index_sha256': 'c' * 64, 'run_config': {'packet_chars': 96000},
19+
'feature_ids': ['library'], 'features': [{'id': 'library', 'owner': 'core', 'title': 'Library'}],
20+
'owner_requests': [], 'counts': {'unknown': 5000}, 'statistics': {'unknown': 5000},
21+
'catalog_boundary_audit': {'done': True, 'identity': {'version': 'boundary-v1'},
22+
'candidate_ids': ['proposal'] * 5000, 'counts': {'yes': 5000}},
23+
'candidates': [{'id': f'proposal-{n}', 'description': 'evidence ' * 250,
24+
'reason': 'unknown ' * 250, 'generator_receipts': [{'trace_id': 'native'}],
25+
'evidence': [{'path': f'src/file-{n}.py', 'start': 1, 'end': 2}]} for n in range(5000)],
26+
'unassociated_implementation_paths': [f'src/unmapped-{n}.py' for n in range(5000)],
27+
'unassociated_entry_leads': [{'path': f'src/unmapped-{n}.py', 'symbols': ['entry']} for n in range(5000)],
28+
'failures': [], 'scope_suggestions': [{'path': f'outside/file-{n}.py'} for n in range(5000)],
29+
'candidate_limit_task_ids': [f'source:{n}' for n in range(500)],
30+
'invalid_candidates': [{'reason': 'invalid citation'} for n in range(5000)]}
31+
32+
33+
def test_large_report_is_compact_and_full_bytes_are_preserved(tmp_path):
34+
full = full_report()
35+
artifact = write_full_discovery_report(tmp_path, full)
36+
compact = compact_discovery_report(full, artifact)
37+
assert len(json.dumps(compact, ensure_ascii=False).encode()) < 100_000
38+
assert artifact['size_bytes'] > 10_000_000
39+
assert compact['candidate_count'] == 5000 and 'candidates' not in compact
40+
assert compact['catalog_boundary_audit']['candidate_count'] == 5000
41+
assert 'candidate_ids' not in compact['catalog_boundary_audit']
42+
assert compact['scope_suggestions']['count'] == 5000
43+
state = {**{k: full[k] for k in ('pin', 'catalog_sha256', 'index_sha256', 'run_config')},
44+
'report_format': compact['report_format'], 'full_artifact': artifact}
45+
assert verify_full_discovery_report(tmp_path, compact, state) == full
46+
raw = Path(artifact['path']).read_bytes()
47+
assert hashlib.sha256(raw).hexdigest() == artifact['sha256']
48+
assert write_full_discovery_report(tmp_path, full) == artifact
49+
assert Path(artifact['path']).read_bytes() == raw
50+
51+
52+
@pytest.mark.parametrize('damage', ['escape', 'symlink', 'checkpoint_binding', 'summary', 'size', 'identity'])
53+
def test_full_report_binding_and_containment_refuse_tampering(tmp_path, damage):
54+
full = full_report(); full['candidates'] = []
55+
artifact = write_full_discovery_report(tmp_path, full)
56+
compact = compact_discovery_report(full, artifact)
57+
state = {**{k: full[k] for k in ('pin', 'catalog_sha256', 'index_sha256', 'run_config')},
58+
'report_format': compact['report_format'], 'full_artifact': deepcopy(artifact)}
59+
if damage == 'escape':
60+
outside = tmp_path / 'elsewhere.json'; outside.write_bytes(Path(artifact['path']).read_bytes())
61+
compact['full_artifact']['path'] = str(outside); state['full_artifact'] = deepcopy(compact['full_artifact'])
62+
elif damage == 'symlink':
63+
path = Path(artifact['path']); outside = tmp_path / 'elsewhere.json'
64+
path.rename(outside); path.symlink_to(outside)
65+
elif damage == 'checkpoint_binding':
66+
state['full_artifact']['sha256'] = 'd' * 64
67+
elif damage == 'summary':
68+
compact['counts']['unknown'] = 0
69+
elif damage == 'size':
70+
compact['full_artifact']['size_bytes'] += 1; state['full_artifact'] = deepcopy(compact['full_artifact'])
71+
else:
72+
state['catalog_sha256'] = 'd' * 64
73+
with pytest.raises(InitError, match='full report artifact'):
74+
verify_full_discovery_report(tmp_path, compact, state)

0 commit comments

Comments
 (0)