From 8f71add0e16b2bd6097ef8f9d37bdf07b1a9f6c4 Mon Sep 17 00:00:00 2001 From: Michael Heller <21163552+mdheller@users.noreply.github.com> Date: Sun, 2 Aug 2026 19:19:32 -0400 Subject: [PATCH 1/3] =?UTF-8?q?feat(vocab):=20governed=20draft->approved?= =?UTF-8?q?=20glossary=20promotion=20=E2=80=94=20the=203-method=20alignmen?= =?UTF-8?q?t=20pass=20(task=20#13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) AND vector-align (vectorLink pinned to the sovereign nomic/768 space AND reciprocated by the named peer) AND implement (estateBinding entity/service/action). It recomputes the meet (never trusts a declared 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 (in make validate) teeth: full alignment promotes; missing-implement, non-reciprocal peer, and off-sovereign-space embedding are each refused; promoted output passes the reused #250 check_alignment. Example: governed-loop <-> operational-dag reciprocal pair. --- CHANGELOG.md | 1 + Makefile | 8 +- .../glossary_promotion.governed_loop.json | 46 ++++++++++ specs/glossary-promotion.md | 23 +++++ tools/promote_glossary_term.py | 84 +++++++++++++++++++ tools/validate_glossary_promotion.py | 82 ++++++++++++++++++ 6 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 examples/glossary_promotion.governed_loop.json create mode 100644 specs/glossary-promotion.md create mode 100644 tools/promote_glossary_term.py create mode 100644 tools/validate_glossary_promotion.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 946a586..f38cac4 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 +- 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. - Vocab-currency loop v0.1 — the vocabulary-currency GovernedLoop made LIVE (task #13): `tools/vocab_currency_loop.py` instantiates the merged `GovernedLoop` contract and is governed BY it (reads bound + tolerance + `onNonConvergence` + admission from the document). The approved glossary is the FIXED set (LSA), the corpus token distribution is the OPEN set (LDA), and uncovered probability mass is the currency signal (the LSI drive: new fields that became vocab but aren't connected). Remediation connects the highest-mass uncovered term (a proposed `GlossaryTerm`), strictly decreasing divergence. Fail-closed: runs at most `maxIterations` (never spins); escalates-human (non-zero exit) if it can't reach tolerance within the bound rather than declaring currency; refuses to run unadmitted (no `superconsciousRef`). `validate-vocab-currency-loop` asserts all three teeth (converges / escalates / refuses). Emits `candidateNewVocab` for routing into ontogenesis/prophet-ontology. Dependency-free. diff --git a/Makefile b/Makefile index 5843511..c4b37b1 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 +.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: 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: 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 @echo "OK: validate" validate-ingestion-pipeline-examples: @@ -15,6 +15,10 @@ validate-glossary-alignment-examples: python3 -m pip install --user jsonschema >/dev/null python3 tools/validate_glossary_alignment_examples.py +validate-glossary-promotion: + python3 -m pip install --user jsonschema >/dev/null + python3 tools/validate_glossary_promotion.py + validate-table-keys: python3 -m pip install --user jsonschema referencing >/dev/null python3 tools/validate_key_bipartite.py diff --git a/examples/glossary_promotion.governed_loop.json b/examples/glossary_promotion.governed_loop.json new file mode 100644 index 0000000..61ee1b0 --- /dev/null +++ b/examples/glossary_promotion.governed_loop.json @@ -0,0 +1,46 @@ +{ + "draftTerm": { + "id": "urn:srcos:glossary:governed-loop", + "type": "GlossaryTerm", + "specVersion": "2.0.0", + "name": "governed loop", + "partOfSpeech": "noun-phrase", + "status": "draft", + "definition": "A correction loop admissible only when bounded, convergent, fail-closed, and admitted by the superconscious." + }, + "alignment": { + "ontologyClassRef": "urn:srcos:ontology-class:governance.GovernedLoop", + "vectorLink": { + "peerRef": "urn:srcos:glossary:operational-dag", + "model": "nomic-ai/nomic-embed-text-v1.5", + "dimension": 768, + "cosine": 0.69 + }, + "estateBinding": { + "kind": "entity", + "ref": "urn:srcos:schema:GovernedLoop" + } + }, + "peer": { + "id": "urn:srcos:glossary:operational-dag", + "type": "GlossaryTerm", + "specVersion": "2.0.0", + "name": "operational DAG", + "partOfSpeech": "noun-phrase", + "status": "approved", + "definition": "The operational task graph carrying identity and dependency; it must be acyclic.", + "alignment": { + "ontologyClassRef": "urn:srcos:ontology-class:governance.OperationalDag", + "vectorLink": { + "peerRef": "urn:srcos:glossary:governed-loop", + "model": "nomic-ai/nomic-embed-text-v1.5", + "dimension": 768, + "cosine": 0.69 + }, + "estateBinding": { + "kind": "entity", + "ref": "urn:srcos:schema:OperationalDAG" + } + } + } +} diff --git a/specs/glossary-promotion.md b/specs/glossary-promotion.md new file mode 100644 index 0000000..f2df557 --- /dev/null +++ b/specs/glossary-promotion.md @@ -0,0 +1,23 @@ +# Glossary term promotion (v0.1) — the governed draft→approved alignment pass + +The vocab-currency loop ingests terms as `draft`; a draft term names something but does not yet +regulate state. It becomes `approved` — able to regulate governance — only through the 3-method +alignment from the GlossaryTerm contract, and only if all three hold as a **fail-closed meet** +(the same shape as DAR promotion = governance ∧ IP/legal): + +1. **capture** — `alignment.ontologyClassRef` (bound to a formal ontology class) +2. **vector-align** — `alignment.vectorLink` (an NP↔VP link in the sovereign 768 space, pinned to + `nomic-ai/nomic-embed-text-v1.5` / dim 768, and **reciprocated** by the named peer — a one-way + link is not an alignment) +3. **implement** — `alignment.estateBinding` (bound to a real estate entity / service / action) + +`tools/promote_glossary_term.py` recomputes the meet (never trusts a declared flag) and promotes +ONLY if all three hold; otherwise it **refuses** and names the unaligned method(s), leaving the +term `draft`. It never approves on a partial alignment — an approved-but-unaligned term is a +governance hole, exactly what the #250 drift-guard rejects. `validate-glossary-promotion` +(in `make validate`) proves the loop closes: the promoted output PASSES that same #250 guard, so +promotion is consistent-by-construction with the alignment contract; and missing / non-reciprocal / +off-space alignments are each refused. + +This closes the vocabulary lifecycle: currency-detect → propose draft → ontogenesis-ingest → +**governed alignment promotion → approved** (regulates state). diff --git a/tools/promote_glossary_term.py b/tools/promote_glossary_term.py new file mode 100644 index 0000000..a994ed8 --- /dev/null +++ b/tools/promote_glossary_term.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Governed draft->approved promotion of a GlossaryTerm via the 3-method alignment (task #13). + +The vocab-currency loop ingests terms as `draft`; a draft term names something but does not yet +regulate state. It becomes `approved` — able to regulate — only through the 3-method alignment +from the GlossaryTerm contract (#250), and only if all three hold as a FAIL-CLOSED MEET (the same +shape as DAR promotion = governance ∧ IP/legal): + + capture — alignment.ontologyClassRef (bound to a formal ontology class) + vector-align — alignment.vectorLink (an NP↔VP link in the sovereign 768 space, + RECIPROCATED by the named peer — a one-way link is not an alignment) + implement — alignment.estateBinding (bound to a real estate entity/service/action) + +promote() returns the approved term ONLY if the meet holds; otherwise it REFUSES and names the +unaligned method(s), leaving the term draft. It never approves on a partial alignment — an approved +term that isn't captured+aligned+implemented is a governance hole (exactly what #250's drift-guard +rejects; this is the active promotion whose output that guard then accepts). +""" +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + +EMBED_MODEL = "nomic-ai/nomic-embed-text-v1.5" +EMBED_DIM = 768 +ESTATE_KINDS = {"entity", "service", "action"} + + +def load(p: Path) -> dict: + return json.loads(Path(p).read_text(encoding="utf-8")) + + +def _method_failures(term: dict, alignment: dict, peer: dict | None) -> list[str]: + """Recompute the meet — never trust a declared 'aligned' flag. Returns the missing methods.""" + fails = [] + # 1. capture + if not str(alignment.get("ontologyClassRef") or "").strip(): + fails.append("capture (alignment.ontologyClassRef missing)") + # 2. vector-align — present, const-pinned to the sovereign space, and RECIPROCATED by the peer + vl = alignment.get("vectorLink") or {} + if not vl.get("peerRef"): + fails.append("vector-align (alignment.vectorLink missing)") + elif vl.get("model") != EMBED_MODEL or vl.get("dimension") != EMBED_DIM: + fails.append(f"vector-align (vectorLink must pin {EMBED_MODEL} / dim {EMBED_DIM})") + else: + back = ((peer or {}).get("alignment") or {}).get("vectorLink") or {} + if not peer or peer.get("id") != vl["peerRef"]: + fails.append(f"vector-align (peer {vl['peerRef']} not supplied for reciprocity check)") + elif back.get("peerRef") != term.get("id"): + fails.append(f"vector-align (peer {vl['peerRef']} does not link back — one-way link is not an alignment)") + # 3. implement + eb = alignment.get("estateBinding") or {} + if eb.get("kind") not in ESTATE_KINDS or not str(eb.get("ref") or "").strip(): + fails.append("implement (alignment.estateBinding missing a valid kind+ref)") + return fails + + +def promote(term: dict, alignment: dict, peer: dict | None = None) -> dict: + fails = _method_failures(term, alignment, peer) + if fails: + return {"promoted": False, "term": term.get("id"), "status": "draft", + "refused": "incomplete-alignment", "unaligned": fails, + "detail": "draft->approved refused: an approved term must be captured + vector-aligned " + "+ implemented (fail-closed meet); it stays draft"} + approved = {**term, "status": "approved", "alignment": alignment} + return {"promoted": True, "term": term["id"], "status": "approved", "approvedTerm": approved} + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--term", required=True, help="draft GlossaryTerm JSON") + ap.add_argument("--alignment", required=True, help="proposed alignment JSON") + ap.add_argument("--peer", help="the vectorLink peer term JSON (for reciprocity)") + args = ap.parse_args() + result = promote(load(Path(args.term)), load(Path(args.alignment)), + load(Path(args.peer)) if args.peer else None) + print(json.dumps(result, indent=2)) + return 0 if result.get("promoted") else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/validate_glossary_promotion.py b/tools/validate_glossary_promotion.py new file mode 100644 index 0000000..bcf9a0c --- /dev/null +++ b/tools/validate_glossary_promotion.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""CI teeth for the governed draft->approved glossary promotion (task #13). + +Asserts the promotion is a real fail-closed meet: a full 3-method alignment promotes, ANY missing +method refuses (term stays draft), and — crucially — the promoted output PASSES #250's own +alignment drift-guard, so promotion can never mint a governance hole (an approved-but-unaligned +term). Reuses `validate_glossary_alignment_examples.check_alignment` rather than reimplementing it. +""" +from __future__ import annotations + +import copy +import json +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import promote_glossary_term as P # noqa: E402 +import validate_glossary_alignment_examples as G250 # noqa: E402 + +ROOT = Path(__file__).resolve().parents[1] +BUNDLE = ROOT / "examples" / "glossary_promotion.governed_loop.json" + +FAILURES: list[str] = [] +CHECKS: dict[str, bool] = {} + + +def main() -> int: + b = json.loads(BUNDLE.read_text(encoding="utf-8")) + draft, alignment, peer = b["draftTerm"], b["alignment"], b["peer"] + + # 1. Full 3-method alignment -> promoted to approved. + r = P.promote(draft, alignment, peer) + if not r.get("promoted") or r["status"] != "approved": + FAILURES.append(f"full alignment should promote to approved, got {r.get('refused')} {r.get('unaligned')}") + else: + CHECKS["full-alignment:promotes"] = True + approved = r["approvedTerm"] + + # 1b. The promoted output must PASS #250's drift-guard (no governance hole). Feed the + # approved term + its peer so the reciprocity lookup resolves. + G250.FAILURES.clear() + G250.check_alignment({"approved": approved, "peer": peer}) + holes = [m for m in G250.FAILURES if approved["id"] in m or "governance hole" in m] + if holes: + FAILURES.append(f"promoted term fails #250 alignment drift-guard: {holes}") + else: + CHECKS["promoted-output:passes-250-guard"] = True + + # 2. Missing a method (implement) -> refused, stays draft. + r = P.promote(draft, {k: v for k, v in alignment.items() if k != "estateBinding"}, peer) + if r.get("promoted") or not any("implement" in m for m in r.get("unaligned", [])): + FAILURES.append("a term missing estateBinding must be refused (implement)") + else: + CHECKS["missing-implement:refused"] = True + + # 3. Non-reciprocal vectorLink (peer doesn't link back) -> refused. + lonely = copy.deepcopy(peer) + lonely["alignment"]["vectorLink"]["peerRef"] = "urn:srcos:glossary:someone-else" + r = P.promote(draft, alignment, lonely) + if r.get("promoted") or not any("vector-align" in m for m in r.get("unaligned", [])): + FAILURES.append("a one-way vectorLink (peer does not reciprocate) must be refused") + else: + CHECKS["non-reciprocal:refused"] = True + + # 4. Off-space embedding (wrong model) -> refused (sovereign-space pin). + offspace = copy.deepcopy(alignment) + offspace["vectorLink"]["model"] = "openai/text-embedding-3-small" + r = P.promote(draft, offspace, peer) + if r.get("promoted") or not any("vector-align" in m for m in r.get("unaligned", [])): + FAILURES.append("a vectorLink off the sovereign embedding space must be refused") + else: + CHECKS["off-space-embedding:refused"] = 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 b71eeb94de26454145691408526ecfbc5e4247b0 Mon Sep 17 00:00:00 2001 From: Michael Heller <21163552+mdheller@users.noreply.github.com> Date: Sun, 2 Aug 2026 19:20:35 -0400 Subject: [PATCH 2/3] =?UTF-8?q?fix(vocab):=20guard=20promotion=20input=20?= =?UTF-8?q?=E2=80=94=20refuse=20non-term=20+=20already-approved=20(own=20r?= =?UTF-8?q?eview)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit promote() now refuses a non-GlossaryTerm input and an already-approved term (no silent mutation), and uses .get('id') so malformed input can't KeyError. Two teeth added; 7 total. --- tools/promote_glossary_term.py | 12 +++++++++++- tools/validate_glossary_promotion.py | 12 ++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/tools/promote_glossary_term.py b/tools/promote_glossary_term.py index a994ed8..1575752 100644 --- a/tools/promote_glossary_term.py +++ b/tools/promote_glossary_term.py @@ -58,9 +58,19 @@ def _method_failures(term: dict, alignment: dict, peer: dict | None) -> list[str def promote(term: dict, alignment: dict, peer: dict | None = None) -> dict: + # Input guard — only promote an actual draft GlossaryTerm; a non-term or an already-approved + # term is not a valid promotion target (don't silently mutate it). + if term.get("type") != "GlossaryTerm" or not str(term.get("id") or "").strip(): + return {"promoted": False, "term": term.get("id"), "status": term.get("status"), + "refused": "not-a-draft-glossary-term", + "detail": "promotion input must be a GlossaryTerm with an id"} + if term.get("status") == "approved": + return {"promoted": False, "term": term["id"], "status": "approved", + "refused": "already-approved", "detail": "term is already approved — nothing to promote"} + fails = _method_failures(term, alignment, peer) if fails: - return {"promoted": False, "term": term.get("id"), "status": "draft", + return {"promoted": False, "term": term["id"], "status": "draft", "refused": "incomplete-alignment", "unaligned": fails, "detail": "draft->approved refused: an approved term must be captured + vector-aligned " "+ implemented (fail-closed meet); it stays draft"} diff --git a/tools/validate_glossary_promotion.py b/tools/validate_glossary_promotion.py index bcf9a0c..7970f5c 100644 --- a/tools/validate_glossary_promotion.py +++ b/tools/validate_glossary_promotion.py @@ -71,6 +71,18 @@ def main() -> int: else: CHECKS["off-space-embedding:refused"] = True + # 5. Input guards — a non-term and an already-approved term are both refused (no silent mutation). + r = P.promote({"id": "x", "type": "NotATerm"}, alignment, peer) + if r.get("promoted") or r.get("refused") != "not-a-draft-glossary-term": + FAILURES.append("a non-GlossaryTerm input must be refused") + else: + CHECKS["non-term-input:refused"] = True + r = P.promote({**draft, "status": "approved"}, alignment, peer) + if r.get("promoted") or r.get("refused") != "already-approved": + FAILURES.append("an already-approved term must be refused (nothing to promote)") + else: + CHECKS["already-approved:refused"] = True + for m in FAILURES: print(f"FAIL: {m}", file=sys.stderr) ok = not FAILURES and all(CHECKS.values()) From dd30c7e4aee77dcbbd5b9eb236c79bec1bad769e Mon Sep 17 00:00:00 2001 From: Michael Heller <21163552+mdheller@users.noreply.github.com> Date: Sun, 2 Aug 2026 19:24:40 -0400 Subject: [PATCH 3/3] =?UTF-8?q?fix(vocab):=20remediate=20Copilot=20review?= =?UTF-8?q?=20on=20#258=20=E2=80=94=20cosine=20+=20schema-valid=20output?= =?UTF-8?q?=20+=20guard=20filter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - promote() now requires vectorLink.cosine (number in [-1,1], schema-required) AND validates the minted approved term against GlossaryTerm.json — it can never produce a schema-invalid approved term (fail-closed on the output, not just the alignment content). - the #250-guard teeth keyed the terms dict by 'approved'/'peer', but check_alignment prefixes messages with the dict key, so a real failure (e.g. non-reciprocal) whose message lacked the term id could slip the filter. Now keyed by id; filter catches any failure prefixed with the id. - added teeth: missing-cosine refused + a regression test proving the #250-guard filter catches a holey approved term. 9 teeth total. --- tools/promote_glossary_term.py | 14 ++++++++++++++ tools/validate_glossary_promotion.py | 29 ++++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/tools/promote_glossary_term.py b/tools/promote_glossary_term.py index 1575752..1f8d438 100644 --- a/tools/promote_glossary_term.py +++ b/tools/promote_glossary_term.py @@ -23,6 +23,10 @@ import sys from pathlib import Path +import jsonschema + +ROOT = Path(__file__).resolve().parents[1] +GLOSSARY_SCHEMA = ROOT / "schemas" / "GlossaryTerm.json" EMBED_MODEL = "nomic-ai/nomic-embed-text-v1.5" EMBED_DIM = 768 ESTATE_KINDS = {"entity", "service", "action"} @@ -40,10 +44,13 @@ def _method_failures(term: dict, alignment: dict, peer: dict | None) -> list[str fails.append("capture (alignment.ontologyClassRef missing)") # 2. vector-align — present, const-pinned to the sovereign space, and RECIPROCATED by the peer vl = alignment.get("vectorLink") or {} + cos = vl.get("cosine") if not vl.get("peerRef"): fails.append("vector-align (alignment.vectorLink missing)") elif vl.get("model") != EMBED_MODEL or vl.get("dimension") != EMBED_DIM: fails.append(f"vector-align (vectorLink must pin {EMBED_MODEL} / dim {EMBED_DIM})") + elif not isinstance(cos, (int, float)) or isinstance(cos, bool) or not (-1 <= cos <= 1): + fails.append("vector-align (vectorLink.cosine must be a number in [-1, 1] — schema-required)") else: back = ((peer or {}).get("alignment") or {}).get("vectorLink") or {} if not peer or peer.get("id") != vl["peerRef"]: @@ -75,6 +82,13 @@ def promote(term: dict, alignment: dict, peer: dict | None = None) -> dict: "detail": "draft->approved refused: an approved term must be captured + vector-aligned " "+ implemented (fail-closed meet); it stays draft"} approved = {**term, "status": "approved", "alignment": alignment} + # Fail-closed on the output: never MINT a schema-invalid approved term. The method meet checks + # the alignment's governance content; this backstops every remaining GlossaryTerm.json constraint. + schema_errs = sorted(jsonschema.Draft202012Validator(load(GLOSSARY_SCHEMA)).iter_errors(approved), key=str) + if schema_errs: + return {"promoted": False, "term": term["id"], "status": "draft", + "refused": "schema-invalid-output", "unaligned": [e.message for e in schema_errs], + "detail": "promotion would produce a GlossaryTerm that fails GlossaryTerm.json; refusing"} return {"promoted": True, "term": term["id"], "status": "approved", "approvedTerm": approved} diff --git a/tools/validate_glossary_promotion.py b/tools/validate_glossary_promotion.py index 7970f5c..d57b82c 100644 --- a/tools/validate_glossary_promotion.py +++ b/tools/validate_glossary_promotion.py @@ -36,11 +36,12 @@ def main() -> int: CHECKS["full-alignment:promotes"] = True approved = r["approvedTerm"] - # 1b. The promoted output must PASS #250's drift-guard (no governance hole). Feed the - # approved term + its peer so the reciprocity lookup resolves. + # 1b. The promoted output must PASS #250's drift-guard (no governance hole of ANY kind). + # check_alignment prefixes each message with the dict KEY, so key by id — then any + # failure about the promoted term (reciprocity, hole, binding, ...) starts with its id. G250.FAILURES.clear() - G250.check_alignment({"approved": approved, "peer": peer}) - holes = [m for m in G250.FAILURES if approved["id"] in m or "governance hole" in m] + G250.check_alignment({approved["id"]: approved, peer["id"]: peer}) + holes = [m for m in G250.FAILURES if m.startswith(approved["id"] + ":")] if holes: FAILURES.append(f"promoted term fails #250 alignment drift-guard: {holes}") else: @@ -71,6 +72,26 @@ def main() -> int: else: CHECKS["off-space-embedding:refused"] = True + # 4b. Missing cosine (schema-required vectorLink field) -> refused (can't mint schema-invalid). + nocos = copy.deepcopy(alignment) + nocos["vectorLink"].pop("cosine", None) + r = P.promote(draft, nocos, peer) + if r.get("promoted") or not any("cosine" in m for m in r.get("unaligned", [])): + FAILURES.append("a vectorLink missing cosine must be refused (schema-required)") + else: + CHECKS["missing-cosine:refused"] = True + + # 4c. The #250-guard filter must actually CATCH a holey approved term (regression on the + # id-keying fix): a hand-built approved term with no estateBinding must be flagged. + G250.FAILURES.clear() + holey = {**draft, "status": "approved", + "alignment": {k: v for k, v in alignment.items() if k != "estateBinding"}} + G250.check_alignment({holey["id"]: holey, peer["id"]: peer}) + if not [m for m in G250.FAILURES if m.startswith(holey["id"] + ":")]: + FAILURES.append("the #250-guard filter failed to catch a holey approved term (filter regression)") + else: + CHECKS["250-guard-filter:catches-hole"] = True + # 5. Input guards — a non-term and an already-approved term are both refused (no silent mutation). r = P.promote({"id": "x", "type": "NotATerm"}, alignment, peer) if r.get("promoted") or r.get("refused") != "not-a-draft-glossary-term":