fix(spine): preserve candidate discoveries without inventing module IDs (#2465)

Migrate 22 malformed contract-discovery IDs to construction-surface, retain evidence/edges, keep candidates DECLARED, unify cold/warm language projection, and refresh the pinned aiohttp lock. Red regression tests precede the fix; local full suite 2301 passed, spine canaries 19/19 and 21/21.
This commit is contained in:
Nick committed 2026-10-05 22:14:46 +01:00
1 parent 6d0d455033
commit 24d14ff8e1
6 files changed
+570 -372

No files matched your search

+16 -1
View File
@@ -272,4 +272,19 @@ observations, fresh resume.
Release-note generator: LIVE-PROVEN (real Gitea, 10 releases). Backfill:
0.66.35→0.66.44 carrying historical-backfill provenance. Pre-0.66.35
history: skipped (weak evidence — honest limitation).
history: skipped (weak evidence — honest limitation).
## Contract discovery → spine (2026-10-05, #2465 CI correction)
The closed construction contract is the authority for discoveries. Spine
rebuild reads its observed entities and candidate corrections; it does not
claim a correction has been built. Qualified IDs such as
`rust-crate:cognition_core` keep their declared kind. Bare file paths,
test suites and other unqualified references become
`construction-surface:<reference>`, never `module`. An observed
surface may be PRESENT; a candidate stays DECLARED until independently
observed. Existing bad records are migrated with their evidence and edge
references intact. This matters: the first write-back mapped bare paths to
modules, made 22 false module records and crashed CI on the next rebuild.
The per-contract quick-reference markdown is generated from the scaffold
store at close; it is a view, not another authority.
+1 -1
View File
@@ -2,7 +2,7 @@
# uv pip compile pyproject.toml -o requirements-ci.txt --extra dev --extra memory --python-version 3.11
aiohappyeyeballs==2.7.1
# via aiohttp
aiohttp==3.14.3
aiohttp==3.14.4
# via rune (pyproject.toml)
aiosignal==1.4.0
# via aiohttp
+49 -15
View File
@@ -382,6 +382,40 @@ def _place_languages(doc: dict) -> int:
return placed
def _normalize_discovery_ids(doc: dict) -> int:
"""Migrate the old unqualified contract references, preserving evidence.
A file path (or 'suite') is not a module ID. Only migrate the malformed
module records produced by the earlier discovery projector; leave all
other open-world entity classes alone.
"""
renamed = {}
observed = {
eid for _, entities in _contract_discoveries()
for eid, _, provenance in entities if provenance == "observed"
}
for e in doc.get("entities", []):
if (e.get("kind") == "module" and ":" not in e.get("id", "")
and any(ref.startswith("contract-discovery:")
for ref in e.get("evidence_refs", []))):
old = e["id"]
e["id"] = f"construction-surface:{old}"
e["kind"] = "construction-surface"
e.setdefault("identities", {})["source_ref"] = old
renamed[old] = e["id"]
if e.get("kind") == "construction-surface" and e.get("id", "").startswith("construction-surface:"):
# Earlier code promoted every *candidate* to PRESENT. Undo only
# that unsupported claim; an actually observed surface retains it.
if e["id"].split(":", 1)[1] not in observed:
e["maturity"] = "DECLARED"
e["tags"] = [tag for tag in e.get("tags", []) if tag != "code"]
for rel in doc.get("relationships", []):
for endpoint in ("from", "to"):
if rel.get(endpoint) in renamed:
rel[endpoint] = renamed[rel[endpoint]]
return len(renamed)
def _apply_contract_discoveries(doc: dict) -> int:
"""Contract-discovery write-back (issue #2465 addition 2): entities
DISCOVERED by closed contracts become modeled spine entities — the
@@ -390,12 +424,15 @@ def _apply_contract_discoveries(doc: dict) -> int:
contracts write back."""
placed = 0
for cid, entities in _contract_discoveries():
for eid, kind in entities:
for eid, kind, provenance in entities:
before = len(doc.get("entities", []))
if kind == "construction-surface":
eid = f"construction-surface:{eid}"
add_entity(doc, id=eid, kind=kind,
tags=["code", "contract-discovery"],
tags=["contract-discovery"],
identities={"source_ref": eid.split(":", 1)[1]} if kind == "construction-surface" else None,
evidence_refs=[f"contract-discovery:{cid}"],
maturity="PRESENT")
maturity="PRESENT" if provenance == "observed" else "DECLARED")
if len(doc.get("entities", [])) > before:
placed += 1
else:
@@ -420,15 +457,17 @@ def _contract_discoveries():
found = []
# expected-new surfaces observed at close (REAL delivered entities)
for eid in q.get("expected_new_observed", []):
kind = eid.split(":", 1)[0] if ":" in eid else "module"
kind = eid.split(":", 1)[0] if ":" in eid else "construction-surface"
if kind in ("rust-crate", "go-surface", "config", "workflow", "module"):
found.append((eid, kind))
found.append((eid, kind, "observed"))
elif kind == "construction-surface":
found.append((eid, kind, "observed"))
# candidate corrections: the closure flagged them for modeling
for cand in q.get("candidate_corrections", []):
eid = cand.get("entity", "")
kind = eid.split(":", 1)[0] if ":" in eid else "module"
if kind in ("rust-crate", "go-surface", "config", "workflow", "module"):
found.append((eid, kind))
kind = eid.split(":", 1)[0] if ":" in eid else "construction-surface"
if eid and kind in ("rust-crate", "go-surface", "config", "workflow", "module", "construction-surface"):
found.append((eid, kind, "candidate"))
if found:
out.append((cid[9:], found))
return out
@@ -439,6 +478,8 @@ def _gather_build_inputs(manifest_path: Path | None = None) -> dict:
Python scans, the core — Rust or Python — only assembles). Both cores
consume this dict, so parity divergence is logic, never environment."""
existing = load()
if existing:
_normalize_discovery_ids(existing)
seeded = bool(existing) and any(
r["type"] == "belongs_to" for r in existing["relationships"])
# ingest manifest: only consulted when the unseeded path needs it
@@ -615,13 +656,6 @@ def _py_build_from_inputs(inputs: dict) -> dict:
evidence_refs=[f"umbrella-manifest#{name}"])
# 6. language placement (issue #2465 addition 3): every entity carries a
# language verdict — rune units get the GO/RUST dichotomy; other
# projects get a fit assessment. The verdict comes from
# construction_decision.language_placement (the protocol's own
# intelligence — the Rust language.rs kernel when built).
_place_languages(doc)
# reconcile: facts owned by projectors that no longer observe them
# become STALE (marked, never deleted — history survives; open-world
# preservation applies to everything, staleness is the truth marker)
+475 -355
View File
File diff suppressed because it is too large. Load diff
+25
View File
@@ -17,6 +17,31 @@ sys.path.insert(0, str(ROOT / "src"))
from rune import spine
def test_contract_discovery_preserves_unqualified_surface_without_inventing_module(monkeypatch):
monkeypatch.setattr(spine, "_contract_discoveries", lambda: [
("example", [("src/rune/audit_ledger.py", "construction-surface", "candidate"),
("rust-crate:cognition_core", "rust-crate", "observed")])])
doc = spine.new_doc()
spine._apply_contract_discoveries(doc)
found = {e["id"]: e for e in doc["entities"]}
assert found["construction-surface:src/rune/audit_ledger.py"]["kind"] == "construction-surface"
assert found["construction-surface:src/rune/audit_ledger.py"]["maturity"] == "DECLARED"
assert found["rust-crate:cognition_core"]["maturity"] == "PRESENT"
def test_legacy_unqualified_discoveries_are_normalized_without_losing_evidence():
doc = spine.new_doc()
spine.add_entity(doc, id="src/rune/audit_ledger.py", kind="module",
maturity="PRESENT", evidence_refs=["contract-discovery:example"])
spine.add_relationship(doc, type="depends_on", from_id="src/rune/audit_ledger.py",
to_id="module:other", evidence_refs=["test"])
spine._normalize_discovery_ids(doc)
assert doc["entities"][0]["id"] == "construction-surface:src/rune/audit_ledger.py"
assert doc["entities"][0]["maturity"] == "DECLARED"
assert doc["entities"][0]["evidence_refs"] == ["contract-discovery:example"]
assert doc["relationships"][0]["from"] == "construction-surface:src/rune/audit_ledger.py"
# ── helpers ───────────────────────────────────────────────────────────────
@pytest.fixture
+4
View File
@@ -211,4 +211,8 @@ class TestSpineExtractionNeutrality:
"go-surface",
"config",
"workflow",
# Closed contracts may name files or validation surfaces that
# are not Python modules. Keep their provenance without lying
# about their module identity (issue #2465 CI regression).
"construction-surface",
}