|
| 1 | +------------------------------- MODULE Normalizer ------------------------------- |
| 2 | +\* SPDX-License-Identifier: PMPL-1.0-or-later |
| 3 | +\* Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> |
| 4 | +\* |
| 5 | +\* V9: Normalizer determinism + convergence. |
| 6 | +\* Corresponds to rust-core/verisim-normalizer/src/lib.rs (StorageRegenerator). |
| 7 | +\* |
| 8 | +\* The verisim-normalizer resolves drift between the 8 modalities of an octad |
| 9 | +\* by picking an authoritative source modality and regenerating the others |
| 10 | +\* from it. The real system has a (source, target) strategy table -- Document |
| 11 | +\* is the usual authoritative source for Vector/Semantic/Graph regeneration, |
| 12 | +\* with cosine-similarity drift measured against Vector and Jaccard against |
| 13 | +\* Semantic. This spec abstracts that machinery into its essential claim: |
| 14 | +\* |
| 15 | +\* - Determinism: normalisation is a *function* of state. Given the same |
| 16 | +\* input octad, normalisation always produces the same output octad; |
| 17 | +\* there is no schedule-dependent or source-rank-tie non-determinism. |
| 18 | +\* - Convergence: starting from any drift-ed state, repeated normalisation |
| 19 | +\* reaches a drift-free fixed point in bounded time. |
| 20 | +\* |
| 21 | +\* The spec also checks that the fixed point is *stable* (Normalize is |
| 22 | +\* identity on a drift-free state). |
| 23 | + |
| 24 | +EXTENDS Naturals, FiniteSets, TLC |
| 25 | + |
| 26 | +CONSTANTS |
| 27 | + Values, \* abstract set of possible modality payload hashes |
| 28 | + MaxSteps \* bound on normalisation rounds for model-checking |
| 29 | + |
| 30 | +\* Modalities and their deterministic priority ordering are module-level (TLC |
| 31 | +\* config files cannot represent record literals, and the real system has a |
| 32 | +\* fixed strategy table anyway). Priority is strictly injective by |
| 33 | +\* construction here -- that injectivity is exactly what makes the CHOOSE in |
| 34 | +\* SourceOf deterministic, and it is the spec's central structural claim. |
| 35 | +Modalities == {"graph", "vector", "semantic", "document"} |
| 36 | + |
| 37 | +Priority == [graph |-> 1, vector |-> 2, semantic |-> 3, document |-> 4] |
| 38 | + |
| 39 | +ASSUME Cardinality(Values) >= 1 |
| 40 | +ASSUME MaxSteps \in Nat |
| 41 | +ASSUME \A m1, m2 \in Modalities: (m1 /= m2) => (Priority[m1] /= Priority[m2]) |
| 42 | + |
| 43 | +VARIABLES |
| 44 | + state, \* [Modalities -> Values] -- current octad snapshot |
| 45 | + steps \* Nat -- normalisation rounds elapsed |
| 46 | + |
| 47 | +vars == <<state, steps>> |
| 48 | + |
| 49 | +TypeOK == |
| 50 | + /\ state \in [Modalities -> Values] |
| 51 | + /\ steps \in 0..MaxSteps |
| 52 | + |
| 53 | +\* Drift holds when any two modalities disagree on the payload. |
| 54 | +HasDrift(s) == |
| 55 | + \E m1, m2 \in Modalities: s[m1] /= s[m2] |
| 56 | + |
| 57 | +\* The deterministic authoritative-source function. Under the injectivity |
| 58 | +\* ASSUME above, CHOOSE returns the unique highest-priority modality. This |
| 59 | +\* is the single piece of the spec that, if wrong, would make the whole |
| 60 | +\* normaliser non-deterministic -- so it is the thing determinism hinges on. |
| 61 | +SourceOf(s) == |
| 62 | + CHOOSE m \in Modalities: |
| 63 | + \A other \in Modalities: Priority[m] >= Priority[other] |
| 64 | + |
| 65 | +\* The normaliser: rewrite every modality to the source's value. |
| 66 | +Normalize(s) == |
| 67 | + [m \in Modalities |-> s[SourceOf(s)]] |
| 68 | + |
| 69 | +\* Non-deterministic initial state; all octads are possible starting points |
| 70 | +\* for the model-check. |
| 71 | +Init == |
| 72 | + /\ state \in [Modalities -> Values] |
| 73 | + /\ steps = 0 |
| 74 | + |
| 75 | +\* One round of normalisation. Guard by HasDrift so converged states are |
| 76 | +\* stuttering; guard by MaxSteps for finite model-check. |
| 77 | +Step == |
| 78 | + /\ steps < MaxSteps |
| 79 | + /\ HasDrift(state) |
| 80 | + /\ state' = Normalize(state) |
| 81 | + /\ steps' = steps + 1 |
| 82 | + |
| 83 | +Next == Step |
| 84 | + |
| 85 | +\* Weak fairness forces Step to fire while drift remains, which is what makes |
| 86 | +\* Convergence true. Without it, the system could stutter forever in a drifted |
| 87 | +\* state (that would be a real bug in the implementation, not the spec). |
| 88 | +Spec == Init /\ [][Next]_vars /\ WF_vars(Step) |
| 89 | + |
| 90 | +-------------------------------------------------------------------------------- |
| 91 | +\* Safety |
| 92 | +-------------------------------------------------------------------------------- |
| 93 | + |
| 94 | +\* I1. SourceOf is well-defined: the CHOOSE always returns an element of |
| 95 | +\* Modalities, and that element is the unique priority-maximum. Trivial from |
| 96 | +\* the ASSUME, but stated explicitly so TLC exercises it on every state. |
| 97 | +SourceIsMaximal == |
| 98 | + \A other \in Modalities: |
| 99 | + Priority[SourceOf(state)] >= Priority[other] |
| 100 | + |
| 101 | +\* I2. Idempotence of Normalize: normalising a normalised state is a no-op. |
| 102 | +\* This is a property of the *definition* of Normalize; TLC checks it across |
| 103 | +\* all reachable states including the non-deterministic Init. |
| 104 | +NormalizeIdempotent == |
| 105 | + Normalize(Normalize(state)) = Normalize(state) |
| 106 | + |
| 107 | +\* I3. Post-Step drift-free: immediately after Step, no drift remains. |
| 108 | +\* Implementation: Step replaces state with Normalize(state), which makes |
| 109 | +\* every modality equal to state[SourceOf(state)] -- so any two modalities |
| 110 | +\* agree, i.e. ~HasDrift. TLC verifies by exploring. |
| 111 | +PostStepNoDrift == |
| 112 | + (steps > 0) => ~HasDrift(state) |
| 113 | + |
| 114 | +\* I4. Stability of fixed point: in any drift-free state, Normalize is the |
| 115 | +\* identity. This is the "once converged, stay converged" guarantee. |
| 116 | +FixedPointStable == |
| 117 | + ~HasDrift(state) => (Normalize(state) = state) |
| 118 | + |
| 119 | +NormalizerSafe == |
| 120 | + /\ TypeOK |
| 121 | + /\ SourceIsMaximal |
| 122 | + /\ NormalizeIdempotent |
| 123 | + /\ PostStepNoDrift |
| 124 | + /\ FixedPointStable |
| 125 | + |
| 126 | +-------------------------------------------------------------------------------- |
| 127 | +\* Liveness / convergence |
| 128 | +-------------------------------------------------------------------------------- |
| 129 | + |
| 130 | +\* Eventually, drift is gone and stays gone. Stronger than "eventually no |
| 131 | +\* drift" because the system could in principle re-drift if Step non- |
| 132 | +\* deterministically reintroduced disagreement -- the <>[] form forbids that. |
| 133 | +Convergence == |
| 134 | + <>[]~HasDrift(state) |
| 135 | + |
| 136 | +THEOREM NormalizerSafety == Spec => []NormalizerSafe |
| 137 | +THEOREM NormalizerConverges == Spec => Convergence |
| 138 | + |
| 139 | +================================================================================ |
0 commit comments