feat: intermediate chain condition steps, graph-version cache invalidation, rolling-hash chain keys; fix vacuous rigor invariants
CI / benchmark (push) Successful in 48s
CI / test (push) Successful in 5m26s
CI / publish (push) Has been skipped

Chain intermediates (rule-based reachability):
- ChainRule: a condition step ({ rule, conditionStep }) at an INTERMEDIATE
  position is now EXPANDED from the current node — the rule's base edges'
  destinations, filtered by its defeaters/requirements — and traversal
  continues from each discovered node. Adds _expandRuleFromSrc / direct /
  logical(union/intersection) / defeasible / nested-chain expansion.
- RuleEvaluator: _subjectIsObject flag for unary predicate calls whose subject
  entity IS the object parameter (trusted(other) inside peer_trusted(user,
  other)); previously only subject-var unary calls (_subjectAsObject) were
  handled, so object-var unary defeaters never fired.

Graph-version cache invalidation:
- Arbiter gains a monotonic _graphVersion, incremented on every relation
  mutation. ChainRule result cache, RuleEvaluator rule-result cache, and
  DecisionCache rule cache now stamp entries with the graph version and treat
  any mismatch as a miss — graph mutations can no longer serve stale
  chain/authorization results.

Rolling-hash cache keys:
- UnifiedKeyManager.createChainKey now builds a 53-bit rolling hash (dual
  FNV-1a lanes, exact for ints/floats/strings/nested configs) instead of
  JSON.stringify — no string allocation or serialization on the chain-cache
  hot path. Composite keys stay structured strings because the direct-check
  cache pattern-invalidates by relation ID.

Rigor invariant migration (correctness):
- All 43 rigor test files' throw-based invariants ({ error, errorMessage } =>
  !error && !errorMessage) never saw fn throws — vacuous. Migrated to
  ({ actual }) => actual !== undefined, which fails on any thrown violation
  while passing legitimate null-skips. The migration immediately surfaced
  two latent bugs, now fixed:
    * node-manager/graph-indices skip paths returned bare undefined (falsy
      sentinel) — return { skipped: true }.
    * complex-graph-values-crucible expiry section rewrote values equal to the
      mutation loop's last write; the engine (by design) keeps the old
      timestamp on same-value rewrites so the pre-expiry grant never
      materialized. Now writes guaranteed-different values.
This commit is contained in:
John Dvorak
2026-08-03 13:26:42 -07:00
parent 4da3158c63
commit ed34df4474
51 changed files with 560 additions and 241 deletions
+13 -13
View File
@@ -92,7 +92,7 @@ describe('QualitativeRelationalComparatorRule._getQualitativeScale (rigor)', ()
)
],
rigor.crucible([
rigor.invariant('known-scales', ({ error, errorMessage }) => !error && !errorMessage)
rigor.invariant('known-scales', ({ actual }) => actual !== undefined)
])
).run({ seed: 'qualitative-known-scales', effort: 500 , artifacts: { dir: '', persist: 'never' }});
@@ -123,7 +123,7 @@ describe('QualitativeRelationalComparatorRule._getQualitativeScale (rigor)', ()
)
],
rigor.crucible([
rigor.invariant('fallback', ({ error, errorMessage }) => !error && !errorMessage)
rigor.invariant('fallback', ({ actual }) => actual !== undefined)
])
).run({ seed: 'qualitative-fallback', effort: 800 , artifacts: { dir: '', persist: 'never' }});
@@ -160,7 +160,7 @@ describe('QualitativeRelationalComparatorRule._calculateDecayedPossibility (rigo
)
],
rigor.crucible([
rigor.invariant('stable-identity', ({ error, errorMessage }) => !error && !errorMessage)
rigor.invariant('stable-identity', ({ actual }) => actual !== undefined)
])
).run({ seed: 'qualitative-stable-identity', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
@@ -195,7 +195,7 @@ describe('QualitativeRelationalComparatorRule._calculateDecayedPossibility (rigo
)
],
rigor.crucible([
rigor.invariant('zero-periods', ({ error, errorMessage }) => !error && !errorMessage)
rigor.invariant('zero-periods', ({ actual }) => actual !== undefined)
])
).run({ seed: 'qualitative-zero-periods', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
@@ -236,7 +236,7 @@ describe('QualitativeRelationalComparatorRule._calculateDecayedPossibility (rigo
)
],
rigor.crucible([
rigor.invariant('down-monotone', ({ error, errorMessage }) => !error && !errorMessage)
rigor.invariant('down-monotone', ({ actual }) => actual !== undefined)
])
).run({ seed: 'qualitative-down-monotone', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
@@ -276,7 +276,7 @@ describe('QualitativeRelationalComparatorRule._calculateDecayedPossibility (rigo
)
],
rigor.crucible([
rigor.invariant('up-monotone', ({ error, errorMessage }) => !error && !errorMessage)
rigor.invariant('up-monotone', ({ actual }) => actual !== undefined)
])
).run({ seed: 'qualitative-up-monotone', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
@@ -312,7 +312,7 @@ describe('QualitativeRelationalComparatorRule._calculateDecayedPossibility (rigo
)
],
rigor.crucible([
rigor.invariant('result-in-scale', ({ error, errorMessage }) => !error && !errorMessage)
rigor.invariant('result-in-scale', ({ actual }) => actual !== undefined)
])
).run({ seed: 'qualitative-result-in-scale', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
@@ -349,7 +349,7 @@ describe('QualitativeRelationalComparatorRule._createQualitativeInterval (rigor)
)
],
rigor.crucible([
rigor.invariant('lower-le-upper', ({ error, errorMessage }) => !error && !errorMessage)
rigor.invariant('lower-le-upper', ({ actual }) => actual !== undefined)
])
).run({ seed: 'qualitative-lower-le-upper', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
@@ -387,7 +387,7 @@ describe('QualitativeRelationalComparatorRule._createQualitativeInterval (rigor)
)
],
rigor.crucible([
rigor.invariant('point-contained', ({ error, errorMessage }) => !error && !errorMessage)
rigor.invariant('point-contained', ({ actual }) => actual !== undefined)
])
).run({ seed: 'qualitative-point-contained', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
@@ -423,7 +423,7 @@ describe('QualitativeRelationalComparatorRule._createQualitativeInterval (rigor)
)
],
rigor.crucible([
rigor.invariant('bounds-in-scale', ({ error, errorMessage }) => !error && !errorMessage)
rigor.invariant('bounds-in-scale', ({ actual }) => actual !== undefined)
])
).run({ seed: 'qualitative-bounds-in-scale', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
@@ -457,7 +457,7 @@ describe('QualitativeRelationalComparatorRule._createQualitativeInterval (rigor)
)
],
rigor.crucible([
rigor.invariant('zero-blur', ({ error, errorMessage }) => !error && !errorMessage)
rigor.invariant('zero-blur', ({ actual }) => actual !== undefined)
])
).run({ seed: 'qualitative-zero-blur', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
@@ -488,7 +488,7 @@ describe('QualitativeRelationalComparatorRule._calculatePossibilityLossSteps (ri
)
],
rigor.crucible([
rigor.invariant('loss-is-zero', ({ error, errorMessage }) => !error && !errorMessage)
rigor.invariant('loss-is-zero', ({ actual }) => actual !== undefined)
])
).run({ seed: 'qualitative-loss-is-zero', effort: 800 , artifacts: { dir: '', persist: 'never' }});
@@ -527,7 +527,7 @@ describe('QualitativeRelationalComparatorRule._calculatePossibilityLossSteps (ri
)
],
rigor.crucible([
rigor.invariant('loss-symmetric', ({ error, errorMessage }) => !error && !errorMessage)
rigor.invariant('loss-symmetric', ({ actual }) => actual !== undefined)
])
).run({ seed: 'qualitative-loss-symmetric', effort: 800 , artifacts: { dir: '', persist: 'never' }});