diff --git a/.github/workflows/semantic-ring-oracle.yml b/.github/workflows/semantic-ring-oracle.yml new file mode 100644 index 000000000..51271d3f4 --- /dev/null +++ b/.github/workflows/semantic-ring-oracle.yml @@ -0,0 +1,33 @@ +name: Semantic ring reference oracle + +on: + pull_request: + paths: + - '.github/workflows/semantic-ring-oracle.yml' + - 'docs/research/versioned-semantic-candidates-fixtures.json' + - 'docs/research/semantic-ring-bound-gold-v1.json' + - 'tools/semantic_ring_oracle/**' + push: + branches: [main] + paths: + - '.github/workflows/semantic-ring-oracle.yml' + - 'docs/research/versioned-semantic-candidates-fixtures.json' + - 'docs/research/semantic-ring-bound-gold-v1.json' + - 'tools/semantic_ring_oracle/**' + +permissions: + contents: read + +jobs: + reference-oracle: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Assert C-01/C-02 and negative controls + run: | + python3 tools/semantic_ring_oracle/ring_reference.py + python3 -m unittest discover -s tools/semantic_ring_oracle -p 'test_*.py' -v diff --git a/docs/research/semantic-ring-bound-gold-v1.json b/docs/research/semantic-ring-bound-gold-v1.json new file mode 100644 index 000000000..9d08e8847 --- /dev/null +++ b/docs/research/semantic-ring-bound-gold-v1.json @@ -0,0 +1,72 @@ +{ + "format": "lance-graph-semantic-ring-bound-gold/v2", + "status": "manually-bound-symbolic-test-gold-not-production-parser-output", + "scope": "C-01/C-02: identities remain stable; a sealed version witnesses an identity assertion at its claim address", + "sentences": [ + "That key opens a door.", + "That door hides a garden.", + "That garden shelters a fox.", + "That fox carries a key." + ], + "facts": [ + { + "sentence_index": 0, + "subject": "first_sentence.key", + "predicate": "opens", + "object": "door" + }, + { + "sentence_index": 1, + "subject": "door", + "predicate": "hides", + "object": "garden" + }, + { + "sentence_index": 2, + "subject": "garden", + "predicate": "shelters", + "object": "fox" + }, + { + "sentence_index": 3, + "subject": "fox", + "predicate": "carries", + "object": "last_sentence.key" + } + ], + "read_heads": { + "C-01": { + "dataset": "symbolic-ring", + "branch": "main", + "version": 17 + }, + "C-02": { + "dataset": "symbolic-ring", + "branch": "main", + "version": 16 + } + }, + "attested_identity": [ + { + "lhs": "last_sentence.key", + "rhs": "first_sentence.key", + "claim_address": "corefers(last_sentence.key,first_sentence.key)", + "sealed_in": { + "dataset": "symbolic-ring", + "branch": "main", + "version": 17 + }, + "source_roots": [ + "independent_coref_witness_1" + ] + } + ], + "caveats": [ + "Identifiers here are fixed manually authored symbolic claim/referent coordinates, not runtime-minted NodeGuids.", + "The version of a claim at its thought address is its historical witness; no separate witness entity or foreign-key lookup is created.", + "The source root is separately retained in the version receipt for evidential independence. Neither the thought slot nor the version number by itself proves that sources are independent.", + "An attestation being present in a version witnesses the assertion in the test history; it does not establish externally observed coreference truth.", + "Read horizons use dataset + branch + version, never a naked version or latest/unbounded temporal default.", + "No production R2IL relation join, CE64 truth promotion, actual Lance manifest or real-world source attestation is claimed." + ] +} diff --git a/tools/semantic_ring_oracle/README.md b/tools/semantic_ring_oracle/README.md new file mode 100644 index 000000000..cf691d699 --- /dev/null +++ b/tools/semantic_ring_oracle/README.md @@ -0,0 +1,58 @@ +# C-01/C-02 semantic ring reference oracle + +**Test-only independent oracle. Not the Rust semantic candidate runtime.** + +This experiment is a versioned identity assertion, not a newly minted +'witness object'. The four bound SPO relations live at fixed symbolic +identities. The identity assertion itself has a **fixed claim address** +`corefers(last_sentence.key,first_sentence.key)`. Its **sealed version** is +the witness of its admission: + +- C-02 reads `(symbolic-ring, main, v16)`: the claim does not yet exist. + Four sentences look cyclic, but their distinct key identities make the + cycle open. +- C-01 reads `(symbolic-ring, main, v17)`: the exact same semantic identities + are read; version 17 has witnessed the added identity assertion, and the + cycle closes in all four rotations. +- Re-reading C-02 at v17 closes the cycle **without reminting any identity**. + An older horizon stays unresolved even if its narrative says a witness + exists. Version visibility is not inferred from a source-root label. + +This directly follows the D-TGA decision in +`.claude/plans/thought-git-api-v1.md`: a sealed version is the PR entry; +`blame` returns a landing version plus original source bits. The active +thought slot `0..63` is recyclable. The history witness is the claim's +**thought address at the sealed version**, not a reusable slot number, an +external pointer, a new witness NodeGuid or a bare numeric Lance version. +The tuple in this **symbolic** fixture includes dataset and branch because +real durable versions need those to disambiguate identity. + +**Evidence root != witness coordinate.** Independent source roots are +metadata in the version receipt for NAL/Gadamer revision. Three thoughts +copying one observation do not yield three independent sources, and neither +a version number nor an address bit alone proves a claim *true*. The fixture +contains a synthetic source root, not external proof of real-world coreference. + +The preregistered C-01/C-02 source JSON in +`docs/research/versioned-semantic-candidates-fixtures.json` remains unchanged. +Its older `external_identity_witness` field is retained for traceability +but is **not** the executable witness index. The new v2 gold is held in +`docs/research/semantic-ring-bound-gold-v1.json` (original path retained), +with explicitly pinned read horizons and a sealed claim-address receipt. + +```sh +python3 tools/semantic_ring_oracle/ring_reference.py +python3 -m unittest discover -s tools/semantic_ring_oracle -p 'test_*.py' -v +``` + +The reference never consults `case.expected` during evaluation. Controls +include four rotations, pre/post version, wrong dataset/branch, spurious +root labels, wrong claim address, missing version, predicate mutation, +truncated facts and source-root de-duplication. + +**Next integration gate:** have the actual DeepNSM/UD binder produce +canonical bound claim addresses and their stamped, versioned thought writes +(or `Unbound`), then differential-check real mask/R2IL execution against this +reference. No parser, real-world evidence authentication, NodeGuid allocation, +Lance13 integration, generalized relational join, causal inference or +production C-01/C-02 semantic closure is claimed here. diff --git a/tools/semantic_ring_oracle/ring_reference.py b/tools/semantic_ring_oracle/ring_reference.py new file mode 100644 index 000000000..983243b88 --- /dev/null +++ b/tools/semantic_ring_oracle/ring_reference.py @@ -0,0 +1,152 @@ +"""Independent symbolic C-01/C-02 reference, never a production graph evaluator. + +Identity is the fixed coordinate. A sealed version witnesses that the +coreference claim was recorded at that coordinate. Source roots remain a +separate evidential property of the version receipt, not another ID. +This harness does NOT parse language, mint NodeGuids, or execute R2IL. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parents[2] +FIXTURES = ROOT / "docs/research/versioned-semantic-candidates-fixtures.json" +GOLD = ROOT / "docs/research/semantic-ring-bound-gold-v1.json" + + +def load_case(case_id: str) -> dict[str, Any]: + fixtures = json.loads(FIXTURES.read_text(encoding="utf-8")) + if fixtures["format"] != "lance-graph-versioned-semantic-candidates/fixtures-v1": + raise ValueError("unexpected fixture version") + matches = [c for c in fixtures["cases"] if c["id"] == case_id] + if len(matches) != 1: + raise ValueError(f"expected one fixture for {case_id}") + return matches[0] + + +def load_gold() -> dict[str, Any]: + gold = json.loads(GOLD.read_text(encoding="utf-8")) + if gold["format"] != "lance-graph-semantic-ring-bound-gold/v2": + raise ValueError("unexpected gold version") + return gold + + +def _validated_ref(ref: dict[str, Any]) -> tuple[str, str, int]: + if not isinstance(ref, dict) or set(ref) != {"dataset", "branch", "version"}: + raise ValueError("read reference must pin dataset, branch and version") + dataset, branch, version = ref["dataset"], ref["branch"], ref["version"] + if not isinstance(dataset, str) or not dataset or not isinstance(branch, str) or not branch: + raise ValueError("read reference has unbound dataset/branch") + if type(version) is not int or version < 0: + raise ValueError("read reference version must be an explicit nonnegative integer") + return dataset, branch, version + + +def evaluate( + case: dict[str, Any], + gold: dict[str, Any], + entry_order: list[int], + *, + facts: list[dict[str, Any]] | None = None, + read_ref: dict[str, Any] | None = None, +) -> dict[str, Any]: + """A narrow independent reference using pinned versions, never case.expected. + + The C-01/C-02 legacy 'external_identity_witness' inputs stay frozen as + originally preregistered. They do NOT control admission. Instead, the + recorded claim at its fixed address becomes visible at its sealed version. + """ + if case["id"] not in ("C-01", "C-02"): + raise ValueError("this oracle covers only C-01/C-02") + if case["input"]["sentences"] != gold["sentences"]: + raise ValueError("sentence/gold binding drift") + if entry_order not in case["input"]["entry_orders"] or sorted(entry_order) != list(range(4)): + raise ValueError("unregistered or incomplete rotation") + if read_ref is None: + read_ref = gold["read_heads"][case["id"]] + dataset, branch, version = _validated_ref(read_ref) + + atoms = list(gold["facts"] if facts is None else facts) + if len({a["sentence_index"] for a in atoms}) != len(atoms): + raise ValueError("multiple atoms per sentence exceed this narrow oracle") + if any(a["sentence_index"] not in range(4) for a in atoms): + raise ValueError("unbound source sentence") + selected = sorted(atoms, key=lambda a: entry_order.index(a["sentence_index"])) + + redirects: dict[str, str] = {} + witnessed: list[tuple[str, str, str, int]] = [] + roots: set[str] = set() + for assertion in gold["attested_identity"]: + lhs, rhs = assertion["lhs"], assertion["rhs"] + # This symbol is a fixed, manually bound *claim* address, never a + # newly minted witness ID. The assertion is located by address. + if assertion["claim_address"] != f"corefers({lhs},{rhs})": + raise ValueError("identity assertion at wrong claim address") + a_dataset, a_branch, a_version = _validated_ref(assertion["sealed_in"]) + if (a_dataset, a_branch) != (dataset, branch) or a_version > version: + continue + if lhs in redirects and redirects[lhs] != rhs: + raise ValueError("conflicting visible identity assertions") + redirects[lhs] = rhs + witnessed.append((assertion["claim_address"], a_dataset, a_branch, a_version)) + roots.update(assertion["source_roots"]) + + def canonical(term: str) -> str: + seen: set[str] = set() + while term in redirects: + if term in seen: + raise ValueError("cycle in identity assertions") + seen.add(term) + term = redirects[term] + return term + + relations = tuple( + sorted((canonical(a["subject"]), a["predicate"], canonical(a["object"])) + for a in selected) + ) + # The test-only traversal checks that the recorded four bound SPO facts + # are a complete cycle, not whether a parser or Datalog join found them. + adjacency: dict[str, list[tuple[str, int]]] = {} + for atom in selected: + adjacency.setdefault(canonical(atom["subject"]), []).append( + (canonical(atom["object"]), atom["sentence_index"]) + ) + current = canonical("first_sentence.key") + visited: set[int] = set() + for _ in range(4): + options = [edge for edge in adjacency.get(current, []) if edge[1] not in visited] + if len(options) != 1: + break + current, sentence_id = options[0] + visited.add(sentence_id) + closed = len(visited) == 4 and current == canonical("first_sentence.key") + return { + "cycle_closed": closed, + "coreference_status": "VersionWitnessed" if witnessed else "Unresolved", + "relations": relations, + "witness_coordinates": tuple(sorted(witnessed)), + "source_roots": tuple(sorted(roots)), + "source_sentence_indices": tuple(sorted(visited)), + "observed_ref": (dataset, branch, version), + } + + +def main() -> None: + gold = load_gold() + for case_id in ("C-01", "C-02"): + case = load_case(case_id) + results = [evaluate(case, gold, order) for order in case["input"]["entry_orders"]] + if not all(x == results[0] for x in results): + raise SystemExit(f"{case_id}: rotation changed canonical result") + if results[0]["cycle_closed"] != case["expected"]["cycle_closed"]: + raise SystemExit(f"{case_id}: closure mismatched original pre-registration") + print(f"{case_id}: rotations={len(results)}, closed={results[0]['cycle_closed']}, " + f"ref={results[0]['observed_ref']}, witness={results[0]['witness_coordinates']}") + print("Reference only: no production parser, R2IL joins or evidence gate tested.") + + +if __name__ == "__main__": + main() diff --git a/tools/semantic_ring_oracle/test_ring_reference.py b/tools/semantic_ring_oracle/test_ring_reference.py new file mode 100644 index 000000000..adb14cfbd --- /dev/null +++ b/tools/semantic_ring_oracle/test_ring_reference.py @@ -0,0 +1,124 @@ +"""Version-address C-01/C-02 falsifiers and independent reference controls.""" + +import copy +import unittest + +from ring_reference import evaluate, load_case, load_gold + + +class RingReferenceTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.gold = load_gold() + cls.positive = load_case("C-01") + cls.negative = load_case("C-02") + + def run_case(self, case, **kw): + return evaluate(case, self.gold, case["input"]["entry_orders"][0], **kw) + + def test_witnessed_closure_is_rotation_invariant(self): + readings = [evaluate(self.positive, self.gold, order) + for order in self.positive["input"]["entry_orders"]] + self.assertTrue(all(x == readings[0] for x in readings)) + self.assertTrue(readings[0]["cycle_closed"]) + self.assertEqual(readings[0]["coreference_status"], "VersionWitnessed") + self.assertEqual(readings[0]["witness_coordinates"], ( + ("corefers(last_sentence.key,first_sentence.key)", "symbolic-ring", "main", 17), + )) + self.assertEqual(readings[0]["source_roots"], ("independent_coref_witness_1",)) + + def test_pre_witness_version_remains_open_under_every_rotation(self): + for order in self.negative["input"]["entry_orders"]: + with self.subTest(order=order): + result = evaluate(self.negative, self.gold, order) + self.assertFalse(result["cycle_closed"]) + self.assertEqual(result["coreference_status"], "Unresolved") + self.assertEqual(result["witness_coordinates"], ()) + self.assertEqual(len(result["relations"]), 4) + + def test_a_later_version_closes_same_C02_without_rewriting_identities(self): + future = {"dataset": "symbolic-ring", "branch": "main", "version": 17} + result = self.run_case(self.negative, read_ref=future) + baseline = self.run_case(self.positive) + self.assertTrue(result["cycle_closed"]) + self.assertEqual(result["relations"], baseline["relations"]) + self.assertEqual(result["witness_coordinates"], baseline["witness_coordinates"]) + + def test_legacy_external_witness_field_does_not_control_admission(self): + candidate = copy.deepcopy(self.positive) + candidate["input"]["external_identity_witness"]["root"] = "spoofed-root" + self.assertTrue(self.run_case(candidate)["cycle_closed"]) + self.assertEqual(self.run_case(candidate)["source_roots"], ( + "independent_coref_witness_1",)) + + def test_declaring_a_root_does_not_bypass_older_read_horizon(self): + old = {"dataset": "symbolic-ring", "branch": "main", "version": 16} + self.assertFalse(self.run_case(self.positive, read_ref=old)["cycle_closed"]) + + def test_dataset_or_branch_mismatch_cannot_witness_identity(self): + for ref in ( + {"dataset": "other-dataset", "branch": "main", "version": 17}, + {"dataset": "symbolic-ring", "branch": "other-branch", "version": 17}, + ): + with self.subTest(ref=ref): + self.assertFalse(self.run_case(self.positive, read_ref=ref)["cycle_closed"]) + + def test_wrong_claim_address_is_refused(self): + gold = copy.deepcopy(self.gold) + gold["attested_identity"][0]["claim_address"] = "invented.witness.id" + with self.assertRaisesRegex(ValueError, "wrong claim address"): + evaluate(self.positive, gold, self.positive["input"]["entry_orders"][0]) + + def test_missing_or_unbounded_version_ref_is_refused(self): + for ref in ({"dataset": "symbolic-ring", "branch": "main"}, + {"dataset": "symbolic-ring", "branch": "main", "version": -1}, + {"dataset": "symbolic-ring", "branch": "main", "version": None}): + with self.subTest(ref=ref), self.assertRaisesRegex(ValueError, "reference"): + self.run_case(self.positive, read_ref=ref) + + def test_missing_last_claim_breaks_cycle_even_with_witness(self): + self.assertFalse(self.run_case(self.positive, + facts=copy.deepcopy(self.gold["facts"][:-1]))["cycle_closed"]) + + def test_changed_predicate_changes_semantics_not_only_topology(self): + baseline = self.run_case(self.positive) + altered = copy.deepcopy(self.gold["facts"]) + altered[-1]["predicate"] = "destroys" + mutant = self.run_case(self.positive, facts=altered) + self.assertTrue(mutant["cycle_closed"], "unlabeled topology still cycles") + self.assertNotEqual(mutant["relations"], baseline["relations"]) + + def test_different_last_referent_stays_open(self): + changed = copy.deepcopy(self.gold["facts"]) + changed[-1]["object"] = "another.key" + self.assertFalse(self.run_case(self.positive, facts=changed)["cycle_closed"]) + + def test_duplicate_source_binding_refused(self): + doubled = copy.deepcopy(self.gold["facts"]) + doubled[1]["sentence_index"] = 0 + with self.assertRaisesRegex(ValueError, "multiple atoms"): + self.run_case(self.positive, facts=doubled) + + def test_unregistered_rotation_refused(self): + with self.assertRaisesRegex(ValueError, "unregistered"): + evaluate(self.positive, self.gold, [3, 2, 1, 0]) + + def test_expected_fields_do_not_drive_oracle(self): + candidate = copy.deepcopy(self.negative) + candidate["expected"]["cycle_closed"] = True + self.assertFalse(self.run_case(candidate)["cycle_closed"]) + + def test_source_roots_are_evidential_metadata_not_witness_identity(self): + gold = copy.deepcopy(self.gold) + gold["attested_identity"][0]["source_roots"] = [ + "source-A", "source-A", "source-B", + ] + result = evaluate(self.positive, gold, self.positive["input"]["entry_orders"][0]) + self.assertTrue(result["cycle_closed"]) + self.assertEqual(result["source_roots"], ("source-A", "source-B")) + self.assertEqual(result["witness_coordinates"], + self.run_case(self.positive)["witness_coordinates"]) + + +if __name__ == "__main__": + unittest.main()