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:
1 parent
6d0d455033
commit
24d14ff8e1
6 files changed
+570
-372
No files matched your search
+16
-1
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
Reference in new issue
Block a user