2 Commits

Author SHA1 Message Date
John Dvorak f530532e48 fix: standard collected-value shape on the direct fast path + caller-clock timestamps
Two remaining clock/shape inconsistencies from the audit:

1. The direct fast path emitted a bare collected-value object
   {value, source, relation, userKey, objectKey} — no possibility, no
   path, no metadata. Value consumers (comparators, chains) rely on the
   self-describing shape the rule paths emit. The fast path now emits
   the standard shape (value/possibility/path/source/metadata), matching
   DirectRule's existing _createCollectedValue contract.

2. Collected-value timestamps fell back to the WALL clock (Date.now())
   even for pinned-clock callers in BaseRule._createCollectedValue,
   ChainRule, and TupleToUsersetRule. The metadata timestamp now honors
   options.now when pinned (changed_last_at wins, then pinned now, then
   wall clock). ValueContext's collectedAt remains metadata-only.

Pinned by ttl-contract.test.js: the fast-path collected value carries
the full shape and its timestamp honors the pinned clock. Rigor 251/251,
full suite 853/791/0.
2026-08-02 17:07:20 -07:00
John Dvorak 342c29f38b fix: pin TTL contract, gate caches on caller clock, stop caching stale values
Three related findings from the nervous-item audit:

1. TTL contract pinned (ttl-contract.test.js + README): TTL is a
   VALUE-FRESHNESS gate, not an access-expiry mechanism. Direct grants
   are timeless; expired values deny comparators and drop from collected
   values. The direct fast path collected values WITHOUT the TTL gate
   (comparators skipped expired relations, the direct path did not) —
   now gated identically.

2. ChainRule cache served pinned-clock callers (ChainRule.js): a chain
   result captured at one time (with then-fresh values) was served to
   callers asking about another time. The chain cache now bypasses
   reads AND writes when options.now is pinned, matching the rule-result
   cache contract.

3. Decision caches bundled stale values (AuthorizationChecker.js):
   the direct-check cache stored collectedValues alongside the timeless
   decision; an unpinned caller past wall-clock expiry got the stale
   value. Value-carrying results are now never cached (the decision is
   timeless, the values are not). The rule-result cache is unchanged —
   it serves snapshots under explicit write-invalidation (its own
   contract, asserted by cache-invalidation tests).

Rigor 250/250, full suite 852/790/0.
2026-08-02 16:07:55 -07:00