From f25da19794bb6b3af00b5b8cfb01f705ca4afb18 Mon Sep 17 00:00:00 2001 From: Michael Heller <21163552+mdheller@users.noreply.github.com> Date: Sun, 2 Aug 2026 19:31:43 -0400 Subject: [PATCH 1/3] =?UTF-8?q?feat(vocab):=20agreement=20test=20=E2=80=94?= =?UTF-8?q?=20glossary=20relations=20vs=20blast-radius=20graph=20(task=20#?= =?UTF-8?q?13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The neurosymbolic check that the vocabulary tells the truth about structure. tools/agreement_test.py projects the glossary's composition relations (has-a/has-member) onto the estate via alignment.estateBinding and compares them to a CONSUMED blast-radius/dependency graph (GBRG owns that graph; this only compares). Fail-closed on OVERCLAIM (a declared dependency with no observed edge = governance hole); reports DRIFT (an observed edge no relation names) as a remediation candidate (a proposed has-a relation), same treatment as the vocab-currency loop's candidate terms. Only composition predicates imply runtime dependencies; is-a/skos:* excluded. validate-agreement (in make validate) teeth: aligned agrees; overclaim refused; drift reported as candidate. --- CHANGELOG.md | 1 + Makefile | 7 +- fixtures/agreement/blast_radius_graph.json | 18 +++++ fixtures/agreement/glossary.json | 45 +++++++++++ specs/agreement-test.md | 22 ++++++ tools/agreement_test.py | 89 ++++++++++++++++++++++ tools/validate_agreement.py | 69 +++++++++++++++++ 7 files changed, 249 insertions(+), 2 deletions(-) create mode 100644 fixtures/agreement/blast_radius_graph.json create mode 100644 fixtures/agreement/glossary.json create mode 100644 specs/agreement-test.md create mode 100644 tools/agreement_test.py create mode 100644 tools/validate_agreement.py diff --git a/CHANGELOG.md b/CHANGELOG.md index f38cac4..83f7e65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Thi ## [Unreleased] ### Added +- Agreement test v0.1 — glossary relations vs the blast-radius graph (task #13, neurosymbolic): `tools/agreement_test.py` projects the glossary's composition relations (`has-a`/`has-member`) onto the estate via `alignment.estateBinding` and compares them to a consumed blast-radius/dependency graph (GBRG owns that graph). Fail-closed on OVERCLAIM (a declared dependency with no observed edge = governance hole); reports DRIFT (an observed edge no relation names) as a remediation candidate (a proposed `has-a` relation), like the vocab-currency loop's candidate terms. `validate-agreement` teeth: aligned agrees; overclaim refused; drift surfaced as candidate. - Glossary promotion v0.1 — the governed draft→approved alignment pass (task #13, closes the vocabulary lifecycle): `tools/promote_glossary_term.py` promotes a `draft` GlossaryTerm to `approved` ONLY when the 3-method alignment holds as a fail-closed MEET — capture (`ontologyClassRef`) ∧ vector-align (`vectorLink`, pinned to the sovereign nomic/768 space AND reciprocated by the named peer) ∧ implement (`estateBinding` entity/service/action). It recomputes the meet (never trusts a flag), refuses on any missing/non-reciprocal/off-space method (term stays draft, no governance hole), and its approved output PASSES the #250 alignment drift-guard by construction. `validate-glossary-promotion` teeth: promotes on full alignment; refuses missing-implement / non-reciprocal / off-space; promoted output passes the #250 guard. - Vocab-currency loop — LIVE dogfood on the estate's own vocab (`make vocab-currency-dogfood`): `tools/dogfood_vocab_currency.py` runs the governed loop over this repo's approved `GlossaryTerm`s (fixed/LSA) vs `specs/*.md` (open/LDA). Today only a handful of terms are approved against the full spec corpus, so the governed loop escalates-human (can't reach currency in its bound), writing real draft `GlossaryTerm` proposals to `build/vocab-currency-proposals/` — the artifacts ontogenesis ingests. Reports the outcome (does not gate on it, exit 0); Markdown-cleaned + generic-word stoplisted so domain terms surface. Enforcement stays on fixtures in `validate-vocab-currency-loop`. - Vocab-currency loop — draft-term emission (closes the currency→proposal lifecycle): each remediation step now emits a conformant status:**draft** `GlossaryTerm` artifact (`result.proposedTerms`) for the token it connects — the loop PROPOSES drafts for ontogenesis's 3-method alignment pass and never self-approves (`partOfSpeech`/`alignment` are left for downstream; status stays `draft`). The CI harness now REFUSES any proposed term that doesn't conform to `GlossaryTerm.json` or that isn't status:draft, so the loop can't push malformed or self-approved vocab into the glossary. 6 teeth total. diff --git a/Makefile b/Makefile index c4b37b1..51113ff 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ -.PHONY: validate validate-resource-contract validate-measurement validate-value-type validate-source-locator validate-sourceos-repo-manifest validate-mesh-action-registry validate-control-plane-examples validate-nlboot-examples validate-lattice-data-governai-examples validate-ops-history-examples validate-runtime-observability-examples validate-interpretability-examples validate-lifecycle-boundary-examples validate-svf-contracts validate-sync-cycle-receipts validate-onboarding-examples validate-runtime-causality-examples validate-agentic-os-examples validate-triparty-examples validate-labor-market-examples validate-supply-chain-risk-examples validate-reasoning-examples validate-mpcc-event-examples validate-knowledge-nugget-examples validate-semantic-action-examples validate-epistemic-kernel-examples validate-ab-update-examples validate-device-service-examples validate-duplicate-schema-ids validate-lawful-dispatch-receipt validate-architectural-building-block validate-agent-passport-examples validate-seam-definition-examples validate-agent-system-vocabulary validate-genesis-inception-examples validate-measurement validate-world-model-examples validate-eval-item-examples validate-ingestion-pipeline-examples validate-data-acquisition-examples validate-glossary-alignment-examples validate-data-class-examples validate-table-keys validate-dag-loop validate-vocab-currency-loop vocab-currency-dogfood validate-glossary-promotion +.PHONY: validate validate-resource-contract validate-measurement validate-value-type validate-source-locator validate-sourceos-repo-manifest validate-mesh-action-registry validate-control-plane-examples validate-nlboot-examples validate-lattice-data-governai-examples validate-ops-history-examples validate-runtime-observability-examples validate-interpretability-examples validate-lifecycle-boundary-examples validate-svf-contracts validate-sync-cycle-receipts validate-onboarding-examples validate-runtime-causality-examples validate-agentic-os-examples validate-triparty-examples validate-labor-market-examples validate-supply-chain-risk-examples validate-reasoning-examples validate-mpcc-event-examples validate-knowledge-nugget-examples validate-semantic-action-examples validate-epistemic-kernel-examples validate-ab-update-examples validate-device-service-examples validate-duplicate-schema-ids validate-lawful-dispatch-receipt validate-architectural-building-block validate-agent-passport-examples validate-seam-definition-examples validate-agent-system-vocabulary validate-genesis-inception-examples validate-measurement validate-world-model-examples validate-eval-item-examples validate-ingestion-pipeline-examples validate-data-acquisition-examples validate-glossary-alignment-examples validate-data-class-examples validate-table-keys validate-dag-loop validate-vocab-currency-loop vocab-currency-dogfood validate-glossary-promotion validate-agreement -validate: validate-data-class-examples validate-glossary-alignment-examples validate-data-acquisition-examples validate-ingestion-pipeline-examples validate-control-plane-examples validate-nlboot-examples validate-lattice-data-governai-examples validate-ops-history-examples validate-runtime-observability-examples validate-interpretability-examples validate-lifecycle-boundary-examples validate-svf-contracts validate-sync-cycle-receipts validate-onboarding-examples validate-runtime-causality-examples validate-agentic-os-examples validate-triparty-examples validate-labor-market-examples validate-supply-chain-risk-examples validate-reasoning-examples validate-mpcc-event-examples validate-knowledge-nugget-examples validate-semantic-action-examples validate-epistemic-kernel-examples validate-ab-update-examples validate-device-service-examples validate-duplicate-schema-ids validate-value-type validate-source-locator validate-sourceos-repo-manifest validate-mesh-action-registry validate-lawful-dispatch-receipt validate-architectural-building-block validate-agent-passport-examples validate-seam-definition-examples validate-agent-system-vocabulary validate-genesis-inception-examples validate-measurement validate-world-model-examples validate-eval-item-examples validate-resource-contract validate-table-keys validate-dag-loop validate-vocab-currency-loop validate-glossary-promotion +validate: validate-data-class-examples validate-glossary-alignment-examples validate-data-acquisition-examples validate-ingestion-pipeline-examples validate-control-plane-examples validate-nlboot-examples validate-lattice-data-governai-examples validate-ops-history-examples validate-runtime-observability-examples validate-interpretability-examples validate-lifecycle-boundary-examples validate-svf-contracts validate-sync-cycle-receipts validate-onboarding-examples validate-runtime-causality-examples validate-agentic-os-examples validate-triparty-examples validate-labor-market-examples validate-supply-chain-risk-examples validate-reasoning-examples validate-mpcc-event-examples validate-knowledge-nugget-examples validate-semantic-action-examples validate-epistemic-kernel-examples validate-ab-update-examples validate-device-service-examples validate-duplicate-schema-ids validate-value-type validate-source-locator validate-sourceos-repo-manifest validate-mesh-action-registry validate-lawful-dispatch-receipt validate-architectural-building-block validate-agent-passport-examples validate-seam-definition-examples validate-agent-system-vocabulary validate-genesis-inception-examples validate-measurement validate-world-model-examples validate-eval-item-examples validate-resource-contract validate-table-keys validate-dag-loop validate-vocab-currency-loop validate-glossary-promotion validate-agreement @echo "OK: validate" validate-ingestion-pipeline-examples: @@ -19,6 +19,9 @@ validate-glossary-promotion: python3 -m pip install --user jsonschema >/dev/null python3 tools/validate_glossary_promotion.py +validate-agreement: + python3 tools/validate_agreement.py + validate-table-keys: python3 -m pip install --user jsonschema referencing >/dev/null python3 tools/validate_key_bipartite.py diff --git a/fixtures/agreement/blast_radius_graph.json b/fixtures/agreement/blast_radius_graph.json new file mode 100644 index 0000000..ee5b504 --- /dev/null +++ b/fixtures/agreement/blast_radius_graph.json @@ -0,0 +1,18 @@ +{ + "nodes": [ + "urn:srcos:service:release-gate", + "urn:srcos:service:attestation-signer", + "urn:srcos:service:epistemic-evaluator", + "urn:srcos:service:key-store" + ], + "edges": [ + { + "from": "urn:srcos:service:release-gate", + "to": "urn:srcos:service:attestation-signer" + }, + { + "from": "urn:srcos:service:attestation-signer", + "to": "urn:srcos:service:key-store" + } + ] +} diff --git a/fixtures/agreement/glossary.json b/fixtures/agreement/glossary.json new file mode 100644 index 0000000..adec3e0 --- /dev/null +++ b/fixtures/agreement/glossary.json @@ -0,0 +1,45 @@ +{ + "terms": [ + { + "id": "urn:srcos:glossary:release-gate", + "status": "approved", + "name": "release gate", + "alignment": { + "estateBinding": { + "kind": "service", + "ref": "urn:srcos:service:release-gate" + } + }, + "relations": [ + { + "predicate": "has-a", + "target": "urn:srcos:glossary:attestation" + } + ] + }, + { + "id": "urn:srcos:glossary:attestation", + "status": "approved", + "name": "attestation", + "alignment": { + "estateBinding": { + "kind": "service", + "ref": "urn:srcos:service:attestation-signer" + } + }, + "relations": [] + }, + { + "id": "urn:srcos:glossary:epistemic-level", + "status": "approved", + "name": "epistemic level", + "alignment": { + "estateBinding": { + "kind": "service", + "ref": "urn:srcos:service:epistemic-evaluator" + } + }, + "relations": [] + } + ] +} diff --git a/specs/agreement-test.md b/specs/agreement-test.md new file mode 100644 index 0000000..f14177b --- /dev/null +++ b/specs/agreement-test.md @@ -0,0 +1,22 @@ +# Agreement test (v0.1) — declared relations vs observed dependencies + +The neurosymbolic check that the vocabulary tells the truth about structure. Two independent views +of the same estate: + +- **symbolic** — the glossary's typed `relations` between terms (composition: `has-a`, `has-member`); +- **observed** — the blast-radius / dependency graph over estate entities (as a governed GBRG + analysis emits it). + +Projected onto each other via `alignment.estateBinding`, they must **agree**: + +- **overclaim** — a term declares `A has-a B` (both bound) but the graph shows no edge → the + vocabulary asserts a dependency the estate doesn't exhibit. **Fail-closed** (governance hole). +- **drift** — the graph shows an edge between two bound entities that no relation names → the estate + has a dependency the vocabulary hasn't captured. Surfaced as a **remediation candidate** (a + proposed `has-a` relation), not a hard failure — same treatment as the vocab-currency loop's + candidate terms. + +`tools/agreement_test.py` CONSUMES a blast-radius graph (GBRG owns that graph; this only compares). +Only composition predicates (`has-a`, `has-member`) imply runtime dependencies; `is-a`/`skos:*` are +subsumption/lexical and excluded. `make validate-agreement` proves: aligned agrees, overclaim is +refused, drift is reported as a candidate. diff --git a/tools/agreement_test.py b/tools/agreement_test.py new file mode 100644 index 0000000..b20b8db --- /dev/null +++ b/tools/agreement_test.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""Agreement test — the glossary's DECLARED relations vs the estate's OBSERVED dependencies (task #13). + +The neurosymbolic check: the vocabulary is the SYMBOLIC view (typed `relations` between terms, e.g. +`has-a` composition); the blast-radius graph is the OBSERVED view (actual dependency edges between +estate entities, as a governed blast-radius/GBRG analysis emits them). Two independent views of the +same structure must AGREE — if they don't, one is wrong: + + * OVERCLAIM — a term declares `A has-a B`, both bound to estate entities, but the blast-radius + graph shows no corresponding edge. The vocabulary asserts a dependency the estate doesn't + exhibit: a governance hole. FAIL-CLOSED. + * DRIFT — the blast-radius graph shows an edge between two bound entities that no declared + relation names. The estate has a dependency the vocabulary hasn't captured: staleness. Reported + as a remediation candidate (a proposed `has-a` relation), not a hard failure — same treatment as + the vocab-currency loop's candidate terms. + +This tool CONSUMES a blast-radius graph (it does not define one — GBRG is that graph's authority); +it only projects the glossary's relations onto the estate via `alignment.estateBinding` and compares. + +Dependency-implying predicates are the composition/aggregation ones (`has-a`, `has-member`) — the +whole depends on / blast-radiates to its parts. Subsumption (`is-a`) and lexical (`skos:*`) links +are NOT runtime dependencies and are excluded. +""" +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +DEP_PREDICATES = {"has-a", "has-member"} + + +def load(p: Path) -> dict: + return json.loads(Path(p).read_text(encoding="utf-8")) + + +def _binding(term: dict) -> str | None: + return ((term.get("alignment") or {}).get("estateBinding") or {}).get("ref") + + +def agreement(glossary: dict, graph: dict) -> dict: + terms = {t["id"]: t for t in glossary["terms"]} + bind = {tid: _binding(t) for tid, t in terms.items() if _binding(t)} + observed = {(e["from"], e["to"]) for e in graph.get("edges", [])} + + # DECLARED estate edges: a dep-implying relation A->B where both A and B are bound entities. + declared: dict[tuple[str, str], tuple[str, str]] = {} + for tid, t in terms.items(): + if tid not in bind: + continue + for rel in t.get("relations", []): + if rel.get("predicate") in DEP_PREDICATES and rel.get("target") in bind: + declared[(bind[tid], bind[rel["target"]])] = (tid, rel["target"]) + + agreements = [{"declaredBy": v, "edge": list(k)} for k, v in declared.items() if k in observed] + overclaims = [{"declaredBy": v, "edge": list(k), + "detail": "vocabulary declares a dependency the blast-radius graph does not show"} + for k, v in declared.items() if k not in observed] + + # DRIFT: an observed edge between two bound entities with no declared dep relation -> propose one. + ent_to_term = {ref: tid for tid, ref in bind.items()} + drift = [] + for (ef, et) in sorted(observed): + if ef in ent_to_term and et in ent_to_term and (ef, et) not in declared: + drift.append({"edge": [ef, et], + "proposedRelation": {"subject": ent_to_term[ef], "predicate": "has-a", + "target": ent_to_term[et]}, + "detail": "estate shows a dependency the vocabulary does not name"}) + + return { + "ok": not overclaims, # fail-closed on overclaims; drift is remediation, not failure + "agreements": agreements, + "overclaims": overclaims, + "driftCandidates": drift, + } + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--glossary", default=str(Path(__file__).resolve().parents[1] / "fixtures" / "agreement" / "glossary.json")) + ap.add_argument("--graph", default=str(Path(__file__).resolve().parents[1] / "fixtures" / "agreement" / "blast_radius_graph.json")) + args = ap.parse_args() + result = agreement(load(Path(args.glossary)), load(Path(args.graph))) + print(json.dumps(result, indent=2)) + return 0 if result["ok"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/validate_agreement.py b/tools/validate_agreement.py new file mode 100644 index 0000000..f62d21e --- /dev/null +++ b/tools/validate_agreement.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""CI teeth for the glossary<->blast-radius agreement test (task #13). + +Asserts the two views are held to AGREE fail-closed: an aligned vocabulary agrees; a vocabulary +that overclaims a dependency the estate doesn't exhibit is REFUSED (governance hole); and an estate +dependency the vocabulary hasn't named is surfaced as a remediation candidate (drift), not silently +passed and not hard-failed. +""" +from __future__ import annotations + +import copy +import json +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import agreement_test as A # noqa: E402 + +ROOT = Path(__file__).resolve().parents[1] +FIX = ROOT / "fixtures" / "agreement" + +FAILURES: list[str] = [] +CHECKS: dict[str, bool] = {} + +EPI_EDGE = {"from": "urn:srcos:service:release-gate", "to": "urn:srcos:service:epistemic-evaluator"} +EPI_REL = {"predicate": "has-a", "target": "urn:srcos:glossary:epistemic-level"} + + +def main() -> int: + glossary = A.load(FIX / "glossary.json") + graph = A.load(FIX / "blast_radius_graph.json") + + # 1. Aligned vocabulary agrees with the observed graph. + r = A.agreement(glossary, graph) + if not r["ok"] or len(r["agreements"]) < 1 or r["overclaims"]: + FAILURES.append(f"aligned case should agree, got overclaims={r['overclaims']}") + else: + CHECKS["aligned:agrees"] = True + + # 2. OVERCLAIM — declare release-gate has-a epistemic-level, but the graph has no such edge. + g2 = copy.deepcopy(glossary) + next(t for t in g2["terms"] if t["id"].endswith("release-gate"))["relations"].append(EPI_REL) + r = A.agreement(g2, graph) # graph lacks release-gate->epistemic-evaluator + if r["ok"] or not any(o["edge"][1].endswith("epistemic-evaluator") for o in r["overclaims"]): + FAILURES.append("an overclaimed dependency (no observed edge) must fail") + else: + CHECKS["overclaim:refused"] = True + + # 3. DRIFT — the graph shows release-gate->epistemic-evaluator, but no relation names it. + g3 = copy.deepcopy(graph) + g3["edges"].append(EPI_EDGE) + r = A.agreement(glossary, g3) # glossary has no has-a epistemic-level + drifted = [d for d in r["driftCandidates"] if d["edge"][1].endswith("epistemic-evaluator")] + if not r["ok"]: + FAILURES.append("drift (observed-but-undeclared) must be reported, not hard-failed") + elif not drifted or drifted[0]["proposedRelation"]["predicate"] != "has-a": + FAILURES.append("drift must surface a proposed has-a relation as a remediation candidate") + else: + CHECKS["drift:reported-as-candidate"] = True + + for m in FAILURES: + print(f"FAIL: {m}", file=sys.stderr) + ok = not FAILURES and all(CHECKS.values()) + print(json.dumps({"ok": ok, "checks": CHECKS}, indent=2, sort_keys=True)) + return 0 if ok else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) From a8b0fdc040379744562fc4c76b3a2bddc81d381f Mon Sep 17 00:00:00 2001 From: Michael Heller <21163552+mdheller@users.noreply.github.com> Date: Sun, 2 Aug 2026 19:32:38 -0400 Subject: [PATCH 2/3] fix(vocab): agreement test only holds APPROVED terms (own review) A draft term hasn't passed the alignment gate and doesn't regulate state, so its relations must not overclaim the estate. Restrict declared edges + bindings to status==approved terms. --- tools/agreement_test.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/agreement_test.py b/tools/agreement_test.py index b20b8db..2b20f40 100644 --- a/tools/agreement_test.py +++ b/tools/agreement_test.py @@ -40,7 +40,10 @@ def _binding(term: dict) -> str | None: def agreement(glossary: dict, graph: dict) -> dict: terms = {t["id"]: t for t in glossary["terms"]} - bind = {tid: _binding(t) for tid, t in terms.items() if _binding(t)} + # Only APPROVED terms regulate state, so only their relations are held to agreement — a draft + # term hasn't been promoted through the alignment gate and can't overclaim the estate. + bind = {tid: _binding(t) for tid, t in terms.items() + if _binding(t) and t.get("status") == "approved"} observed = {(e["from"], e["to"]) for e in graph.get("edges", [])} # DECLARED estate edges: a dep-implying relation A->B where both A and B are bound entities. From 37b7b86ca76bab767f5ba24ad7b3b4dbdba3942f Mon Sep 17 00:00:00 2001 From: Michael Heller <21163552+mdheller@users.noreply.github.com> Date: Sun, 2 Aug 2026 19:36:26 -0400 Subject: [PATCH 3/3] fix(vocab): fail-closed on ambiguous bindings + malformed graph edges (Copilot #259) - two approved terms binding the SAME estate entity made drift attribution ambiguous (dict silently dropped a duplicate). Now surfaced as bindingConflicts and fails closed; only unambiguously-bound entities participate in drift. - a graph edge missing from/to would KeyError-crash. Now malformed edges are collected into malformedEdges and fail closed instead of crashing. Two teeth added (ambiguous-binding, malformed-edge); 5 total. --- tools/agreement_test.py | 26 +++++++++++++++++++++++--- tools/validate_agreement.py | 22 ++++++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/tools/agreement_test.py b/tools/agreement_test.py index 2b20f40..c494fdd 100644 --- a/tools/agreement_test.py +++ b/tools/agreement_test.py @@ -44,7 +44,22 @@ def agreement(glossary: dict, graph: dict) -> dict: # term hasn't been promoted through the alignment gate and can't overclaim the estate. bind = {tid: _binding(t) for tid, t in terms.items() if _binding(t) and t.get("status") == "approved"} - observed = {(e["from"], e["to"]) for e in graph.get("edges", [])} + + # Fail-closed on ambiguous bindings: if two approved terms bind the SAME estate entity, drift + # attribution is ambiguous (which term should own a proposed relation?). Surface it, don't guess. + ref_owners: dict[str, list[str]] = {} + for tid, ref in bind.items(): + ref_owners.setdefault(ref, []).append(tid) + binding_conflicts = [{"estateRef": ref, "terms": sorted(tids)} + for ref, tids in ref_owners.items() if len(tids) > 1] + + # Fail-closed on a malformed consumed graph: an edge missing from/to can't be reasoned about. + observed, malformed_edges = set(), [] + for e in graph.get("edges", []): + if isinstance(e, dict) and e.get("from") and e.get("to"): + observed.add((e["from"], e["to"])) + else: + malformed_edges.append(e) # DECLARED estate edges: a dep-implying relation A->B where both A and B are bound entities. declared: dict[tuple[str, str], tuple[str, str]] = {} @@ -61,7 +76,8 @@ def agreement(glossary: dict, graph: dict) -> dict: for k, v in declared.items() if k not in observed] # DRIFT: an observed edge between two bound entities with no declared dep relation -> propose one. - ent_to_term = {ref: tid for tid, ref in bind.items()} + # Only unambiguously-bound entities participate (conflicting refs are surfaced separately above). + ent_to_term = {ref: tids[0] for ref, tids in ref_owners.items() if len(tids) == 1} drift = [] for (ef, et) in sorted(observed): if ef in ent_to_term and et in ent_to_term and (ef, et) not in declared: @@ -71,10 +87,14 @@ def agreement(glossary: dict, graph: dict) -> dict: "detail": "estate shows a dependency the vocabulary does not name"}) return { - "ok": not overclaims, # fail-closed on overclaims; drift is remediation, not failure + # Fail-closed on overclaims AND on ambiguous bindings / malformed graph edges; drift alone + # is remediation, not failure. + "ok": not overclaims and not binding_conflicts and not malformed_edges, "agreements": agreements, "overclaims": overclaims, "driftCandidates": drift, + "bindingConflicts": binding_conflicts, + "malformedEdges": malformed_edges, } diff --git a/tools/validate_agreement.py b/tools/validate_agreement.py index f62d21e..70a6b85 100644 --- a/tools/validate_agreement.py +++ b/tools/validate_agreement.py @@ -58,6 +58,28 @@ def main() -> int: else: CHECKS["drift:reported-as-candidate"] = True + # 4. Ambiguous binding — two approved terms bind the SAME estate entity -> fail-closed. + g4 = copy.deepcopy(glossary) + next(t for t in g4["terms"] if t["id"].endswith("attestation"))["alignment"]["estateBinding"]["ref"] = \ + "urn:srcos:service:release-gate" # now two terms bind release-gate + r = A.agreement(g4, graph) + if r["ok"] or not r["bindingConflicts"]: + FAILURES.append("two terms binding the same estate entity must fail-closed (ambiguous)") + else: + CHECKS["ambiguous-binding:refused"] = True + + # 5. Malformed graph edge (missing to) -> fail-closed, not a crash. + g5 = copy.deepcopy(graph) + g5["edges"].append({"from": "urn:srcos:service:release-gate"}) # no 'to' + try: + r = A.agreement(glossary, g5) + if r["ok"] or not r["malformedEdges"]: + FAILURES.append("a malformed graph edge must fail-closed and be surfaced") + else: + CHECKS["malformed-edge:refused"] = True + except Exception as exc: # noqa: BLE001 + FAILURES.append(f"malformed edge crashed instead of failing closed: {exc!r}") + for m in FAILURES: print(f"FAIL: {m}", file=sys.stderr) ok = not FAILURES and all(CHECKS.values())