diff --git a/AGENTS.md b/AGENTS.md index 9b64cda..733f317 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,7 +16,7 @@ description: | 1. `CANON.md` 2. `docs/GONOL_LANGUAGE_BOUNDARY.md` -3. `.agents/skills/gonol-build/SKILL.md` for gonol, character, morphology, definition, recursive-relation, or affixiation work +3. `.agents/skills/gonol-build/SKILL.md` for gonol, character, word, definition, recursive-relation, or affixiation work 4. `README.md` 5. `docs/UCNS_EDCM_EXPERIMENT_PROGRAM.md` 6. `CLAUDE.md` @@ -56,6 +56,7 @@ EDCM owns the character-admission profile and linguistic/semantic construction. - `edcm/measurement/` is the frozen maintained baseline candidate. - `edcm.ucns_edcm_experiments` is the first joint experiment runner. - EDCM owns current text-domain gonol admission and construction; measurement remains separately frozen and evaluated. +- `edcm.gonol` is the implemented candidate constructor for closing gonols through declared scale option sets. None is selected canon. - UCNS structural policies, product-character candidates, faithful-breadth candidates, and unresolved Möbius coupling laws remain noncanonical unless current UCNS authority says otherwise. - EDCM axes, thresholds, marker lists, and circuit parameters remain candidates unless an explicit canon decision says otherwise. - A passing hypothesis is experiment-supported evidence, not canon. @@ -103,7 +104,7 @@ python -m twine check dist/* ## Usage guidance -For new text-gonol construction, begin in EDCM. Resolve the exact EDCM source/admission profile, import METAPAT affixiation invariants without redefining them, consume current UCNS geometry, build the declared construction, and replay it independently before claiming completion. Freeze any EDCM measurement only after construction and keep its evidence status separate. +For new text-gonol construction, begin in EDCM. Resolve the exact EDCM source/admission profile, import METAPAT affixiation invariants without redefining them, consume current UCNS geometry where normally available, and close gonols with `edcm.gonol` scale option sets. Once closed, a gonol is atomic at any scale; admissible larger-scale construction may consume closed gonols directly without reopening. Replay independently before claiming completion. Freeze any EDCM measurement only after construction and keep its evidence status separate. For historical experiments, reproduce the exact historical producer epoch and names. Historical UCNS-owned lexical artifacts remain evidence; they do not restore current language authority to UCNS. diff --git a/CANON.md b/CANON.md index c1d7c51..f1ceba1 100644 --- a/CANON.md +++ b/CANON.md @@ -56,19 +56,9 @@ every admitted character is a gonol This is an EDCM text-domain rule. EDCM owns the declared source/profile that determines what is admitted as a character. If a construction has not yet selected its exact unit—Unicode code point, grapheme, Public Gonol glyph inventory, or another explicit unit—that admission boundary remains `hmmm`. UCNS may realize admitted characters geometrically; UCNS does not own the linguistic admission rule merely because it owns the geometry. -The current EDCM construction dependency is: +Current EDCM text construction uses declared scale option sets rather than a mandatory adjacent-scale ladder. `edcm.gonol` is the implemented candidate constructor for closing one gonol through a declared scale option set. Admissible scales may include character, word, suffix, suffix-coupling, definition, recursive relation, morphology, punctuation-function, source-specific, or later named scales when the governing construction declares their options. -```text -characters - ↓ -morphology - ↓ -definitions - ↓ -recursive gonol relations -``` - -This is a dependency order, not permission to flatten completed gonols or impose conventional NLP intermediate scales. A completed gonol is atomic for participation at its consuming scale while constituent identity and provenance remain recoverable. +Once closed, a gonol is atomic at any scale. Closed gonols may participate directly at any admissible scale without reopening, while identity, order, multiplicity, source positions, relation identity, and provenance remain recoverable. Suffix-coupling exceptions are carried by the closed suffix gonol, such as `ing` carrying `suffix-coupling.final-y-after-consonant = preserve-y`, rather than by a global morphology law. Historical morphology evidence remains historical; the complete English morphology law remains `hmmm`. Affixiation is defined by METAPAT, not by EDCM and not by UCNS. EDCM applies affixiation to text-domain gonols. UCNS owns any exact geometric realization on the native Möbius/Public Gonol carrier. An unresolved UCNS coupling law remains `hmmm`; neither EDCM nor UCNS may fill it by importing an invented carrier, topology, scale increment, arity rule, containment rule, or linguistic convention. @@ -245,7 +235,7 @@ This phase may implement: - positive unresolved-reference and alternative-bundle evidence; - reproducible reports and policy-preservation findings; - CI execution against exact UCNS commits where experiments require them; -- source-bound EDCM character, morphology, definition, and recursive-gonol candidate constructors under the current authority split; +- source-bound EDCM gonol candidate construction through declared scale option sets under the current authority split; - candidate UCNS Möbius-carrier realizations of affixiation under fixed METAPAT invariants; and - preregistered EDCM measurements of completed text-gonol constructions. diff --git a/CLAUDE.md b/CLAUDE.md index efede29..06b1388 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -99,19 +99,7 @@ every admitted character is a gonol This is an EDCM text-domain rule. EDCM owns the source/profile that decides what is admitted as a character. If a profile has not selected Unicode code point, grapheme, exact Public Gonol glyph inventory, or another explicit unit, that admission boundary remains `hmmm`; UCNS does not silently decide it merely because UCNS supplies the geometry. -The active construction dependency is: - -```text -characters - ↓ -morphology - ↓ -definitions - ↓ -recursive gonol relations -``` - -Relationships that constitute a gonol enter the construction. Completed gonols are atomic for participation at the consuming scale without becoming structureless or provenance-free. Do not reopen a completed gonol or impose a conventional NLP intermediate scale merely because doing so simplifies later processing. +Active construction uses declared scale option sets rather than a mandatory adjacent-scale ladder. `edcm.gonol` is the implemented candidate constructor for closing one gonol at a declared scale. Once closed, a gonol is atomic at any scale; do not reopen a completed gonol or invent UCNS coupling geometry. Do not insert a conventional NLP tokenizer, token IDs, subword pieces, opaque external vector lookup, or whole-string hash as a substitute for gonol identity. Source prose, dictionaries, corpora, labels, and annotations may remain evidence and provenance; they become gonol semantics only through a source-bound EDCM construction whose admission, relation, closure, and replay boundaries are explicit. diff --git a/README.md b/README.md index dd36030..75b3b0e 100644 --- a/README.md +++ b/README.md @@ -62,19 +62,9 @@ every admitted character is a gonol EDCM owns the source/profile that determines what is admitted as a character. If a profile has not selected Unicode code point, grapheme, exact Public Gonol glyph inventory, or another explicit unit, that admission boundary remains `hmmm`; UCNS does not silently choose it merely because UCNS owns the geometry. -The current EDCM construction dependency is: +Current EDCM text construction uses declared scale option sets rather than a mandatory adjacent-scale ladder. `edcm.gonol` is the implemented candidate constructor for closing one gonol at an admissible scale. None of its scale options is selected canon, and construction does not activate measurement. Suffix-coupling exceptions are carried by the closed suffix gonol, such as `ing` carrying `suffix-coupling.final-y-after-consonant = preserve-y`, rather than by a global morphology law. UCNS coupling geometry remains `hmmm`. -```text -characters - ↓ -morphology - ↓ -definitions - ↓ -recursive gonol relations -``` - -Affixiation is defined by METAPAT. UCNS owns any exact geometric realization. EDCM applies affixiation to text-domain gonols. Completed gonols are atomic for participation at the consuming scale while constituent identity and provenance remain recoverable. +Affixiation is defined by METAPAT. UCNS owns any exact geometric realization. EDCM applies affixiation to text-domain gonols. Once closed, a gonol is atomic at any scale. Closed gonols may participate directly at any admissible scale without reopening, while constituent identity and provenance remain recoverable. Do not insert conventional NLP token IDs, subword pieces, opaque external embedding vectors, or whole-string hashes as substitutes for gonol identity. Source prose, dictionaries, corpora, labels, and annotations may remain evidence and provenance; they become gonol semantics only through explicit EDCM construction. diff --git a/docs/ENGLISH_LEXICAL_FLOOR.md b/docs/ENGLISH_LEXICAL_FLOOR.md index 2451909..d3ceaae 100644 --- a/docs/ENGLISH_LEXICAL_FLOOR.md +++ b/docs/ENGLISH_LEXICAL_FLOOR.md @@ -50,19 +50,9 @@ Intrinsic `*.ucns.json` files contain only dense integer occurrence addresses, o This sealed run does **not** define the current text basis. -For active EDCM text construction: - -```text -every admitted character is a gonol - ↓ -morphology - ↓ -definitions - ↓ -recursive gonol relations -``` +For active EDCM text construction, every admitted character is a gonol, and later closures use declared scale option sets rather than a mandatory adjacent-scale ladder. -This construction is EDCM-owned. METAPAT supplies the conceptual affixiation contract. UCNS supplies the gonol/Möbius/Public Gonol geometry and only those function operations that geometry establishes. The precise EDCM source/admission profile and unresolved UCNS coupling geometry remain explicit boundaries rather than being inferred from this historical OEWN experiment. +This construction is EDCM-owned. METAPAT supplies the conceptual affixiation contract. UCNS supplies the gonol/Möbius/Public Gonol geometry and only those function operations that geometry establishes. Once closed, a gonol is atomic at any scale. The precise EDCM source/admission profile and unresolved UCNS coupling geometry remain explicit boundaries rather than being inferred from this historical OEWN experiment. The former rule requiring definitions to close over a fixed NGSL word set is deprecated. OEWN definition strings loaded by this historical EDCM run remain source prose and provenance evidence; they are not automatically promoted into current EDCM gonol semantics. diff --git a/docs/GONOL_LANGUAGE_BOUNDARY.md b/docs/GONOL_LANGUAGE_BOUNDARY.md index fec0ab1..76d9f57 100644 --- a/docs/GONOL_LANGUAGE_BOUNDARY.md +++ b/docs/GONOL_LANGUAGE_BOUNDARY.md @@ -46,21 +46,11 @@ Every admitted occurrence remains separately addressable. Repeated characters ma ## Active text construction -The current EDCM construction dependency is: +Current EDCM text construction uses declared scale option sets rather than a mandatory adjacent-scale ladder. Every admitted character is a gonol, but EDCM must not encode `character -> word -> definition -> recursive` as the only lawful construction path. -```text -characters - ↓ -morphology - ↓ -definitions - ↓ -recursive gonol relations -``` +Once closed, a gonol is atomic at any scale. Closed gonols may participate directly at any admissible scale without reopening, while identity, order, multiplicity, source positions, relation identity, and provenance remain recoverable. -This is a dependency order, not a demand to reopen every completed object at every later scale. A completed gonol is atomic for participation at its consuming scale while retaining recoverable constituent identity and provenance. - -Words and other intermediate closures may be used where an explicit EDCM construction requires them. EDCM must not manufacture a mandatory intermediate scale merely because a conventional NLP pipeline normally contains one. +`edcm.gonol` is the implemented candidate constructor for closing one gonol through a declared scale option set. Its current option sets include character, word, suffix, suffix-coupling, definition, and recursive relation scales. None is selected canon. UCNS coupling geometry remains `hmmm`, and absent `ucns.public_gonol` does not prevent candidate construction. Relationships that constitute a gonol enter the construction. They are not merely external semantic edges. Sidecars may index, cache, project, or record provenance; they do not replace intrinsic relational content. @@ -74,6 +64,8 @@ UCNS owns any exact geometric realization of that operation. The native Möbius/ EDCM applies affixiation to text-domain gonols. Linguistic prefixes and suffixes are one instance of affixiation; they do not define affixiation. +Suffix-coupling options that affect a suffix relation are carried by the relevant closed suffix gonol. For example, a closed `ing` suffix gonol may carry `suffix-coupling.final-y-after-consonant = preserve-y`; the suffix-coupling construction preserves that option through the suffix participant instead of storing an `ing` exception in a global morphology law or reopening the suffix. + The complete English root, stem, affix, irregular-transformation, and family law remains unresolved unless source-backed EDCM evidence establishes it. EDCM must not invent decomposition to complete a pipeline. ## Public Gonol function positions @@ -124,13 +116,58 @@ Preflight real resource requirements before a complete run. Once a healthy admit ## Usage guidance +Copy-pasteable unified candidate: + +```python +from edcm.gonol import construct_gonol, replay_gonol + +word = construct_gonol(scale="word", source="try", source_id="example:try") +ing = construct_gonol( + scale="suffix", + source="ing", + source_id="example:ing", + carried_options=(("suffix-coupling.final-y-after-consonant", "preserve-y"),), +) +definition = construct_gonol( + scale="definition", + relation="example:definition-evidence", + source="to divide with a sharp edge", + participants=(word.gonol,), + source_id="example:cut#1", +) +recursive = construct_gonol( + scale="recursive", + relation="example:mentions", + participants=(word.gonol, definition.gonol), + source_id="example:relation#1", +) +suffix_coupling = construct_gonol( + scale="suffix-coupling", + participants=(word.gonol, ing.gonol), + source_id="example:trying#1", +) +assert recursive.receipt_digest == replay_gonol( + scale="recursive", + relation="example:mentions", + participants=(word.gonol, definition.gonol), + source_id="example:relation#1", +).receipt_digest +assert suffix_coupling.receipt_digest == replay_gonol( + scale="suffix-coupling", + participants=(word.gonol, ing.gonol), + source_id="example:trying#1", +).receipt_digest +``` + +If `ucns.public_gonol` is normally importable and its digest matches the pinned Public Gonol identity, `edcm.gonol` observes source-unit positions. If it is absent, construction records geometry as `hmmm` and still closes the candidate. It does not mutate `sys.path`. + For new text-gonol work: 1. start in EDCM; 2. resolve the exact EDCM source/admission profile; 3. import current METAPAT affixiation invariants rather than redefining them; 4. consume current UCNS geometry rather than moving text semantics into UCNS; -5. construct characters, morphology, definitions, and recursive relations under explicit EDCM receipts; +5. close gonols through declared scale option sets under explicit EDCM receipts; use `edcm.gonol`; 6. keep unresolved UCNS geometric operations as `hmmm`; 7. replay the complete declared construction before claiming completion; and 8. freeze any later EDCM measurement separately. @@ -142,5 +179,6 @@ For UCNS geometry work, work in UCNS. For changes to the meaning of affixiation, - the exact EDCM character-admission unit for any source/profile that has not yet selected one; - the source-supported complete English morphology law; - the exact UCNS Möbius-carrier affixiation/coupling law exposed by implementation; +- which scale option sets and recursive relations, if any, are later selected, and the exact UCNS geometry for those relations; - executable direct coupling across distant recursive scales; - the EDCM projection, information-loss accounting, metric, benchmark, and falsifier for recursive text-gonol evaluation. diff --git a/docs/integrity-gates.md b/docs/integrity-gates.md index 7d65c9f..4993f26 100644 --- a/docs/integrity-gates.md +++ b/docs/integrity-gates.md @@ -76,7 +76,7 @@ This prevents a false green where source tests pass while packaged resources or ## Skill-lib and msdmd EDCM vendors a bounded build/evidence subset from -`The-Interdependency/skill-lib@2b24be24947223b86440f59f1bd9766130f9cc11`. +`The-Interdependency/skill-lib@a1c6a7124af537ee9937b6fc6084940091982fe5`. The skill-compliance workflow checks those files byte-for-byte, generates the canonical `edcm_msdmd.ts` collection, compares it with the tracked collection, and runs the EDCM-native metadata validator. @@ -86,7 +86,7 @@ Local validation: ```bash python /path/to/skill-lib/tools/check_consumer_drift.py . \ --canon-root /path/to/skill-lib \ - --sha 2b24be24947223b86440f59f1bd9766130f9cc11 \ + --sha a1c6a7124af537ee9937b6fc6084940091982fe5 \ --strict-sha --require-vendored PYTHONPATH=/path/to/skill-lib python /path/to/skill-lib/msdmd/collect.py \ diff --git a/docs/ucns-adapter.md b/docs/ucns-adapter.md index 3f0c6c7..6b75530 100644 --- a/docs/ucns-adapter.md +++ b/docs/ucns-adapter.md @@ -188,7 +188,7 @@ New text-gonol work starts in EDCM and follows the active contract: - METAPAT supplies affixiation semantics without being redefined downstream; - UCNS supplies the current gonol/Möbius/Public Gonol geometry; - unresolved UCNS operations remain `hmmm` rather than being inferred from Unicode names, dictionary definitions, adjacency, glyph shape, or conventional grammar; -- EDCM constructs morphology, definitions, and recursive gonol relations while preserving identity, occurrence, order, multiplicity, relation identity, scale, source, and provenance; +- EDCM closes gonols through declared scale option sets while preserving identity, occurrence, order, multiplicity, relation identity, scale, source, and provenance; `edcm.gonol` is the implemented candidate constructor; - completion claims require a deterministic receipt and independent complete replay; and - EDCM measurement remains a separate projection with explicit information loss and falsifiers. diff --git a/edcm/gonol.py b/edcm/gonol.py new file mode 100644 index 0000000..1534a7c --- /dev/null +++ b/edcm/gonol.py @@ -0,0 +1,612 @@ +"""Unified EDCM gonol candidate constructor. + +Usage guidance +-------------- +This is an implemented candidate, not selected canon. It closes one gonol at a +declared scale using the scale's option set. It does not encode a mandatory +``character -> word -> definition -> recursive`` ladder. + + from edcm.gonol import construct_gonol, replay_gonol + + word = construct_gonol(scale="word", source="try", source_id="example:try") + ing = construct_gonol( + scale="suffix", + source="ing", + source_id="example:ing", + carried_options=(("suffix-coupling.final-y-after-consonant", "preserve-y"),), + ) + rel = construct_gonol( + scale="suffix-coupling", + participants=(word.gonol, ing.gonol), + source_id="example:trying#1", + ) + assert rel.receipt_digest == replay_gonol( + scale="suffix-coupling", + participants=(word.gonol, ing.gonol), + source_id="example:trying#1", + ).receipt_digest + +Frozen choices for ``edcm.gonol/v1``: + +- construction is one constructor selected by a scale option set; +- once closed, a gonol is atomic at any scale; +- closed gonols may participate directly at any admissible scale without + reopening; +- source strings are exact Unicode scalar sequences: no normalization, case + folding, trimming, deduplication, or token substitution; +- relation identity is exact caller-supplied text where the option set requires + it; +- suffix-coupling exceptions are carried by the closed suffix gonol, not by a + global morphology law or by reopening the suffix during coupling; +- UCNS Public Gonol geometry is consumed only when normally importable, and + absence remains ``hmmm`` rather than a base-package failure; +- no UCNS function operation or Mobius coupling law is invented. +""" + +# === MODULE_BUILD === +# id: edcm_gonol +# module_name: gonol +# module_kind: engine +# summary: unified EDCM candidate constructor that closes gonols through declared scale option sets while preserving closed-gonol atomicity, carried suffix options, deterministic replay, and UCNS/METAPAT authority boundaries +# owner: Erin Spencer +# public_surface: CONSTRUCTOR_ID, CONSTRUCTOR_VERSION, PINNED_PUBLIC_GONOL_SHA256, ScaleOptionSet, ClosedGonol, GonolReceipt, GonolConstructionError, SCALE_OPTION_SETS, construct_gonol, replay_gonol, canonical_receipt_bytes +# internal_surface: _option_set, _require_text, _source_units, _closed_participants, _carried_option_pairs, _has_suffix_coupling_options, _relation_value, _load_optional_public_gonol, _geometry_observation, _participant_payload, _atomic_payload, _receipt_payload, _digest +# auth_boundary: EDCM owns text-domain closure; UCNS Public Gonol geometry is optional observation unless normally importable with matching digest; METAPAT affixiation semantics are consumed, not redefined +# storage_boundary: none; receipts remain caller-owned in-memory objects +# network_boundary: none +# user_data_boundary: caller-supplied source, relation, participants, and source_id remain in memory and are not transmitted +# admin_only: false +# tests: tests.test_gonol_constructor +# rollout: explicit candidate constructor; no canon selection, measurement activation, UCNS function operation, or Mobius coupling promotion +# rollback: remove this module; historical lexical-floor and UCNS observation adapters remain unchanged +# requires: none +# since: 2026-08-22 +# unresolved: exact UCNS geometric operation of Public Gonol function positions; Mobius-carrier affixiation/coupling law; which scales and relations are later selected; complete English morphology law +# === END MODULE_BUILD === + +# === CONTRACTS === +# id: single_constructor_uses_scale_option_sets +# given: a caller closes source evidence or closed gonol participants +# then: edcm.gonol uses the declared scale option set rather than dispatching through specialized ladder constructors +# class: construction +# since: 2026-08-22 +# +# id: closed_gonol_atomic_at_any_scale +# given: a closed gonol participates in another construction +# then: the participant is consumed by atomic identity while recoverable provenance and nested structure remain available +# class: construction +# since: 2026-08-22 +# +# id: suffix_exception_carried_by_suffix_gonol +# given: suffix coupling has a final-y exception such as ing preserving y after a consonant +# then: the exception is stored on the closed suffix gonol participant and replayed through participant provenance rather than global morphology law +# class: construction +# since: 2026-08-22 +# +# id: construction_survives_absent_ucns_geometry +# given: ucns.public_gonol is not normally importable +# then: construction records geometry as hmmm and does not mutate sys.path or fail base-package CI +# class: safety +# since: 2026-08-22 +# +# id: geometry_mismatch_fails_closed +# given: importable UCNS Public Gonol geometry has a digest different from the pinned identity +# then: construction raises rather than consuming or copying mismatched geometry +# class: safety +# since: 2026-08-22 +# +# id: unified_candidate_does_not_select_canon +# given: a receipt is minted +# then: standing is implemented-candidate, selection_effect is none, and measurement, UCNS operation, and METAPAT promotion remain nonclaims +# class: doctrine +# since: 2026-08-22 +# === END CONTRACTS === + +from __future__ import annotations + +from dataclasses import dataclass +from hashlib import sha256 +import importlib +import json +from types import ModuleType +from typing import Any, Mapping, Sequence + + +CONSTRUCTOR_ID = "edcm.gonol" +CONSTRUCTOR_VERSION = "v1" +PINNED_PUBLIC_GONOL_SHA256 = ( + "55d10c84529a4d7bc7714786357e977b68d9df2ac3f73d20e229580b552c2ef5" +) +STANDING = "implemented-candidate" +SELECTION_EFFECT = "none" + +NONCLAIMS: tuple[str, ...] = ( + "not selected canon", + "not EDCM measurement validity", + "not a mandatory character-word-definition-recursive ladder", + "not complete English morphology law", + "not a UCNS geometric function operation", + "not a UCNS Mobius coupling law", + "not METAPAT canon promotion", +) + +HMMM: tuple[str, ...] = ( + "exact UCNS geometric operation of each Public Gonol function position", + "UCNS Mobius-carrier affixiation/coupling law", + "which scales and relations, if any, are later selected", + "source-supported complete English morphology law", +) + + +class GonolConstructionError(RuntimeError): + """Fail-closed constructor error.""" + + +@dataclass(frozen=True, slots=True) +class ScaleOptionSet: + """Frozen options for one admissible gonol scale.""" + + scale: str + option_set_id: str + source_policy: str + participant_policy: str + relation_policy: str + default_relation: str | None + closure_policy: str + geometry_policy: str + carried_option_policy: str + + +SCALE_OPTION_SETS: Mapping[str, ScaleOptionSet] = { + "character": ScaleOptionSet( + scale="character", + option_set_id="edcm.gonol.scale.character/v1", + source_policy="exactly-one-unicode-scalar", + participant_policy="none", + relation_policy="declared-default", + default_relation="admitted-character", + closure_policy="close-one-source-unit", + geometry_policy="observe-public-gonol-position-if-importable", + carried_option_policy="none", + ), + "word": ScaleOptionSet( + scale="word", + option_set_id="edcm.gonol.scale.word/v1", + source_policy="exact-source-string-or-closed-participants", + participant_policy="any-closed-gonols-without-reopening", + relation_policy="caller-supplied-or-declared-default", + default_relation="word-closure", + closure_policy="close-declared-word-scale-object", + geometry_policy="observe-public-gonol-positions-if-importable", + carried_option_policy="declared-on-closed-gonol", + ), + "suffix": ScaleOptionSet( + scale="suffix", + option_set_id="edcm.gonol.scale.suffix/v1", + source_policy="exact-source-string", + participant_policy="none", + relation_policy="declared-default", + default_relation="suffix-form", + closure_policy="close-declared-suffix-scale-object", + geometry_policy="observe-public-gonol-positions-if-importable", + carried_option_policy="declared-on-closed-suffix-gonol", + ), + "suffix-coupling": ScaleOptionSet( + scale="suffix-coupling", + option_set_id="edcm.gonol.scale.suffix-coupling/v1", + source_policy="optional-exact-source-evidence", + participant_policy="closed-base-and-closed-suffix-without-reopening", + relation_policy="caller-supplied-or-declared-default", + default_relation="suffix-coupling", + closure_policy="close-relation-over-atomic-base-and-suffix", + geometry_policy="observe-public-gonol-positions-if-importable", + carried_option_policy="consume-carried-options-from-suffix-participant", + ), + "definition": ScaleOptionSet( + scale="definition", + option_set_id="edcm.gonol.scale.definition/v1", + source_policy="exact-source-string-or-closed-participants", + participant_policy="any-closed-gonols-without-reopening", + relation_policy="caller-supplied-or-declared-default", + default_relation="definition-evidence", + closure_policy="close-declared-definition-scale-object", + geometry_policy="observe-public-gonol-positions-if-importable", + carried_option_policy="declared-on-closed-gonol", + ), + "recursive": ScaleOptionSet( + scale="recursive", + option_set_id="edcm.gonol.scale.recursive/v1", + source_policy="optional-exact-source-evidence", + participant_policy="any-closed-gonols-without-reopening", + relation_policy="caller-supplied-required", + default_relation=None, + closure_policy="close-relation-over-atomic-participants", + geometry_policy="observe-public-gonol-positions-if-importable", + carried_option_policy="declared-on-closed-gonol", + ), +} + + +@dataclass(frozen=True, slots=True) +class ClosedGonol: + """Closed gonol value. Atomic identity participates; internals remain recoverable.""" + + occurrence: int + scale: str + option_set_id: str + relation: str + source_id: str + source_units: tuple[str, ...] + participants: tuple["ClosedGonol", ...] + carried_options: tuple[tuple[str, str], ...] + atomic_id: str + provenance: tuple[tuple[str, str], ...] + + @property + def kind_id(self) -> tuple[str, str]: + return (self.scale, self.atomic_id) + + +@dataclass(frozen=True, slots=True) +class GonolReceipt: + """Deterministic construction receipt. Digest is replay identity.""" + + constructor_id: str + constructor_version: str + standing: str + selection_effect: str + source_id: str + option_set: ScaleOptionSet + gonol: ClosedGonol + geometry: Mapping[str, Any] + nonclaims: tuple[str, ...] + hmmm: tuple[str, ...] + receipt_digest: str + + +def _option_set(scale: str) -> ScaleOptionSet: + if not isinstance(scale, str) or not scale: + raise GonolConstructionError("scale must be a non-empty string") + try: + return SCALE_OPTION_SETS[scale] + except KeyError as exc: + allowed = ", ".join(sorted(SCALE_OPTION_SETS)) + raise GonolConstructionError(f"scale must be one of: {allowed}") from exc + + +def _require_text(value: str, *, field: str, allow_empty: bool = False) -> str: + if not isinstance(value, str): + raise GonolConstructionError(f"{field} must be an exact Unicode string") + if not allow_empty and not value: + raise GonolConstructionError(f"{field} must be a non-empty string") + return value + + +def _source_units(source: str | None, *, options: ScaleOptionSet) -> tuple[str, ...]: + if source is None: + return () + text = _require_text(source, field="source", allow_empty=False) + units = tuple(text) + for unit in units: + codepoint = ord(unit) + if 0xD800 <= codepoint <= 0xDFFF: + raise GonolConstructionError("surrogate code points are not Unicode scalars") + if options.scale == "character" and len(units) != 1: + raise GonolConstructionError("character scale closes exactly one Unicode scalar") + if options.scale in {"word", "suffix"} and any(unit.isspace() for unit in units): + raise GonolConstructionError( + f"{options.scale} scale source must be one closed source unit, not whitespace-delimited text" + ) + return units + + +def _closed_participants(participants: Sequence[ClosedGonol] | None) -> tuple[ClosedGonol, ...]: + if participants is None: + return () + if not isinstance(participants, Sequence) or isinstance(participants, (str, bytes)): + raise GonolConstructionError("participants must be an ordered sequence of closed gonols") + closed = tuple(participants) + for item in closed: + if not isinstance(item, ClosedGonol): + raise GonolConstructionError("participants must already be closed gonols") + return closed + + +def _carried_option_pairs( + carried_options: Sequence[Sequence[str]] | None, +) -> tuple[tuple[str, str], ...]: + if carried_options is None: + return () + if not isinstance(carried_options, Sequence) or isinstance(carried_options, (str, bytes)): + raise GonolConstructionError("carried_options must be an ordered sequence of exact text pairs") + pairs: list[tuple[str, str]] = [] + for pair in carried_options: + if not isinstance(pair, Sequence) or isinstance(pair, (str, bytes)) or len(pair) != 2: + raise GonolConstructionError("each carried option must be an exact text pair") + key, value = pair + if not isinstance(key, str) or not key or key.isspace(): + raise GonolConstructionError("carried option key must be exact non-empty text") + if not isinstance(value, str) or not value or value.isspace(): + raise GonolConstructionError("carried option value must be exact non-empty text") + pairs.append((key, value)) + return tuple(pairs) + + +def _has_suffix_coupling_options(carried_options: tuple[tuple[str, str], ...]) -> bool: + return any(key.startswith("suffix-coupling.") for key, _value in carried_options) + + +def _relation_value(relation: str | None, *, options: ScaleOptionSet) -> str: + if relation is None: + if options.default_relation is None: + raise GonolConstructionError("relation must be exact caller-supplied text for this scale") + return options.default_relation + if not isinstance(relation, str) or not relation or relation.isspace(): + raise GonolConstructionError("relation must be exact non-empty caller-supplied text") + return relation + + +def _load_optional_public_gonol() -> ModuleType | None: + try: + return importlib.import_module("ucns.public_gonol") + except ImportError: + return None + + +def _geometry_observation(source_units: tuple[str, ...]) -> dict[str, Any]: + module = _load_optional_public_gonol() + if module is None: + return { + "state": "hmmm", + "authority": "ucns.public_gonol", + "reason": "ucns.public_gonol not normally importable", + "positions": [], + } + digest = str(getattr(module, "PUBLIC_GONOL_SHA256", "")) + if digest != PINNED_PUBLIC_GONOL_SHA256: + raise GonolConstructionError( + "UCNS Public Gonol digest mismatch: " + f"constructor pins {PINNED_PUBLIC_GONOL_SHA256}, " + f"imported {digest or 'missing'}" + ) + position_of = getattr(module, "public_gonol_position", None) + if not callable(position_of): + raise GonolConstructionError("UCNS public_gonol is missing public_gonol_position") + return { + "state": "observed", + "authority": "ucns.public_gonol", + "carrier_digest": digest, + "positions": [position_of(unit) for unit in source_units], + } + + +def _kind_payload(value: Any) -> Any: + if isinstance(value, tuple): + return [_kind_payload(item) for item in value] + if isinstance(value, list): + return [_kind_payload(item) for item in value] + return value + + +def _option_payload(options: ScaleOptionSet) -> dict[str, str | None]: + return { + "scale": options.scale, + "option_set_id": options.option_set_id, + "source_policy": options.source_policy, + "participant_policy": options.participant_policy, + "relation_policy": options.relation_policy, + "default_relation": options.default_relation, + "closure_policy": options.closure_policy, + "geometry_policy": options.geometry_policy, + "carried_option_policy": options.carried_option_policy, + } + + +def _participant_payload(item: ClosedGonol) -> dict[str, Any]: + return { + "scale": item.scale, + "occurrence": item.occurrence, + "relation": item.relation, + "source_id": item.source_id, + "kind_id": _kind_payload(item.kind_id), + "atomic_id": item.atomic_id, + "option_set_id": item.option_set_id, + "carried_options": [list(pair) for pair in item.carried_options], + "provenance": [list(pair) for pair in item.provenance], + } + + +def _atomic_payload( + *, + occurrence: int, + source_id: str, + options: ScaleOptionSet, + relation: str, + source_units: tuple[str, ...], + participants: tuple[ClosedGonol, ...], + carried_options: tuple[tuple[str, str], ...], +) -> dict[str, Any]: + return { + "constructor_id": CONSTRUCTOR_ID, + "constructor_version": CONSTRUCTOR_VERSION, + "standing": STANDING, + "selection_effect": SELECTION_EFFECT, + "occurrence": occurrence, + "source_id": source_id, + "option_set": _option_payload(options), + "relation": relation, + "source_units": list(source_units), + "participants": [_participant_payload(item) for item in participants], + "carried_options": [list(pair) for pair in carried_options], + "closure_invariant": "once closed, a gonol is atomic at any scale", + } + + +def _receipt_payload( + *, + source_id: str, + options: ScaleOptionSet, + gonol: ClosedGonol, + geometry: Mapping[str, Any], +) -> dict[str, Any]: + return { + "constructor_id": CONSTRUCTOR_ID, + "constructor_version": CONSTRUCTOR_VERSION, + "standing": STANDING, + "selection_effect": SELECTION_EFFECT, + "source_id": source_id, + "option_set": _option_payload(options), + "gonol": _atomic_payload( + occurrence=gonol.occurrence, + source_id=gonol.source_id, + options=options, + relation=gonol.relation, + source_units=gonol.source_units, + participants=gonol.participants, + carried_options=gonol.carried_options, + ), + "atomic_id": gonol.atomic_id, + "geometry": dict(geometry), + "nonclaims": list(NONCLAIMS), + "hmmm": list(HMMM), + } + + +def canonical_receipt_bytes(payload: Mapping[str, Any]) -> bytes: + """Return stable JSON bytes for construction receipts.""" + + return json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode( + "utf-8" + ) + + +def _digest(payload: Mapping[str, Any]) -> str: + return sha256(canonical_receipt_bytes(payload)).hexdigest() + + +def construct_gonol( + *, + scale: str, + source_id: str, + source: str | None = None, + participants: Sequence[ClosedGonol] | None = None, + relation: str | None = None, + carried_options: Sequence[Sequence[str]] | None = None, + occurrence: int = 0, +) -> GonolReceipt: + """Close one gonol at a declared scale using its option set.""" + + options = _option_set(scale) + source_id = _require_text(source_id, field="source_id") + if isinstance(occurrence, bool) or not isinstance(occurrence, int) or occurrence < 0: + raise GonolConstructionError("occurrence must be a non-negative integer") + units = _source_units(source, options=options) + closed = _closed_participants(participants) + carried = _carried_option_pairs(carried_options) + if options.scale == "character" and closed: + raise GonolConstructionError("character scale does not accept closed participants") + if options.scale == "character" and carried: + raise GonolConstructionError("character scale does not carry declared options") + if options.scale != "suffix" and _has_suffix_coupling_options(carried): + raise GonolConstructionError("suffix-coupling options must be carried by a closed suffix gonol") + if options.scale == "suffix" and closed: + raise GonolConstructionError("suffix scale does not accept closed participants") + if options.scale == "suffix-coupling": + if len(closed) != 2 or closed[1].scale != "suffix": + raise GonolConstructionError( + "suffix-coupling scale requires ordered closed base and closed suffix participants" + ) + if carried: + raise GonolConstructionError( + "suffix-coupling options must be carried by the closed suffix participant" + ) + if not units and not closed: + raise GonolConstructionError("construction requires source evidence or closed participants") + if options.scale == "recursive" and len(closed) < 2: + raise GonolConstructionError("recursive scale requires at least two closed participants") + relation_value = _relation_value(relation, options=options) + geometry = _geometry_observation(units) + atomic_payload = _atomic_payload( + occurrence=occurrence, + source_id=source_id, + options=options, + relation=relation_value, + source_units=units, + participants=closed, + carried_options=carried, + ) + atomic_id = _digest(atomic_payload) + provenance = ( + ("constructor", f"{CONSTRUCTOR_ID}/{CONSTRUCTOR_VERSION}"), + ("source_id", source_id), + ("option_set", options.option_set_id), + ) + tuple(("carried_option", f"{key}={value}") for key, value in carried) + gonol = ClosedGonol( + occurrence=occurrence, + scale=options.scale, + option_set_id=options.option_set_id, + relation=relation_value, + source_id=source_id, + source_units=units, + participants=closed, + carried_options=carried, + atomic_id=atomic_id, + provenance=provenance, + ) + payload = _receipt_payload( + source_id=source_id, + options=options, + gonol=gonol, + geometry=geometry, + ) + return GonolReceipt( + constructor_id=CONSTRUCTOR_ID, + constructor_version=CONSTRUCTOR_VERSION, + standing=STANDING, + selection_effect=SELECTION_EFFECT, + source_id=source_id, + option_set=options, + gonol=gonol, + geometry=geometry, + nonclaims=NONCLAIMS, + hmmm=HMMM, + receipt_digest=_digest(payload), + ) + + +def replay_gonol( + *, + scale: str, + source_id: str, + source: str | None = None, + participants: Sequence[ClosedGonol] | None = None, + relation: str | None = None, + carried_options: Sequence[Sequence[str]] | None = None, + occurrence: int = 0, +) -> GonolReceipt: + """Independently reconstruct the same declared gonol closure.""" + + return construct_gonol( + scale=scale, + source_id=source_id, + source=source, + participants=participants, + relation=relation, + carried_options=carried_options, + occurrence=occurrence, + ) + + +__all__ = [ + "CONSTRUCTOR_ID", + "CONSTRUCTOR_VERSION", + "PINNED_PUBLIC_GONOL_SHA256", + "SCALE_OPTION_SETS", + "ScaleOptionSet", + "ClosedGonol", + "GonolConstructionError", + "GonolReceipt", + "canonical_receipt_bytes", + "construct_gonol", + "replay_gonol", +] diff --git a/edcm_msdmd.ts b/edcm_msdmd.ts index 6b0761f..1d3b327 100644 --- a/edcm_msdmd.ts +++ b/edcm_msdmd.ts @@ -702,6 +702,96 @@ export default defineMsdmdCollection({ "file": "edcm/goal_vector_experiment.py", "id": "edcm_goal_vector_experiment" }, + { + "block": "CONTRACTS", + "fields": { + "class": "construction", + "given": "a closed gonol participates in another construction", + "since": "2026-08-22", + "then": "the participant is consumed by atomic identity while recoverable provenance and nested structure remain available" + }, + "file": "edcm/gonol.py", + "id": "closed_gonol_atomic_at_any_scale" + }, + { + "block": "CONTRACTS", + "fields": { + "class": "safety", + "given": "ucns.public_gonol is not normally importable", + "since": "2026-08-22", + "then": "construction records geometry as hmmm and does not mutate sys.path or fail base-package CI" + }, + "file": "edcm/gonol.py", + "id": "construction_survives_absent_ucns_geometry" + }, + { + "block": "CONTRACTS", + "fields": { + "class": "safety", + "given": "importable UCNS Public Gonol geometry has a digest different from the pinned identity", + "since": "2026-08-22", + "then": "construction raises rather than consuming or copying mismatched geometry" + }, + "file": "edcm/gonol.py", + "id": "geometry_mismatch_fails_closed" + }, + { + "block": "CONTRACTS", + "fields": { + "class": "construction", + "given": "a caller closes source evidence or closed gonol participants", + "since": "2026-08-22", + "then": "edcm.gonol uses the declared scale option set rather than dispatching through specialized ladder constructors" + }, + "file": "edcm/gonol.py", + "id": "single_constructor_uses_scale_option_sets" + }, + { + "block": "CONTRACTS", + "fields": { + "class": "construction", + "given": "suffix coupling has a final-y exception such as ing preserving y after a consonant", + "since": "2026-08-22", + "then": "the exception is stored on the closed suffix gonol participant and replayed through participant provenance rather than global morphology law" + }, + "file": "edcm/gonol.py", + "id": "suffix_exception_carried_by_suffix_gonol" + }, + { + "block": "CONTRACTS", + "fields": { + "class": "doctrine", + "given": "a receipt is minted", + "since": "2026-08-22", + "then": "standing is implemented-candidate, selection_effect is none, and measurement, UCNS operation, and METAPAT promotion remain nonclaims" + }, + "file": "edcm/gonol.py", + "id": "unified_candidate_does_not_select_canon" + }, + { + "block": "MODULE_BUILD", + "fields": { + "admin_only": "false", + "auth_boundary": "EDCM owns text-domain closure; UCNS Public Gonol geometry is optional observation unless normally importable with matching digest; METAPAT affixiation semantics are consumed, not redefined", + "internal_surface": "_option_set, _require_text, _source_units, _closed_participants, _carried_option_pairs, _has_suffix_coupling_options, _relation_value, _load_optional_public_gonol, _geometry_observation, _participant_payload, _atomic_payload, _receipt_payload, _digest", + "module_kind": "engine", + "module_name": "gonol", + "network_boundary": "none", + "owner": "Erin Spencer", + "public_surface": "CONSTRUCTOR_ID, CONSTRUCTOR_VERSION, PINNED_PUBLIC_GONOL_SHA256, ScaleOptionSet, ClosedGonol, GonolReceipt, GonolConstructionError, SCALE_OPTION_SETS, construct_gonol, replay_gonol, canonical_receipt_bytes", + "requires": "none", + "rollback": "remove this module; historical lexical-floor and UCNS observation adapters remain unchanged", + "rollout": "explicit candidate constructor; no canon selection, measurement activation, UCNS function operation, or Mobius coupling promotion", + "since": "2026-08-22", + "storage_boundary": "none; receipts remain caller-owned in-memory objects", + "summary": "unified EDCM candidate constructor that closes gonols through declared scale option sets while preserving closed-gonol atomicity, carried suffix options, deterministic replay, and UCNS/METAPAT authority boundaries", + "tests": "tests.test_gonol_constructor", + "unresolved": "exact UCNS geometric operation of Public Gonol function positions; Mobius-carrier affixiation/coupling law; which scales and relations are later selected; complete English morphology law", + "user_data_boundary": "caller-supplied source, relation, participants, and source_id remain in memory and are not transmitted" + }, + "file": "edcm/gonol.py", + "id": "edcm_gonol" + }, { "block": "MODULE_BUILD", "fields": { @@ -1852,6 +1942,78 @@ export default defineMsdmdCollection({ "file": "tests/test_goal_vector_experiment.py", "id": "check_goal_vector_sealed_evidence" }, + { + "block": "CHECKS", + "fields": { + "call": "self::test_closed_gonols_participate_directly_without_ladder", + "cleanup": "none", + "mutates": "none", + "proves": "closed_gonol_atomic_at_any_scale", + "timeout": "30" + }, + "file": "tests/test_gonol_constructor.py", + "id": "closed_gonol_atomic_at_any_scale_check" + }, + { + "block": "CHECKS", + "fields": { + "call": "self::test_base_construction_survives_absent_ucns_without_sys_path_mutation", + "cleanup": "none", + "mutates": "none", + "proves": "construction_survives_absent_ucns_geometry", + "timeout": "30" + }, + "file": "tests/test_gonol_constructor.py", + "id": "construction_survives_absent_ucns_geometry_check" + }, + { + "block": "CHECKS", + "fields": { + "call": "self::test_digest_mismatch_fails_closed", + "cleanup": "none", + "mutates": "none", + "proves": "geometry_mismatch_fails_closed", + "timeout": "30" + }, + "file": "tests/test_gonol_constructor.py", + "id": "geometry_mismatch_fails_closed_check" + }, + { + "block": "CHECKS", + "fields": { + "call": "self::test_constructor_uses_declared_scale_option_set", + "cleanup": "none", + "mutates": "none", + "proves": "single_constructor_uses_scale_option_sets", + "timeout": "30" + }, + "file": "tests/test_gonol_constructor.py", + "id": "single_constructor_uses_scale_option_sets_check" + }, + { + "block": "CHECKS", + "fields": { + "call": "self::test_suffix_coupling_exception_is_carried_by_closed_suffix", + "cleanup": "none", + "mutates": "none", + "proves": "suffix_exception_carried_by_suffix_gonol", + "timeout": "30" + }, + "file": "tests/test_gonol_constructor.py", + "id": "suffix_exception_carried_by_suffix_gonol_check" + }, + { + "block": "CHECKS", + "fields": { + "call": "self::test_receipt_remains_candidate", + "cleanup": "none", + "mutates": "none", + "proves": "unified_candidate_does_not_select_canon", + "timeout": "30" + }, + "file": "tests/test_gonol_constructor.py", + "id": "unified_candidate_does_not_select_canon_check" + }, { "block": "CHECKS", "fields": { @@ -4663,6 +4825,48 @@ export default defineMsdmdCollection({ "source_id": "check_ucns_edcm_v4_resolvers", "to": "python3" }, + { + "from": "closed_gonol_atomic_at_any_scale_check", + "kind": "calls", + "source_block": "CHECKS", + "source_id": "closed_gonol_atomic_at_any_scale_check", + "to": "self::test_closed_gonols_participate_directly_without_ladder" + }, + { + "from": "closed_gonol_atomic_at_any_scale_check", + "kind": "claims_proves", + "source_block": "CHECKS", + "source_id": "closed_gonol_atomic_at_any_scale_check", + "to": "closed_gonol_atomic_at_any_scale" + }, + { + "from": "construction_survives_absent_ucns_geometry_check", + "kind": "calls", + "source_block": "CHECKS", + "source_id": "construction_survives_absent_ucns_geometry_check", + "to": "self::test_base_construction_survives_absent_ucns_without_sys_path_mutation" + }, + { + "from": "construction_survives_absent_ucns_geometry_check", + "kind": "claims_proves", + "source_block": "CHECKS", + "source_id": "construction_survives_absent_ucns_geometry_check", + "to": "construction_survives_absent_ucns_geometry" + }, + { + "from": "geometry_mismatch_fails_closed_check", + "kind": "calls", + "source_block": "CHECKS", + "source_id": "geometry_mismatch_fails_closed_check", + "to": "self::test_digest_mismatch_fails_closed" + }, + { + "from": "geometry_mismatch_fails_closed_check", + "kind": "claims_proves", + "source_block": "CHECKS", + "source_id": "geometry_mismatch_fails_closed_check", + "to": "geometry_mismatch_fails_closed" + }, { "from": "language_relational_branch_check", "kind": "calls", @@ -4747,6 +4951,48 @@ export default defineMsdmdCollection({ "source_id": "oewn_builder_order_check", "to": "oewn_source_is_exact_pinned_and_resumable" }, + { + "from": "single_constructor_uses_scale_option_sets_check", + "kind": "calls", + "source_block": "CHECKS", + "source_id": "single_constructor_uses_scale_option_sets_check", + "to": "self::test_constructor_uses_declared_scale_option_set" + }, + { + "from": "single_constructor_uses_scale_option_sets_check", + "kind": "claims_proves", + "source_block": "CHECKS", + "source_id": "single_constructor_uses_scale_option_sets_check", + "to": "single_constructor_uses_scale_option_sets" + }, + { + "from": "suffix_exception_carried_by_suffix_gonol_check", + "kind": "calls", + "source_block": "CHECKS", + "source_id": "suffix_exception_carried_by_suffix_gonol_check", + "to": "self::test_suffix_coupling_exception_is_carried_by_closed_suffix" + }, + { + "from": "suffix_exception_carried_by_suffix_gonol_check", + "kind": "claims_proves", + "source_block": "CHECKS", + "source_id": "suffix_exception_carried_by_suffix_gonol_check", + "to": "suffix_exception_carried_by_suffix_gonol" + }, + { + "from": "unified_candidate_does_not_select_canon_check", + "kind": "calls", + "source_block": "CHECKS", + "source_id": "unified_candidate_does_not_select_canon_check", + "to": "self::test_receipt_remains_candidate" + }, + { + "from": "unified_candidate_does_not_select_canon_check", + "kind": "claims_proves", + "source_block": "CHECKS", + "source_id": "unified_candidate_does_not_select_canon_check", + "to": "unified_candidate_does_not_select_canon" + }, { "from": "edcm_ucns_fork_lint_docs", "kind": "covers", @@ -4831,6 +5077,20 @@ export default defineMsdmdCollection({ "source_id": "edcm_goal_vector_experiment", "to": "edcm_ucns_adapter" }, + { + "from": "edcm_gonol", + "kind": "owns", + "source_block": "MODULE_BUILD", + "source_id": "edcm_gonol", + "to": "Erin Spencer" + }, + { + "from": "edcm_gonol", + "kind": "requires", + "source_block": "MODULE_BUILD", + "source_id": "edcm_gonol", + "to": "none" + }, { "from": "edcm_integrity", "kind": "owns", diff --git a/tests/test_gonol_constructor.py b/tests/test_gonol_constructor.py new file mode 100644 index 0000000..17130f1 --- /dev/null +++ b/tests/test_gonol_constructor.py @@ -0,0 +1,264 @@ +# === CHECKS === +# id: single_constructor_uses_scale_option_sets_check +# proves: single_constructor_uses_scale_option_sets +# call: self::test_constructor_uses_declared_scale_option_set +# timeout: 30 +# mutates: none +# cleanup: none +# +# id: closed_gonol_atomic_at_any_scale_check +# proves: closed_gonol_atomic_at_any_scale +# call: self::test_closed_gonols_participate_directly_without_ladder +# timeout: 30 +# mutates: none +# cleanup: none +# +# id: suffix_exception_carried_by_suffix_gonol_check +# proves: suffix_exception_carried_by_suffix_gonol +# call: self::test_suffix_coupling_exception_is_carried_by_closed_suffix +# timeout: 30 +# mutates: none +# cleanup: none +# +# id: construction_survives_absent_ucns_geometry_check +# proves: construction_survives_absent_ucns_geometry +# call: self::test_base_construction_survives_absent_ucns_without_sys_path_mutation +# timeout: 30 +# mutates: none +# cleanup: none +# +# id: geometry_mismatch_fails_closed_check +# proves: geometry_mismatch_fails_closed +# call: self::test_digest_mismatch_fails_closed +# timeout: 30 +# mutates: none +# cleanup: none +# +# id: unified_candidate_does_not_select_canon_check +# proves: unified_candidate_does_not_select_canon +# call: self::test_receipt_remains_candidate +# timeout: 30 +# mutates: none +# cleanup: none +# === END CHECKS === + +from __future__ import annotations + +import sys +from types import SimpleNamespace +import unittest +from unittest.mock import patch + +from edcm.gonol import ( + CONSTRUCTOR_ID, + CONSTRUCTOR_VERSION, + PINNED_PUBLIC_GONOL_SHA256, + SCALE_OPTION_SETS, + GonolConstructionError, + construct_gonol, + replay_gonol, +) + + +class GonolConstructorTest(unittest.TestCase): + def test_constructor_uses_declared_scale_option_set(self) -> None: + receipt = construct_gonol(scale="word", source="cut", source_id="fixture:cut") + self.assertEqual(receipt.constructor_id, CONSTRUCTOR_ID) + self.assertEqual(receipt.constructor_version, CONSTRUCTOR_VERSION) + self.assertEqual(receipt.option_set, SCALE_OPTION_SETS["word"]) + self.assertEqual(receipt.gonol.scale, "word") + self.assertEqual(receipt.gonol.source_units, ("c", "u", "t")) + self.assertEqual(receipt.gonol.relation, "word-closure") + + def test_base_construction_survives_absent_ucns_without_sys_path_mutation(self) -> None: + before = tuple(sys.path) + with patch("edcm.gonol.importlib.import_module", side_effect=ImportError("no ucns")): + receipt = construct_gonol(scale="word", source="cut", source_id="fixture:no-ucns") + self.assertEqual(tuple(sys.path), before) + self.assertEqual(receipt.geometry["state"], "hmmm") + self.assertEqual(receipt.geometry["reason"], "ucns.public_gonol not normally importable") + self.assertEqual(receipt.gonol.kind_id[0], "word") + + def test_importable_geometry_is_observed_without_operation_claim(self) -> None: + fake = SimpleNamespace( + PUBLIC_GONOL_SHA256=PINNED_PUBLIC_GONOL_SHA256, + public_gonol_position=lambda glyph: {" ": 0, "A": 1}.get(glyph), + ) + with patch("edcm.gonol.importlib.import_module", return_value=fake): + receipt = construct_gonol(scale="character", source="A", source_id="fixture:A") + self.assertEqual(receipt.geometry["state"], "observed") + self.assertEqual(receipt.geometry["positions"], [1]) + self.assertIn("not a UCNS geometric function operation", receipt.nonclaims) + self.assertIn("exact UCNS geometric operation", receipt.hmmm[0]) + + def test_digest_mismatch_fails_closed(self) -> None: + fake = SimpleNamespace( + PUBLIC_GONOL_SHA256="0" * 64, + public_gonol_position=lambda _glyph: 0, + ) + with patch("edcm.gonol.importlib.import_module", return_value=fake): + with self.assertRaisesRegex(GonolConstructionError, "digest mismatch"): + construct_gonol(scale="character", source="A", source_id="fixture:mismatch") + + def test_closed_gonols_participate_directly_without_ladder(self) -> None: + character = construct_gonol(scale="character", source="c", source_id="fixture:c") + word = construct_gonol(scale="word", source="cut", source_id="fixture:cut") + definition = construct_gonol( + scale="definition", + relation="fixture:defines-directly", + participants=(character.gonol, word.gonol), + source="direct cross-scale evidence", + source_id="fixture:def", + ) + self.assertEqual(definition.gonol.participants, (character.gonol, word.gonol)) + self.assertEqual([item.scale for item in definition.gonol.participants], ["character", "word"]) + self.assertNotIn("mandatory character-word-definition-recursive ladder", definition.receipt_digest) + self.assertIn("not a mandatory character-word-definition-recursive ladder", definition.nonclaims) + + def test_suffix_coupling_exception_is_carried_by_closed_suffix(self) -> None: + base = construct_gonol(scale="word", source="try", source_id="fixture:try") + ing = construct_gonol( + scale="suffix", + source="ing", + source_id="fixture:ing", + carried_options=(("suffix-coupling.final-y-after-consonant", "preserve-y"),), + ) + coupling = construct_gonol( + scale="suffix-coupling", + participants=(base.gonol, ing.gonol), + source_id="fixture:trying", + ) + + self.assertEqual(coupling.option_set, SCALE_OPTION_SETS["suffix-coupling"]) + self.assertEqual(coupling.gonol.relation, "suffix-coupling") + self.assertEqual(coupling.gonol.participants, (base.gonol, ing.gonol)) + self.assertEqual( + coupling.gonol.participants[1].carried_options, + (("suffix-coupling.final-y-after-consonant", "preserve-y"),), + ) + self.assertEqual(coupling.gonol.carried_options, ()) + self.assertIn(("carried_option", "suffix-coupling.final-y-after-consonant=preserve-y"), ing.gonol.provenance) + self.assertNotIn("final-y-after-consonant", repr(SCALE_OPTION_SETS["suffix-coupling"])) + self.assertNotIn("preserve-y", repr(SCALE_OPTION_SETS["suffix-coupling"])) + + replay = replay_gonol( + scale="suffix-coupling", + participants=(base.gonol, ing.gonol), + source_id="fixture:trying", + ) + self.assertEqual(coupling.receipt_digest, replay.receipt_digest) + + def test_suffix_carried_options_are_part_of_identity_and_fail_closed(self) -> None: + base = construct_gonol(scale="word", source="try", source_id="fixture:try") + ing_preserve = construct_gonol( + scale="suffix", + source="ing", + source_id="fixture:ing", + carried_options=(("suffix-coupling.final-y-after-consonant", "preserve-y"),), + ) + ing_change = construct_gonol( + scale="suffix", + source="ing", + source_id="fixture:ing", + carried_options=(("suffix-coupling.final-y-after-consonant", "change-y-to-i"),), + ) + self.assertNotEqual(ing_preserve.gonol.atomic_id, ing_change.gonol.atomic_id) + + preserved = construct_gonol( + scale="suffix-coupling", + participants=(base.gonol, ing_preserve.gonol), + source_id="fixture:trying", + ) + changed = construct_gonol( + scale="suffix-coupling", + participants=(base.gonol, ing_change.gonol), + source_id="fixture:trying", + ) + self.assertNotEqual(preserved.gonol.atomic_id, changed.gonol.atomic_id) + + with self.assertRaisesRegex(GonolConstructionError, "carried by a closed suffix gonol"): + construct_gonol( + scale="suffix-coupling", + participants=(base.gonol, ing_preserve.gonol), + carried_options=(("suffix-coupling.final-y-after-consonant", "preserve-y"),), + source_id="fixture:bad-carrier", + ) + + with self.assertRaisesRegex(GonolConstructionError, "carried by a closed suffix gonol"): + construct_gonol( + scale="word", + source="try", + source_id="fixture:bad-word-carrier", + carried_options=(("suffix-coupling.final-y-after-consonant", "preserve-y"),), + ) + + with self.assertRaisesRegex(GonolConstructionError, "closed base and closed suffix"): + construct_gonol( + scale="suffix-coupling", + participants=(ing_preserve.gonol, base.gonol), + source_id="fixture:bad-order", + ) + + def test_recursive_requires_relation_and_two_closed_participants(self) -> None: + word = construct_gonol(scale="word", source="cut", source_id="fixture:one") + with self.assertRaisesRegex(GonolConstructionError, "relation must be exact"): + construct_gonol(scale="recursive", participants=(word.gonol, word.gonol), source_id="bad") + with self.assertRaisesRegex(GonolConstructionError, "at least two closed participants"): + construct_gonol( + scale="recursive", + relation="fixture:loop", + participants=(word.gonol,), + source_id="bad-one", + ) + + def test_order_and_multiplicity_are_preserved_in_atomic_identity(self) -> None: + a = construct_gonol(scale="character", source="a", source_id="fixture:a") + b = construct_gonol(scale="character", source="b", source_id="fixture:b") + first = construct_gonol( + scale="recursive", + relation="fixture:sequence", + participants=(a.gonol, a.gonol, b.gonol), + source_id="fixture:seq-aab", + ) + second = construct_gonol( + scale="recursive", + relation="fixture:sequence", + participants=(a.gonol, b.gonol, a.gonol), + source_id="fixture:seq-aba", + ) + self.assertNotEqual(first.gonol.atomic_id, second.gonol.atomic_id) + self.assertEqual(first.gonol.participants[0], first.gonol.participants[1]) + self.assertEqual([item.source_id for item in first.gonol.participants], ["fixture:a", "fixture:a", "fixture:b"]) + + def test_replay_matches_byte_identity(self) -> None: + word = construct_gonol(scale="word", source="cut", source_id="fixture:replay-word") + kwargs = { + "scale": "recursive", + "relation": "fixture:pair", + "participants": (word.gonol, word.gonol), + "source_id": "fixture:replay", + } + first = construct_gonol(**kwargs) + second = replay_gonol(**kwargs) + self.assertEqual(first.receipt_digest, second.receipt_digest) + self.assertEqual(first.gonol.atomic_id, second.gonol.atomic_id) + + def test_character_and_word_validation_fail_closed(self) -> None: + with self.assertRaisesRegex(GonolConstructionError, "exactly one Unicode scalar"): + construct_gonol(scale="character", source="ab", source_id="fixture:bad-char") + with self.assertRaisesRegex(GonolConstructionError, "surrogate"): + construct_gonol(scale="character", source="\ud800", source_id="fixture:bad-surrogate") + with self.assertRaisesRegex(GonolConstructionError, "whitespace-delimited"): + construct_gonol(scale="word", source="two words", source_id="fixture:bad-word") + + def test_receipt_remains_candidate(self) -> None: + receipt = construct_gonol(scale="word", source="cut", source_id="fixture:standing") + self.assertEqual(receipt.standing, "implemented-candidate") + self.assertEqual(receipt.selection_effect, "none") + self.assertIn("not selected canon", receipt.nonclaims) + self.assertIn("not EDCM measurement validity", receipt.nonclaims) + self.assertIn("which scales and relations, if any, are later selected", receipt.hmmm) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_gonol_language_authority.py b/tests/test_gonol_language_authority.py index d8b1acd..e412d2c 100644 --- a/tests/test_gonol_language_authority.py +++ b/tests/test_gonol_language_authority.py @@ -31,6 +31,13 @@ def test_live_surfaces_route_gonol_authority_consistently() -> None: assert "METAPAT defines affixiation" in boundary assert "UCNS owns any exact geometric realization" in boundary assert "EDCM applies affixiation to text-domain gonols" in boundary + for text in (boundary, canon, agents): + assert "scale option sets" in text + assert "edcm.gonol" in text + assert "Once closed, a gonol is atomic at any scale" in text + assert "mandatory adjacent-scale ladder" in boundary + assert "Closed gonols may participate directly at any admissible scale" in boundary + assert "does not mutate `sys.path`" in boundary def test_live_surfaces_do_not_restore_ucns_language_ownership() -> None: