The now parameter (and partialGraph.now) replaced the need for global
Date.now patching in tests. The ttl-expiry parity campaign's module-scope
patch and the relational-comparator Time-Based Decay tests' before/after
patches now pass the clock explicitly through check options — no global
mutation, no restore-order fragility, and the pinned-clock cache bypass
keeps every evaluation honest. No stale references to the removed
compiled evaluator or useCompiled option remain in the test suite.
The partial graph is the caller's self-contained view of the world — so
the time belongs in it. partialGraph.now becomes the evaluation clock for
that check (TTL gates, proof expiry, decay all honor it), with an
explicit options.now taking precedence. The explain rerun replays the
SAME partial graph object and reproduces the original decision; the
serializer records request.temporal.now for the record.
Pins: partialGraph.now driving a challenge decision both sides of the
window, the explicit-override precedence, and the explain rerun with the
same object.
The errors-skill's two-pass model applied without a journal: the caller
owns the temporal context. Every decision-flipping temporal feature (the
clock behind TTL gates, challenge-proof expiry, value decay) is now
parameterized as options.now, so an explain rerun that replays the
original temporal parameters reproduces the original decision exactly.
Threaded now through: the comparator's operand value paths
(_getCachedDirectValue/_extractValues/isWithinTTL), the challenge proof
lookup, and the multi_hop value collection (isWithinTTL + blur). All four
result caches bypass cached decisions when the clock is pinned (rule-
result cache, the checker's rule/direct caches, and the comparator's
derived operand cache) — interleaved pinned-clock checks are per-time
with no cross-contamination.
The explain serializer records request.temporal.now so the caller knows
exactly what to replay. The happy path stays minimal and fast (no now ->
no parameter, no cache changes); the rerun (explain with the temporal
context) carries the full diagnostics.
Pins: interleaved fresh/expired/expired-again comparator checks, the
explain replay of both decisions + the recorded temporal context, and
challenge-proof expiry replay.
The compiled evaluator is gone, so useCompiled: false is inert. Removed
every remaining reference: the TTU matrix pin's redundant fallback check,
the OWA wrapper's useCompiled parameter, the comparator full-path test's
redundant rule-path check, and the compiled-rule-parity campaign (which
existed solely to compare the two paths — now trivially identical).
The RuleCompiler remains as the config validator; _compiled metadata
still rides in snapshots.
The persistence probe found three silent-loss defects in the snapshot
round trip: relation validity labels (finite_sample downgraded to
heuristic after save/load!), decay configs, and the value manager's
per-relation TTL settings all vanished. The restored arbiter built its
indices directly from the condensed graph, whose edge channel carries
possibility/reliability/value only.
Format version 2 now carries per-relation metadata (validity, decayConfig)
and the valueTtls table in the snapshot payload; both the snapshot-access
layer and the CondensedGraphIndices build merge the metadata back, so a
restored arbiter is lossless end to end. Version 1 buffers are rejected
with the existing clean version error.
Pinned: a persistence round-trip test asserting validity label, decay
config, TTL, and the restored check's validity label. Suites: rigor
207/0, full 809/747/0.
Per the trust-boundary direction (the caller owns evidence validation):
- Explicit possibilistic semantics module (src/core/possibility.js): the
single authoritative home for what each operator means (max = disjunctive
already-valid; min = unvalidified conjunctive ranking with the K-
validification and surfaced conflict mass; product = Thm-4 heuristic;
interior OWA = non-maxitive heuristic; reliability = adaptation, never
conflated with plausibility).
- Provenance is opt-in (re-entrant tracing practice): default check
results carry only {label, operator, regime}; conflictMass,
validifiedPossibility, sources, and nonMaxitive appear only under
includeMeta and on the explain surface. The direct-check cache now
caches only the meta-less form — includeMeta callers always get a fresh
full evaluation (previously a cached minimal result was served for
includeMeta requests, silently stripping detail).
- Audit affordance: new Arbiter({ audit }) emits one record per check
(decision, possibility, binary, partialGraphUsed, validityLabel,
sources). The engine stores nothing — the caller owns persistence;
zero cost when the hook is absent (and the full validity is forced only
on audit-enabled deployments).
- DoS hardening: partial-graph size limits are enforced BEFORE the
context allocation (the caller-supplied overlay is the per-check
allocation point); the CondensedGraphBinary reader gained full bounds
guards so malformed snapshot buffers fail with clean errors instead of
RangeError crashes or oversized allocations.
- New security-affordance pins: gating, audit records, and pre-allocation
limits.
Adds an epistemic validity layer in the spirit of the zig-contour fusion
spec: every check result now carries a validity block {label, operator,
regime, sources, conflictMass, validifiedPossibility, nonMaxitive}.
- Relations accept a validity label (default heuristic = unlabeled input).
- Labels propagate through fusion: identity/max preserve the weakest
source label (max is already valid under arbitrary dependence); min
(conjunctive: intersection, chain, TTU, multi_hop, parent) is
approximate at best, surfaces the conflict mass (1 - possibility) that
was previously dropped, and exposes the arbitrary-regime validification
min(1, K*gamma); product-style operators (exclusion, defeasible) and
interior OWA averaging are always heuristic, with nonMaxitive flagged.
- Reliability and validity are now explicitly distinct: reliability stays
the scalar confidence adaptation; validity tracks the epistemic label.
- The hottest paths attach a shared frozen default block instead of
allocating (perf A/B shows no regression: ~300k ops/s direct both ways).
- Pre-existing fixes surfaced while wiring: the array-form logical config
dropped top-level aggregator/owaWeights (average union compiled as max),
and _createStandardResult dropped unknown fields (validity never
survived rule results).
New campaign validity-parity.test.js pins the label taxonomy, conflict
mass, validification, weakest-propagation, and the reliability/validity
separation. Suites: rigor 203/0, full 803/741/0.
Probe sweep of the OWA surfaces found three real defects:
- reliabilityWeighting was a silent no-op everywhere: every implementation
scaled possibilities by metas[i].reliability, but no child meta ever
carried a reliability field (the compiled direct omitted it and the
DirectRule handler omitted it too), so the weighting was always x1.0.
All weighting branches now use the tracked child reliabilities, and the
DirectRule handler + its meta now carry the relation's reliability.
- The compiled union and the direct_list fast path had no
reliabilityWeighting branch at all; both now apply it.
- Shorthand children ({ relation: 'editor' }) dispatch to the direct
handler but carry no type, so _getRuleResultCacheKey derived the generic
'rule' suffix for every shorthand child of a logical rule — the first
child's cached result was served for all of them (the fallback path
returned the owner's 0.8 for the editor). The key derivation now matches
the shorthand dispatch. The RuleEvaluator also treats shorthand operands
as direct rules instead of unknown_rule_type on the non-compiled path.
New pins: an OWA differential property (custom weights, max/min/average
aggregators, reliabilityWeighting, compiled path) and a multi_hop
pathAggregation=owa fixed pin with reliability propagation.
The transient full-suite flake (fail 1 in ~5-10% of runs, only visible on
some invocations) was finally captured: challenge-proof's most-recent-proof
property failed once in 800 cases. Root cause: _addChallengeProof built
issuedAt with proof.issuedAt || proof.issued_at || proof.timestamp ||
Date.now() — a generated issuedAt of 0 (epoch) was replaced with the wall
clock, so an epoch-issued proof became the most recent one and the
lookup returned a timestamp (1.7e12) that could never match the
expectation. The same ||-chain corrupted expiresAt: 0 into null, making
epoch-expired proofs never expire. All three chains (issuedAt, expiresAt,
proofId) now use nullish coalescing so zero is a valid timestamp.
Fixed pin added; the challenge-proof file went from ~10% flake to 20/20
clean, and the full suite is stable at 801 tests / 739 pass / 0 fail.
The new value-collection crucibles in the TTU and chain differential
campaigns immediately found three engine defects:
- TTU join pushed 0-strength 'matches' (missing computed leg, or
0-possibility edges, with minPossibility 0) as valid paths: denied
decisions reported tuple_to_userset_found and leaked the tupleset edge's
value into collectedValues. Both join modes now require combined > 0.
- A ReferenceError (bare resolveKey) crashed the computed-join mode under
collectValues, silently turning the whole check into an evaluation_error
denial. Fixed the call to this.arbiter.resolveKey.
- Multi-path TTU fusion fell back to Math.max over all path reliabilities,
pairing the winning possibility with another intermediate's reliability.
The fallback now picks the max-possibility path's reliability.
New campaigns: defeasible and intersection differential properties
(when/unless and min-children with reliability parity under persistent/
partial splits). The model-based campaign keeps its reliability crucible;
its value comparison was reverted — the harness's shrink reporting is
opaque and unreconstructable there, and the value semantics are covered by
the TTU/chain campaigns instead.
The reliability gap found last round was invisible to every parity mirror
(they compared possibility only). Hardened the existing campaigns so the
mirrors carry reliability too:
- batch-order-parity: batch ops carry reliability; the mirror tracks
last-write-wins reliability and the crucible asserts engine reliability
parity (mirror corrected: add-on-existing preserves reliability, it does
not reset it).
- rule-kind-partial-parity: the TTU differential property now generates
per-edge reliabilities and asserts the winning intermediate's
reliability (tupleset.reli * computed.reli); a new chain reliability
differential property does the same for 2-step chains.
- snapshot-quantization-parity: edges carry deterministic reliabilities and
the round-trip pins the codec's reliability channel (product-aware
tolerance: chain reliability multiplies two quantized inputs).
- model-based-graph: the reference model tracks reliability per tuple and
checks it alongside possibility for direct and chain queries.
The model crucible immediately caught a real bug: the direct-check fast
path returned the relation's reliability on a DENIED decision (possibility
0), while the rule-collection path zeroes it — denied results leaked
reliability. Both fast-path branches (direct match and threshold_not_met)
now report reliability 0 when the decision is denied.
Systemic reliability gap found by the probe sweep: the compiled evaluation
paths never emitted the reliability the engine computes.
- Compiled _evaluateDirect omitted the relation's reliability, and the
chain/multi_hop rules hardcoded reliability: 1.0 — so check() results
reported 1.0 for any rule whose decision came through a chain, multi_hop,
union, intersection, exclusion, or defeasible combination.
- The chain and multi_hop traversals now track per-path reliability (product
of edge reliabilities) and report the winning path's value; the compiled
and fallback logical operators (union/intersection/exclusion, direct_list
fast path, early exits) report the selected child's reliability
(max/min child or OWA trace index; exclusion multiplies both legs), and
normal-mode defeasible combines base x requires x defeater reliabilities.
- The checker's logical fast path dropped collectedValues from union/
intersection/exclusion results; it now passes them through.
- MultiHopRule.valueManager was read off relationManager where the real
arbiter keeps it on the arbiter — collectValues: true on a multi_hop rule
with a value-carrying edge crashed the evaluation (error result, silent
denial). Now resolved at the arbiter level with a relationManager
fallback for stubs.
Campaign pins: reliability per kind (chain/multi_hop product, union/intersection
selected child, exclusion/defeasible product), and multi_hop value collection
through persistent and partial contexts.
Two issues found by the extended probe sweep:
- A relational_comparator operand backed by a tuple_to_userset rule always
denied: the TTU rule collected only the intermediate KEY, which the
operand extraction skips as non-numeric, so no value was ever available.
The TTU rule now emits a value-carrying collected entry when the
tupleset edge carries a numeric value (entityKey = tuple src, relation =
tupleset relation), keeping the bare intermediate key when there is no
value. Comparator-with-TTU-operand now allows/denies on the tuple value
through both persistent and partial contexts.
- _modifyRelation ignored the changed_last_at override that the add path
honors: value-changing modifies stamped fresh Date.now() regardless of
the pin, so replay/restore tools pinning timestamps got different
semantics via modify vs add. The override now applies to refresh events
(value/reliability/possibility change) and is ignored for value-unchanged
writes, preserving the TTL parity contract that identical replays never
un-expire old values.
Verified clean: intersection through partial, defeasible with logical
when, challenge subject object/session with sessionKey, non-binary
minAllowPossibility threshold, batch value updates, explain agreement
under partial.
Bug 34: ArbiterChecks.check destructured { reliability, ...rest } out of
every normal (and binary) check result since the initial commit — the
engine computes reliability faithfully (TTU tupleset.reli * computed.reli,
direct relation reliability) and explain() preserved it, but the public
check() API silently dropped it. explain() kept it, so exposing it in
check() is the intended contract. The destructuring is removed; direct
and TTU checks now return reliability (0.6 / 0.72 in the pins).
Campaign pins added: TTU multi-path fusion (max over intermediates of
min(legs)) with reliability propagation through both persistent and
partial contexts, the maxIntermediates circuit breaker through partial,
and snapshot-restored arbiters evaluating TTU.
The full suite intermittently failed (pass counts 725-735, fail 0-1) under
parallel node --test execution. The reachability integration test asserted
a single-shot 100ms quick-failure bound and big-graph-optimized a 1s load
bound — both are regression smoke checks that can blow on a GC pause or
CPU contention spike while other files' campaigns run concurrently.
Both now re-measure once before failing, keeping the tight regression
signal while eliminating load-induced flakes. Full suite is now stable at
735 pass / 0 fail / 62 skipped across repeated runs.
The reverse branch of TupleToUsersetRule ignored tuplesetDirection: it
looked up the tupleset relation as outgoing-from-user even when 'in' was
set, while the join honored 'in' by using t.src as the intermediate —
the documented shape (intermediates hold the relation TO the user) never
matched, and only a degenerate join-on-user shape produced results. The
same gap existed in the compiled direct_join optimization.
Fixed the fallback tupleset lookup (including the graph-neighbor path and
the computed-join tupleEdge direction) and the optimized direct_join to
honor tuplesetDirection in reverse mode. Matrix pins reverse-in (both
evaluation paths, persistent + partial), multi_hop reverse, chain-in,
union-with-chain-child, defeasible split legs, TTU value flow, and
challenge-via-binary.
Two real bugs found by the new rule-kind x partial-graph parity campaign:
- TupleToUsersetRule 'computed' join mode (computed side has fewer
intermediates than the tupleset side) pushed path objects carrying
combinedPossibility, but _buildFinalResult reads path.possibility —
every valid TTU grant in that mode silently returned 0, in persistent
and partial contexts alike.
- _collectRelationUsages only registered explicit type:'direct' children,
so shorthand logical operands ({ relation: 'owner' } inside union/
intersection/exclusion) left the dependency index empty: writes to a
base relation never invalidated cached logical decisions, and a check
performed before an add kept serving its stale result forever.
New campaign rule-kind-partial-parity.test.js pins the full kind x
persistent/partial matrix (direct, chain, multi_hop, TTU out/in/reverse,
parent, computed, defeasible, union, exclusion, comparator, challenge,
binary) plus seeded differential properties for TTU, comparator, and
exclusion; artifact persistence disabled to avoid disk bloat.
- Arbiter constructor and setPartialGraphPolicy dropped maxNodes,
maxRelations, and reservedRelations — the partial-graph context always
saw the 1000/2000 defaults, silently disabling configured DoS guards.
Both paths now carry the limits through; policy limits test pins
constructor + setter enforcement and custom reservedRelations.
The binary branch of AuthorizationChecker.check rebuilt its options with a
fixed six-field object, silently discarding partialGraphContext (and any
other caller option) — binary checks denied grants the normal path
allowed. Now spreads all caller options through. binary-partial-parity
pins: partial grants above the threshold allow, below deny, persistent
wins over partial, and binary decisions agree with normal decisions on
the same overlay.
- invalidateRuleResultCacheByRelation now walks the transitive closure of
the dependency index (owner -> is_owner -> computed can_read): mutating
a base relation left computed/multi_hop results stale
- _collectRelationUsages registers multi_hop and parent rule relations
(parentRelation + subject relation) so their caches invalidate on
base-relation mutations
- advanced-rule-kinds.test.js: multi_hop (min-over-path, depth-limited),
computed (userset alias), parent (subject relation on the target's
parent) through the full check() path under random mutations
- _addRelationInternal now validates possibility like the single-add path
(batch adds could store out-of-range values and make the transactional
wrapper report success:true after a partial apply)
- tx-rollback-parity.test.js: poison at every batch position rolls back
fully; clean batches apply in order; batch+PLTC reachability reflects
batch writes immediately
updateRelationsBatch previously pre-sorted ops remove->modify->add, which
changed the final state whenever one tuple was touched by mixed kinds:
[add, remove] left the tuple present, [modify, add, modify] ended with
the middle value. Now ops apply strictly in the given order via the
dedup-aware _addRelationInternal (in-place last-write-wins) with upfront
validation, post-batch PLTC edge updates, and per-relation arbiter-level
cache invalidation.
batch-order-parity.test.js pins the contract with an in-order mirror
(modify-of-missing is a silent no-op).
Engine fixes:
- RelationUpdates.updateRelationsBatch: invalidate arbiter-level caches
(rule result cache, ChainRule caches, direct-check cache) per affected
relation — batch updates bypassed Arbiter.addRelation and served stale
decisions after batch modify/swap with warm caches
- updateRelationsBatchTransactional rollback: new Map(Set) crashed with
'Iterator value is not an entry object' — fixed to new Set
- RelationalComparatorRule: value extraction (direct-list and cached
direct paths) now gates on valueManager._isValueExpired — TTL-expired
values no longer feed comparator decisions
Campaigns:
- value-freshness-parity.test.js: batch modify/swap/tx rollback freshness
with comparator mirror (batch MODIFY of a missing relation is a silent
no-op — pinned)
- ttl-expiry-parity.test.js: injected-clock TTL expiry through the
comparator path (exact parity with caching off; bounded staleness with
caching on), faithful changed_last_at mirror semantics