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..c494fdd --- /dev/null +++ b/tools/agreement_test.py @@ -0,0 +1,112 @@ +#!/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"]} + # 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"} + + # 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]] = {} + 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. + # 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: + 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 { + # 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, + } + + +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..70a6b85 --- /dev/null +++ b/tools/validate_agreement.py @@ -0,0 +1,91 @@ +#!/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 + + # 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()) + print(json.dumps({"ok": ok, "checks": CHECKS}, indent=2, sort_keys=True)) + return 0 if ok else 1 + + +if __name__ == "__main__": + raise SystemExit(main())