Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f6e3ae1922 | |||
| bd9c74fb0e | |||
| ab0e569552 | |||
| ed34df4474 | |||
| 4da3158c63 |
+57
-57
@@ -1,140 +1,140 @@
|
||||
{
|
||||
"version": 1,
|
||||
"generated": "2026-08-03T03:25:46.644Z",
|
||||
"generated": "2026-08-03T22:37:59.060Z",
|
||||
"actions": {
|
||||
"check[direct-hit]": {
|
||||
"mostPlausible": 0.2641342114909732,
|
||||
"mostPlausible": 0.10672237227527051,
|
||||
"alphaCuts": {
|
||||
"p50": {
|
||||
"lower": 0.11993100000017193,
|
||||
"upper": 0.6825795028928235
|
||||
"lower": 0.105663874043709,
|
||||
"upper": 0.10779178510155751
|
||||
},
|
||||
"p95": {
|
||||
"lower": 0.11993100000017193,
|
||||
"upper": 0.6825795028928235
|
||||
"lower": 0.10367558838055704,
|
||||
"upper": 0.10985914641251807
|
||||
},
|
||||
"p99": {
|
||||
"lower": 0.11993100000017193,
|
||||
"upper": 0.7088692690070402
|
||||
"lower": 0.10273583797909441,
|
||||
"upper": 0.11086333384764993
|
||||
}
|
||||
}
|
||||
},
|
||||
"check[union-ttu]": {
|
||||
"mostPlausible": 0.5572143564747223,
|
||||
"mostPlausible": 0.11249228961043199,
|
||||
"alphaCuts": {
|
||||
"p50": {
|
||||
"lower": 0.10743099999989908,
|
||||
"upper": 0.5572148030094048
|
||||
"lower": 0.111604019277894,
|
||||
"upper": 0.11338800600185026
|
||||
},
|
||||
"p95": {
|
||||
"lower": 0.10743099999989908,
|
||||
"upper": 0.5572148030094048
|
||||
"lower": 0.10993048219120201,
|
||||
"upper": 0.11511402952231622
|
||||
},
|
||||
"p99": {
|
||||
"lower": 0.10743099999989908,
|
||||
"upper": 0.5572148030094048
|
||||
"lower": 0.1091378967617418,
|
||||
"upper": 0.11594970606842417
|
||||
}
|
||||
}
|
||||
},
|
||||
"check[denied-miss]": {
|
||||
"mostPlausible": 0.17001072644302176,
|
||||
"mostPlausible": 0.08747524001151907,
|
||||
"alphaCuts": {
|
||||
"p50": {
|
||||
"lower": 0.10983099999997875,
|
||||
"upper": 0.27403866354448264
|
||||
"lower": 0.08668417849217541,
|
||||
"upper": 0.08827353325683636
|
||||
},
|
||||
"p95": {
|
||||
"lower": 0.10983099999997875,
|
||||
"upper": 0.27403866354448264
|
||||
"lower": 0.085196295997943,
|
||||
"upper": 0.08981478022606769
|
||||
},
|
||||
"p99": {
|
||||
"lower": 0.10983099999997875,
|
||||
"upper": 0.2792968083762517
|
||||
"lower": 0.08449268650925873,
|
||||
"upper": 0.09056310270952082
|
||||
}
|
||||
}
|
||||
},
|
||||
"check[include-meta]": {
|
||||
"mostPlausible": 0.4313421276490078,
|
||||
"mostPlausible": 0.17368093893555905,
|
||||
"alphaCuts": {
|
||||
"p50": {
|
||||
"lower": 0.2319310000000246,
|
||||
"upper": 0.9209354557840623
|
||||
"lower": 0.17235808249234505,
|
||||
"upper": 0.17501399072522753
|
||||
},
|
||||
"p95": {
|
||||
"lower": 0.2319310000000246,
|
||||
"upper": 0.9209354557840623
|
||||
"lower": 0.1698647154489402,
|
||||
"upper": 0.17758260042495247
|
||||
},
|
||||
"p99": {
|
||||
"lower": 0.2319310000000246,
|
||||
"upper": 0.9491636618587536
|
||||
"lower": 0.16868343694884977,
|
||||
"upper": 0.178826872871734
|
||||
}
|
||||
}
|
||||
},
|
||||
"check[overlay-on-top]": {
|
||||
"mostPlausible": 2.4209884301909703,
|
||||
"mostPlausible": 0.34658090606556824,
|
||||
"alphaCuts": {
|
||||
"p50": {
|
||||
"lower": 2.4209884301909703,
|
||||
"upper": 2.4209887680862785
|
||||
"lower": 0.343563635087355,
|
||||
"upper": 0.3496248120197711
|
||||
},
|
||||
"p95": {
|
||||
"lower": 2.4209884301909703,
|
||||
"upper": 2.4209887680862785
|
||||
"lower": 0.33788494447103923,
|
||||
"upper": 0.3555006559358586
|
||||
},
|
||||
"p99": {
|
||||
"lower": 2.4209884301909703,
|
||||
"upper": 2.4209887680862785
|
||||
"lower": 0.33519739588838326,
|
||||
"upper": 0.35835082770353416
|
||||
}
|
||||
}
|
||||
},
|
||||
"check[binary-direct]": {
|
||||
"mostPlausible": 0.24005599110530157,
|
||||
"mostPlausible": 0.10136432040214981,
|
||||
"alphaCuts": {
|
||||
"p50": {
|
||||
"lower": 0.154731000000304,
|
||||
"upper": 0.2756826494555559
|
||||
"lower": 0.10038730449515523,
|
||||
"upper": 0.10235072528492808
|
||||
},
|
||||
"p95": {
|
||||
"lower": 0.154731000000304,
|
||||
"upper": 0.41012851350897955
|
||||
"lower": 0.0985518364498951,
|
||||
"upper": 0.10425698760463886
|
||||
},
|
||||
"p99": {
|
||||
"lower": 0.154731000000304,
|
||||
"upper": 0.41896614930102694
|
||||
"lower": 0.09768455509480933,
|
||||
"upper": 0.10518292733500886
|
||||
}
|
||||
}
|
||||
},
|
||||
"snapshot[build-binary]": {
|
||||
"mostPlausible": 7.446251599999954,
|
||||
"mostPlausible": 2.848037533444555,
|
||||
"alphaCuts": {
|
||||
"p50": {
|
||||
"lower": 5.3102783288306075,
|
||||
"upper": 9.582224244110606
|
||||
"lower": 2.827413825511722,
|
||||
"upper": 2.868811889350609
|
||||
},
|
||||
"p95": {
|
||||
"lower": 5.3102783288306075,
|
||||
"upper": 9.582224244110606
|
||||
"lower": 2.788520370277795,
|
||||
"upper": 2.9088252291455836
|
||||
},
|
||||
"p99": {
|
||||
"lower": 5.225227783447299,
|
||||
"upper": 9.667235344457687
|
||||
"lower": 2.770077133158435,
|
||||
"upper": 2.9281936091667653
|
||||
}
|
||||
}
|
||||
},
|
||||
"snapshot[restore-binary]": {
|
||||
"mostPlausible": 9.853481541731316,
|
||||
"mostPlausible": 2.9488926124434385,
|
||||
"alphaCuts": {
|
||||
"p50": {
|
||||
"lower": 7.845275634169648,
|
||||
"upper": 12.375747204692138
|
||||
"lower": 2.9247809483999716,
|
||||
"upper": 2.9732031186198484
|
||||
},
|
||||
"p95": {
|
||||
"lower": 4.7317310000000745,
|
||||
"upper": 21.54997911145655
|
||||
"lower": 2.8793716893148757,
|
||||
"upper": 3.020092288118393
|
||||
},
|
||||
"p99": {
|
||||
"lower": 4.7317310000000745,
|
||||
"upper": 25.40483299999982
|
||||
"lower": 2.8578657882531426,
|
||||
"upper": 3.0428172805315117
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@arbiter/core",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.6",
|
||||
"description": "Arbiter core engine: graph indices, relation/reachability, authorization rule evaluator, DSL/AST, condensed & sharded snapshots, and evidence fusion.",
|
||||
"license": "ISC",
|
||||
"author": "",
|
||||
|
||||
+96
-12
@@ -28,11 +28,24 @@ const BASELINE_PATH = (() => {
|
||||
|
||||
benchmark.config({
|
||||
measurements: ['timing'],
|
||||
uncertaintyThreshold: 0.99,
|
||||
minSamples: 0,
|
||||
maxSamples: 200,
|
||||
// Stop collecting once 90% confident (was 0.99 ≈ "confident" immediately,
|
||||
// so the loop stopped at the first check and the sub-ms checks were
|
||||
// measured from ~5 noisy samples). A tighter threshold forces the loop to
|
||||
// keep sampling until variance actually tightens.
|
||||
uncertaintyThreshold: 0.1,
|
||||
// Sample-count floor before any early stop (was 0). Sub-ms checks need a
|
||||
// real sample population for a stable mostPlausible / p95.
|
||||
minSamples: 200,
|
||||
// Headroom for noisy actions to run to (was 200, which was the effective
|
||||
// cap and doubled as the practical floor).
|
||||
maxSamples: 2000,
|
||||
overheadCompensation: true,
|
||||
gcBetweenSamples: true,
|
||||
// Per-sample GC injected a global.gc() between every iteration, which
|
||||
// dominated sub-ms timings and produced the wild run-to-run swings
|
||||
// (check[overlay-on-top] flagged -23%..-83% "faster" across runs of
|
||||
// unchanged code). GC is left to the runtime; the sample floor + tight
|
||||
// confidence threshold now stabilize the distribution instead.
|
||||
gcBetweenSamples: false,
|
||||
})
|
||||
|
||||
let CRASHED = 0
|
||||
@@ -63,34 +76,64 @@ function buildEngine() {
|
||||
|
||||
const engine = buildEngine()
|
||||
|
||||
// ── Warmup ────────────────────────────────────────────────────────────
|
||||
// Sub-ms checks measured without a warmup phase mix first-touch allocation,
|
||||
// lazy index construction, and JIT compilation into the sample population —
|
||||
// a bimodal distribution (check[direct-hit] ~4µs vs ~20µs) that flapped the
|
||||
// regression gate across runs of unchanged code. Run each hot path to a
|
||||
// steady state before any sampling.
|
||||
function warmupChecks(a, iterations = 20000) {
|
||||
const ctx = new PartialGraphContext(a, {
|
||||
relations: [{ src: 'user:1', relation: 'owner', dst: 'doc:1', possibility: 0.5 }]
|
||||
})
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
a.check('user:1', 'owner', 'doc:1')
|
||||
a.check('user:1', 'can_read', 'doc:1')
|
||||
a.check('user:5', 'owner', 'doc:1')
|
||||
a.check('user:1', 'can_read', 'doc:1', { includeMeta: true })
|
||||
a.check('user:1', 'owner', 'doc:1', { partialGraphContext: ctx })
|
||||
}
|
||||
}
|
||||
warmupChecks(engine)
|
||||
|
||||
// Sub-ms checks are 4–20µs per call — below reliable single-call timing
|
||||
// resolution, so one GC tick or context switch inflates a sample (bimodal
|
||||
// distributions flapped the gate). Each sub-ms action measures a BATCH of
|
||||
// calls per sample; jitter amortizes across the batch and the relative
|
||||
// comparison against the baseline (which uses the same BATCH) stays exact.
|
||||
const BATCH = 100
|
||||
|
||||
const directBench = benchmark('check[direct-hit]', () => {
|
||||
engine.check('user:1', 'owner', 'doc:1')
|
||||
for (let i = 0; i < BATCH; i++) engine.check('user:1', 'owner', 'doc:1')
|
||||
})
|
||||
|
||||
const unionBench = benchmark('check[union-ttu]', () => {
|
||||
engine.check('user:1', 'can_read', 'doc:1')
|
||||
for (let i = 0; i < BATCH; i++) engine.check('user:1', 'can_read', 'doc:1')
|
||||
})
|
||||
|
||||
const deniedBench = benchmark('check[denied-miss]', () => {
|
||||
engine.check('user:5', 'owner', 'doc:1')
|
||||
for (let i = 0; i < BATCH; i++) engine.check('user:5', 'owner', 'doc:1')
|
||||
})
|
||||
|
||||
const metaBench = benchmark('check[include-meta]', () => {
|
||||
engine.check('user:1', 'can_read', 'doc:1', { includeMeta: true })
|
||||
for (let i = 0; i < BATCH; i++) engine.check('user:1', 'can_read', 'doc:1', { includeMeta: true })
|
||||
})
|
||||
|
||||
const overlayBench = benchmark('check[overlay-on-top]', () => {
|
||||
for (let i = 0; i < BATCH; i++) {
|
||||
const ctx = new PartialGraphContext(engine, {
|
||||
relations: [{ src: 'user:1', relation: 'owner', dst: 'doc:1', possibility: 0.5 }]
|
||||
})
|
||||
engine.check('user:1', 'owner', 'doc:1', { partialGraphContext: ctx })
|
||||
}
|
||||
})
|
||||
|
||||
const binaryBench = (() => {
|
||||
const snap = buildEngine()
|
||||
snap.enableCondensedSnapshot()
|
||||
for (let i = 0; i < 5000; i++) snap.check('user:1', 'owner', 'doc:1', { binary: true })
|
||||
return benchmark('check[binary-direct]', () => {
|
||||
snap.check('user:1', 'owner', 'doc:1', { binary: true })
|
||||
for (let i = 0; i < BATCH; i++) snap.check('user:1', 'owner', 'doc:1', { binary: true })
|
||||
})
|
||||
})()
|
||||
|
||||
@@ -164,9 +207,50 @@ if (SAVE) {
|
||||
const baseline = JSON.parse(fs.readFileSync(BASELINE_PATH, 'utf8'))
|
||||
const regResult = detectRegressions(current, baseline)
|
||||
if (!AS_JSON) console.log(formatRegressions(regResult, 'pretty'))
|
||||
const critical = regResult.regressions.filter(r => r.severity === 'high')
|
||||
if (critical.length > 0) {
|
||||
console.error(`❌ ${critical.length} critical regression(s): ${critical.map(r => r.name).join(', ')}`)
|
||||
|
||||
// ── Ratio-based self-calibration ─────────────────────────────────────
|
||||
// Absolute timings swing with machine load (a shared runner at load 19
|
||||
// shifted every action +15..+50%). Comparing each action's RATIO to a cheap
|
||||
// reference action cancels the load: load scales all actions proportionally,
|
||||
// while a code regression shifts only the affected action's ratio. The
|
||||
// reference action itself is still gate-checked absolutely with a loose
|
||||
// bound (a regression of the reference would otherwise mask every ratio).
|
||||
const RATIO_REFERENCE = process.env.BENCH_RATIO_REFERENCE || 'check[direct-hit]'
|
||||
const RATIO_PERCENT = Number(process.env.RATIO_REGRESSION_PERCENT ?? 15)
|
||||
const REFERENCE_PERCENT = Number(process.env.REFERENCE_REGRESSION_PERCENT ?? 30)
|
||||
|
||||
const currentRef = current.actions?.[RATIO_REFERENCE]?.mostPlausible
|
||||
const baselineRef = baseline.actions?.[RATIO_REFERENCE]?.mostPlausible
|
||||
const ratioViolations = []
|
||||
let referenceViolation = false
|
||||
|
||||
if (currentRef > 0 && baselineRef > 0) {
|
||||
if (currentRef > baselineRef * (1 + REFERENCE_PERCENT / 100)) {
|
||||
referenceViolation = true
|
||||
ratioViolations.push(
|
||||
`${RATIO_REFERENCE} (reference) baseline=${baselineRef.toFixed(4)} → current=${currentRef.toFixed(4)} (+${(((currentRef / baselineRef) - 1) * 100).toFixed(1)}%)`
|
||||
)
|
||||
}
|
||||
for (const [name, cur] of Object.entries(current.actions)) {
|
||||
if (name === RATIO_REFERENCE) continue
|
||||
const base = baseline.actions?.[name]
|
||||
if (!base || !(base.mostPlausible > 0)) continue
|
||||
const curRatio = cur.mostPlausible / currentRef
|
||||
const baseRatio = base.mostPlausible / baselineRef
|
||||
if (curRatio > baseRatio * (1 + RATIO_PERCENT / 100)) {
|
||||
ratioViolations.push(
|
||||
`${name} ratio ${curRatio.toFixed(3)} → baseline ${baseRatio.toFixed(3)} (+${(((curRatio / baseRatio) - 1) * 100).toFixed(1)}%)`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ratioViolations.length > 0) {
|
||||
console.error(`❌ ${ratioViolations.length} ratio regression(s):`)
|
||||
for (const v of ratioViolations) console.error(` - ${v}`)
|
||||
if (referenceViolation) {
|
||||
console.error(' (the reference action regressed absolutely — ratios may be unreliable)')
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
} else if (!SAVE) {
|
||||
|
||||
@@ -967,6 +967,15 @@ export class AuthorizationChecker {
|
||||
if (!this.decisionCache.directEnabled) return;
|
||||
const cacheKey = this._getDirectCheckCacheKey(userKey, relation, objectKey);
|
||||
this.decisionCache.setDirect(cacheKey, result);
|
||||
// Track the (hashed) key under the checked relation + its base relations
|
||||
// + the subject/object nodes so relation- and node-level invalidation can
|
||||
// delete it without parsing the key.
|
||||
const arb = this.arbiter;
|
||||
if (arb && typeof arb._trackDirectCheckKey === 'function') {
|
||||
const srcId = arb.resolveNodeId(userKey);
|
||||
const dstId = arb.resolveNodeId(objectKey);
|
||||
arb._trackDirectCheckKey(relation, srcId, dstId, cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
_getVisitedMode(visited) {
|
||||
|
||||
@@ -107,6 +107,11 @@ export class DecisionCache {
|
||||
if (!this.ruleEnabled) return undefined;
|
||||
const entry = this.arbiter.ruleResultCache.get(ruleCacheKey);
|
||||
if (!entry) return undefined;
|
||||
// A graph mutation since the entry was computed invalidates it (stale
|
||||
// derived result). _graphVersion increments on every relation mutation.
|
||||
if (entry.graphVersion !== undefined && entry.graphVersion !== (this.arbiter._graphVersion ?? 0)) {
|
||||
return undefined;
|
||||
}
|
||||
if (this.clock() - entry.timestamp >= this.arbiter.ruleResultCacheTTL) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -117,7 +122,8 @@ export class DecisionCache {
|
||||
if (!this.ruleEnabled) return;
|
||||
this.arbiter.ruleResultCache.set(ruleCacheKey, {
|
||||
result,
|
||||
timestamp: this.clock()
|
||||
timestamp: this.clock(),
|
||||
graphVersion: this.arbiter._graphVersion ?? 0
|
||||
});
|
||||
}
|
||||
|
||||
@@ -168,32 +174,9 @@ export class DecisionCache {
|
||||
if (!a?.directCheckCache) return;
|
||||
const nodeId = a.nodeIdByKey?.get(nodeKey);
|
||||
if (nodeId === undefined) return;
|
||||
const cache = a.directCheckCache;
|
||||
|
||||
// Pattern-based invalidation is the safest path — the cache file
|
||||
// owns its storage layout and decides how to enumerate keys.
|
||||
if (typeof cache.invalidateByPattern === 'function') {
|
||||
// Match a nodeId that appears in any position of the composite
|
||||
// key. The composite key format is `${srcId}|${rel}|${dstId}` and
|
||||
// components are pipe-delimited, so a digit-boundary regex is
|
||||
// safer than a plain substring match.
|
||||
const escaped = String(nodeId).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const pattern = new RegExp(`(?:^|\\|)${escaped}(?:\\||$)`);
|
||||
cache.invalidateByPattern(pattern);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: explicit key iteration. HyperbolicLRUCache does not
|
||||
// expose `keys()`, so this branch is unreachable for that cache
|
||||
// family. If a future cache implementation exposes iteration,
|
||||
// we walk it without leaking storage-layout details.
|
||||
if (typeof cache.keys === 'function') {
|
||||
for (const key of cache.keys()) {
|
||||
if (typeof key === 'string' && key.includes(String(nodeId))) {
|
||||
cache.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Keys are rolling hashes (not pipe-delimited strings), so node-level
|
||||
// invalidation uses the per-node key index populated at cache-set time.
|
||||
a._invalidateDirectCheckCacheByNode?.(nodeId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -204,6 +187,9 @@ export class DecisionCache {
|
||||
const a = this.arbiter;
|
||||
if (!a) return;
|
||||
a.directCheckCache?.clear?.();
|
||||
a.directCheckCacheKeysByRelation?.clear?.();
|
||||
a.directCheckCacheKeysByNode?.clear?.();
|
||||
a.directKeyToRelations?.clear?.();
|
||||
a.invalidateAllRuleResultCache?.();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export class RuleEvaluator {
|
||||
tuple_to_userset: new TupleToUsersetRule(arbiter),
|
||||
multi_hop: new MultiHopRule(arbiter),
|
||||
relational_comparator: new RelationalComparatorRouter(arbiter, this),
|
||||
chain: new ChainRule(arbiter),
|
||||
chain: new ChainRule(arbiter, this),
|
||||
challenge: new ChallengeRule(arbiter)
|
||||
};
|
||||
|
||||
@@ -33,7 +33,7 @@ export class RuleEvaluator {
|
||||
|
||||
|
||||
// Convert string keys to numeric IDs if needed
|
||||
const numericUserId = typeof userId === 'string' ? this.arbiter.resolveNodeId(userId, options) : userId;
|
||||
let numericUserId = typeof userId === 'string' ? this.arbiter.resolveNodeId(userId, options) : userId;
|
||||
let numericObjectId = typeof objectId === 'string' ? this.arbiter.resolveNodeId(objectId, options) : objectId;
|
||||
|
||||
// Unary / subject-scoped rules (e.g. a DSL predicate call `banned(user)`
|
||||
@@ -45,6 +45,13 @@ export class RuleEvaluator {
|
||||
numericObjectId = numericUserId;
|
||||
objectKey = userKey;
|
||||
}
|
||||
// A unary call whose subject entity IS the object parameter
|
||||
// (`trusted(other)` inside peer_trusted(user, other)) checks the relation
|
||||
// as a self-edge on the object — rewrite the subject to the object.
|
||||
if (rule._subjectIsObject) {
|
||||
numericUserId = numericObjectId;
|
||||
userKey = objectKey;
|
||||
}
|
||||
|
||||
const needsValueContext = valueContext !== null && valueContext !== undefined
|
||||
? true
|
||||
@@ -66,7 +73,11 @@ export class RuleEvaluator {
|
||||
if (canCacheRuleResult) {
|
||||
const cached = this.arbiter.ruleResultCache.get(ruleCacheKey);
|
||||
const cacheNow = this.arbiter.clock ? this.arbiter.clock() : Date.now();
|
||||
if (cached && cacheNow - cached.timestamp < this.arbiter.ruleResultCacheTTL) {
|
||||
// A graph mutation since the entry was computed invalidates it — the
|
||||
// result is derived from the graph and would be stale. _graphVersion
|
||||
// increments on every relation mutation.
|
||||
const freshGraph = cached && cached.graphVersion === (this.arbiter._graphVersion ?? 0);
|
||||
if (freshGraph && cacheNow - cached.timestamp < this.arbiter.ruleResultCacheTTL) {
|
||||
this.arbiter.ruleResultCacheStats.hits++;
|
||||
if (collectValues && finalValueContext && cached.result?.collectedValues?.length) {
|
||||
const ruleType = rule?.type || (rule?.union ? 'union' : rule?.intersection ? 'intersection' : rule?.exclusion ? 'exclusion' : 'rule');
|
||||
@@ -173,7 +184,8 @@ export class RuleEvaluator {
|
||||
const cacheNow = this.arbiter.clock ? this.arbiter.clock() : Date.now();
|
||||
this.arbiter.ruleResultCache.set(cacheKey, {
|
||||
result,
|
||||
timestamp: cacheNow
|
||||
timestamp: cacheNow,
|
||||
graphVersion: this.arbiter._graphVersion ?? 0
|
||||
});
|
||||
this.arbiter._cacheRuleResult(relation, cacheKey);
|
||||
return result;
|
||||
|
||||
@@ -44,8 +44,9 @@ import { QualitativeScale } from '../../qualitative/QualitativeScale.js';
|
||||
* }
|
||||
*/
|
||||
export class ChainRule extends BaseRule {
|
||||
constructor(arbiter) {
|
||||
constructor(arbiter, ruleEvaluator = null) {
|
||||
super(arbiter);
|
||||
this.ruleEvaluator = ruleEvaluator;
|
||||
|
||||
// Chain-specific caching with HyperbolicLRUCache for better memory management
|
||||
this.maxCacheSize = 2000;
|
||||
@@ -200,6 +201,80 @@ export class ChainRule extends BaseRule {
|
||||
: rawStep;
|
||||
const { relation: stepRelation, direction } = step;
|
||||
|
||||
// CONDITION STEP (rule-based step): a step carrying a `rule` config is a
|
||||
// condition-gated hop, not a plain edge traversal. The DSL compiler emits
|
||||
// these when a chain step references a logical/defeasible evidence.
|
||||
// - FINAL step: the object is known, so each current path's node is
|
||||
// checked against the object through the referenced rule.
|
||||
// - INTERMEDIATE step: the rule is EXPANDED from each current node
|
||||
// (rule-based reachability) — the reachable destinations of the
|
||||
// rule's base edges, filtered by its defeaters/requirements — and
|
||||
// the traversal continues from each discovered node.
|
||||
// The rule config is part of the chain cache key, so cache correctness
|
||||
// is preserved.
|
||||
if (step.rule) {
|
||||
if (!this.ruleEvaluator) {
|
||||
return this._createStandardResult({
|
||||
possibility: 0,
|
||||
reliability: 1.0,
|
||||
...(includeMeta && { meta: null }),
|
||||
reason: 'condition_step_requires_rule_evaluator'
|
||||
}, []);
|
||||
}
|
||||
if (stepIndex === steps.length - 1) {
|
||||
const conditionPaths = [];
|
||||
for (const currentPath of currentPaths) {
|
||||
const condResult = this.ruleEvaluator.evaluateRule(
|
||||
currentPath.id, currentPath.key, objectIdNum, objectKey,
|
||||
step.rule, new Set(visited || []), currentRelation, options
|
||||
);
|
||||
const condPossibility = condResult.possibility ?? 0;
|
||||
if (condPossibility <= 0) continue;
|
||||
const nextPossibility = Math.min(currentPath.possibility, condPossibility);
|
||||
if (fastPath && nextPossibility < minPossibility) continue;
|
||||
conditionPaths.push({
|
||||
id: objectIdNum,
|
||||
key: objectKey,
|
||||
possibility: nextPossibility,
|
||||
reliability: (currentPath.reliability ?? 1.0) * (condResult.reliability ?? 1.0),
|
||||
path: [...currentPath.path, objectKey],
|
||||
pathEntities: [...currentPath.pathEntities, { id: objectIdNum, key: objectKey, source: 'condition' }]
|
||||
});
|
||||
}
|
||||
currentPaths = conditionPaths;
|
||||
break;
|
||||
}
|
||||
|
||||
// INTERMEDIATE condition step: expand the rule from each current node.
|
||||
const pathMap = new Map();
|
||||
for (const currentPath of currentPaths) {
|
||||
const reachable = this._expandRuleFromSrc(currentPath.id, step.rule, options);
|
||||
for (const [nextId, cand] of reachable) {
|
||||
const nextKey = this.arbiter.resolveKey(nextId, options);
|
||||
if (!nextKey) continue;
|
||||
const nextPossibility = Math.min(currentPath.possibility, cand.possibility);
|
||||
if (fastPath && nextPossibility < minPossibility) continue;
|
||||
const nextReliability = (currentPath.reliability ?? 1.0) * cand.reliability;
|
||||
const existing = pathMap.get(nextId);
|
||||
if (existing && existing.possibility > nextPossibility) continue;
|
||||
if (existing && existing.possibility === nextPossibility && existing.reliability >= nextReliability) continue;
|
||||
pathMap.set(nextId, {
|
||||
id: nextId,
|
||||
key: nextKey,
|
||||
possibility: nextPossibility,
|
||||
reliability: nextReliability,
|
||||
path: [...currentPath.path, nextKey],
|
||||
pathEntities: [...currentPath.pathEntities, { id: nextId, key: nextKey, source: 'condition' }]
|
||||
});
|
||||
}
|
||||
}
|
||||
currentPaths = Array.from(pathMap.values());
|
||||
if (currentPaths.length === 0) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!stepRelation || !direction) {
|
||||
currentPaths = [];
|
||||
break;
|
||||
@@ -419,6 +494,205 @@ export class ChainRule extends BaseRule {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rule-based reachability: expand a rule config from a source node into its
|
||||
// reachable destinations. Used by intermediate condition steps — a
|
||||
// logical/defeasible evidence as a non-final chain step discovers its
|
||||
// reachable nodes (the base edges' destinations, filtered by the rule's
|
||||
// defeaters/requirements) instead of checking a single (src, dst) pair.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Expand a rule config from a source node into Map<dstId, {possibility, reliability}>.
|
||||
* Supports direct rules, logical union/intersection nodes, defeasible rules
|
||||
* (when/unless/never/requires/always), and nested chains.
|
||||
*/
|
||||
_expandRuleFromSrc(srcId, rule, options = null) {
|
||||
if (!rule || typeof rule !== 'object') return new Map();
|
||||
if (rule.type === 'direct' && rule.relation) {
|
||||
return this._expandDirectFromSrc(srcId, rule.relation, options);
|
||||
}
|
||||
if (rule.type === 'chain' && Array.isArray(rule.steps)) {
|
||||
return this._expandChainRuleFromSrc(srcId, rule, options);
|
||||
}
|
||||
if (rule.type === 'tuple_to_userset') {
|
||||
return this._expandTtuFromSrc(srcId, rule, options);
|
||||
}
|
||||
if (rule.type === 'logical') {
|
||||
if (rule.when || rule.unless || rule.never || rule.requires || rule.always) {
|
||||
return this._expandDefeasibleFromSrc(srcId, rule, options);
|
||||
}
|
||||
if (rule.union) {
|
||||
return this._expandLogicalNodeFromSrc(srcId, rule.union, 'union', options);
|
||||
}
|
||||
if (rule.intersection) {
|
||||
return this._expandLogicalNodeFromSrc(srcId, rule.intersection, 'intersection', options);
|
||||
}
|
||||
}
|
||||
return new Map();
|
||||
}
|
||||
|
||||
_expandDirectFromSrc(srcId, relation, options = null) {
|
||||
const edges = this.arbiter.relationManager.getRelationsFromSrc(srcId, relation, options);
|
||||
const out = new Map();
|
||||
for (const e of edges || []) {
|
||||
out.set(e.dst, { possibility: e.possibility ?? 1, reliability: e.reliability ?? 1 });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand a tuple_to_userset config from a source node. Semantics (forward,
|
||||
* the common case): src →computed→ intermediate, and intermediate ↔object
|
||||
* via tupleset (direction decides which end the intermediate sits on). The
|
||||
* reachable set is the OBJECTS sharing an intermediate with src. Combined
|
||||
* possibility is the weakest link across the two hops.
|
||||
*/
|
||||
_expandTtuFromSrc(srcId, rule, options = null) {
|
||||
const { tuplesetRelation, computedRelation, tuplesetDirection = 'out', reverse = false } = rule;
|
||||
if (!tuplesetRelation || !computedRelation) return new Map();
|
||||
// reverse swaps the roles: src is the object, intermediates come from the
|
||||
// tupleset side, and computed edges go from intermediate to the user.
|
||||
const srcAsUser = !reverse;
|
||||
const computedRel = srcAsUser ? computedRelation : tuplesetRelation;
|
||||
const tuplesetRel = srcAsUser ? tuplesetRelation : computedRelation;
|
||||
const computedEdges = this.arbiter.relationManager.getRelationsFromSrc(srcId, computedRel, options);
|
||||
const out = new Map();
|
||||
for (const ce of computedEdges || []) {
|
||||
const intermediateId = ce.dst;
|
||||
const tsEdges = tuplesetDirection === 'in'
|
||||
? this.arbiter.relationManager.getRelationsFromSrc(intermediateId, tuplesetRel, options)
|
||||
: this.arbiter.relationManager.getRelationsToDst(intermediateId, tuplesetRel, options);
|
||||
for (const te of tsEdges || []) {
|
||||
const objId = tuplesetDirection === 'in' ? te.dst : te.src;
|
||||
const poss = Math.min(ce.possibility ?? 1, te.possibility ?? 1);
|
||||
const rel = (ce.reliability ?? 1) * (te.reliability ?? 1);
|
||||
const cur = out.get(objId);
|
||||
if (!cur || poss > cur.possibility) out.set(objId, { possibility: poss, reliability: rel });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
_expandLogicalNodeFromSrc(srcId, node, op, options = null) {
|
||||
const rules = (node && node.rules) || [];
|
||||
if (rules.length === 0) return new Map();
|
||||
if (op === 'union') {
|
||||
const out = new Map();
|
||||
for (const r of rules) {
|
||||
const m = this._expandRuleFromSrc(srcId, r, options);
|
||||
for (const [id, v] of m) {
|
||||
const cur = out.get(id);
|
||||
if (!cur || v.possibility > cur.possibility) out.set(id, v);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
// intersection: nodes reachable via every branch, weakest-link combined
|
||||
const maps = rules.map(r => this._expandRuleFromSrc(srcId, r, options));
|
||||
const out = new Map();
|
||||
for (const [id, v] of maps[0]) {
|
||||
let ok = true;
|
||||
let min = v.possibility;
|
||||
let rel = v.reliability;
|
||||
for (let i = 1; i < maps.length; i++) {
|
||||
const other = maps[i].get(id);
|
||||
if (!other) { ok = false; break; }
|
||||
min = Math.min(min, other.possibility);
|
||||
rel *= other.reliability;
|
||||
}
|
||||
if (ok) out.set(id, { possibility: min, reliability: rel });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
_expandDefeasibleFromSrc(srcId, rule, options = null) {
|
||||
const base = this._expandBaseFromSrc(srcId, rule, options);
|
||||
if (base.size === 0) return base;
|
||||
const out = new Map();
|
||||
const evalAt = (candId, candKey, node) => {
|
||||
if (!node) return { poss: 1, rel: 1 };
|
||||
const wrapped = node.union ? { union: node.union } : node;
|
||||
const r = this.ruleEvaluator.evaluateRule(
|
||||
srcId, null, candId, candKey, wrapped, new Set(), null, options || {}
|
||||
);
|
||||
return { poss: r.possibility ?? 0, rel: r.reliability ?? 1 };
|
||||
};
|
||||
for (const [candId, cand] of base) {
|
||||
const candKey = this.arbiter.resolveKey(candId, options);
|
||||
if (rule.never) {
|
||||
const n = evalAt(candId, candKey, rule.never);
|
||||
if (n.poss > 0.5) continue;
|
||||
}
|
||||
let defeat = 0;
|
||||
let defeatRel = 1;
|
||||
if (rule.unless) {
|
||||
const u = evalAt(candId, candKey, rule.unless);
|
||||
defeat = u.poss;
|
||||
defeatRel = u.rel;
|
||||
}
|
||||
let req = 1;
|
||||
let reqRel = 1;
|
||||
if (rule.requires) {
|
||||
const q = evalAt(candId, candKey, rule.requires);
|
||||
req = q.poss;
|
||||
reqRel = q.rel;
|
||||
}
|
||||
const poss = cand.possibility * req * (1 - defeat);
|
||||
if (poss <= 0) continue;
|
||||
out.set(candId, {
|
||||
possibility: poss,
|
||||
reliability: cand.reliability * reqRel * defeatRel
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
_expandBaseFromSrc(srcId, rule, options = null) {
|
||||
if (rule.when && rule.when.intersection) {
|
||||
return this._expandLogicalNodeFromSrc(srcId, rule.when.intersection, 'intersection', options);
|
||||
}
|
||||
if (rule.when && rule.when.union) {
|
||||
return this._expandLogicalNodeFromSrc(srcId, rule.when.union, 'union', options);
|
||||
}
|
||||
if (rule.when && rule.when.rules) {
|
||||
return this._expandLogicalNodeFromSrc(srcId, rule.when, 'union', options);
|
||||
}
|
||||
if (rule.union) {
|
||||
return this._expandLogicalNodeFromSrc(srcId, rule.union, 'union', options);
|
||||
}
|
||||
if (rule.always && rule.always.direct) {
|
||||
return this._expandRuleFromSrc(srcId, rule.always.direct, options);
|
||||
}
|
||||
return new Map();
|
||||
}
|
||||
|
||||
_expandChainRuleFromSrc(srcId, rule, options = null) {
|
||||
let current = [{ id: srcId, possibility: 1, reliability: 1 }];
|
||||
for (const step of rule.steps || []) {
|
||||
const stepName = typeof step === 'string' ? step : step.relation;
|
||||
const stepRule = typeof step === 'string' ? null : step.rule;
|
||||
const next = [];
|
||||
for (const node of current) {
|
||||
const m = stepRule
|
||||
? this._expandRuleFromSrc(node.id, stepRule, options)
|
||||
: this._expandDirectFromSrc(node.id, stepName, options);
|
||||
for (const [id, v] of m) {
|
||||
next.push({
|
||||
id,
|
||||
possibility: Math.min(node.possibility, v.possibility),
|
||||
reliability: node.reliability * v.reliability
|
||||
});
|
||||
}
|
||||
}
|
||||
current = next;
|
||||
if (current.length === 0) break;
|
||||
}
|
||||
const out = new Map();
|
||||
for (const n of current) out.set(n.id, { possibility: n.possibility, reliability: n.reliability });
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cache key for chain result using numeric IDs
|
||||
* @private
|
||||
@@ -433,6 +707,12 @@ export class ChainRule extends BaseRule {
|
||||
const key = this._getChainResultCacheKey(userId, objectId, steps);
|
||||
const entry = this.chainResultCache.get(key);
|
||||
|
||||
// A graph mutation since the entry was computed invalidates it: the
|
||||
// reachability it captured is stale. The arbiter increments _graphVersion
|
||||
// on every relation mutation, so any mismatch is a miss.
|
||||
if (entry && entry.graphVersion !== (this.arbiter._graphVersion ?? 0)) {
|
||||
return null;
|
||||
}
|
||||
const cacheNow = this.arbiter.clock ? this.arbiter.clock() : Date.now();
|
||||
if (entry && cacheNow - entry.timestamp < this.cacheTTL) {
|
||||
return entry.result;
|
||||
@@ -455,7 +735,8 @@ export class ChainRule extends BaseRule {
|
||||
const cacheNow = this.arbiter.clock ? this.arbiter.clock() : Date.now();
|
||||
this.chainResultCache.set(key, {
|
||||
result,
|
||||
timestamp: cacheNow
|
||||
timestamp: cacheNow,
|
||||
graphVersion: this.arbiter._graphVersion ?? 0
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+97
-10
@@ -25,6 +25,13 @@ export class Arbiter {
|
||||
constructor(options = {}) {
|
||||
this.options = options;
|
||||
|
||||
// Monotonic graph mutation counter: incremented on every relation edge
|
||||
// mutation (and node removal). Derived-result caches (e.g. the ChainRule
|
||||
// result cache) stamp entries with the version they were computed at and
|
||||
// treat any newer version as a miss, so a graph change can never serve a
|
||||
// stale chain/authorization result.
|
||||
this._graphVersion = 0;
|
||||
|
||||
// Audit affordance (caller-wired, zero cost when absent): invoked once
|
||||
// per check with a minimal decision record. The engine does NOT store
|
||||
// audit state — the caller owns persistence and retention. The record
|
||||
@@ -64,6 +71,16 @@ export class Arbiter {
|
||||
this.directCheckCacheSize = options.directCheckCacheSize || 10000;
|
||||
this.directCheckCacheTTL = options.directCheckCacheTTL || 60000; // 1 minute
|
||||
|
||||
// Direct-check cache key indexes (keys are rolling hashes, so relation-
|
||||
// and node-level invalidation cannot parse them): relation -> keys,
|
||||
// nodeId -> keys, key -> relations. Populated via _trackDirectCheckKey.
|
||||
this.directCheckCacheKeysByRelation = new Map();
|
||||
this.directCheckCacheKeysByNode = new Map();
|
||||
this.directKeyToRelations = new Map();
|
||||
// Reverse of dependencyIndex: checked relation -> Set of base relations it
|
||||
// reads (so a base-relation mutation invalidates overridden-config keys).
|
||||
this.dependencyIndexByRelation = new Map();
|
||||
|
||||
// Only create cache if caching is not disabled
|
||||
if (!this.disableCaching && !this.disableDirectCaching) {
|
||||
this.directCheckCache = this.cacheFactory(this.directCheckCacheSize, {
|
||||
@@ -219,6 +236,14 @@ export class Arbiter {
|
||||
this.dependencyIndex.set(baseRel, entry);
|
||||
}
|
||||
entry.all.add(relation);
|
||||
// Reverse map: the checked relation reads baseRel, so a baseRel
|
||||
// mutation must invalidate this relation's direct-check keys.
|
||||
let bases = this.dependencyIndexByRelation.get(relation);
|
||||
if (!bases) {
|
||||
bases = new Set();
|
||||
this.dependencyIndexByRelation.set(relation, bases);
|
||||
}
|
||||
bases.add(baseRel);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -426,20 +451,23 @@ export class Arbiter {
|
||||
for (const rel of entry.all) affected.add(rel);
|
||||
}
|
||||
|
||||
// Keys are rolling hashes (no pipe-delimited string form to match), so
|
||||
// invalidation uses the per-relation key index populated at cache-set
|
||||
// time: every direct-check result is tracked under the CHECKED relation
|
||||
// AND its base relations (via _trackDirectCheckKey), so deleting the
|
||||
// tracked keys for an affected relation covers overridden-config checks.
|
||||
for (const rel of affected) {
|
||||
// Composite cache keys are `<stringId|relationId|stringId>` — the
|
||||
// middle component is the relation ID, so match on that (a name-based
|
||||
// pattern can never hit). This also covers keys built from either
|
||||
// string ids or node ids, since only the middle component matters.
|
||||
if (typeof this.directCheckCache.invalidateByPattern === 'function') {
|
||||
const relId = this.keyManager._getRelationId(rel);
|
||||
const pattern = new RegExp(`^[^|]*\\|${relId}\\|[^|]*$`);
|
||||
this.directCheckCache.invalidateByPattern(pattern);
|
||||
const keys = this.directCheckCacheKeysByRelation.get(rel);
|
||||
if (!keys) continue;
|
||||
for (const key of keys) {
|
||||
this._deleteFromCache(this.directCheckCache, key);
|
||||
this.directKeyToRelations.delete(key);
|
||||
}
|
||||
keys.clear();
|
||||
}
|
||||
|
||||
// Explicit composite-key deletes (kept for cache implementations that
|
||||
// lack invalidateByPattern).
|
||||
// Explicit composite-key deletes (recomputed hashes) — cheap belt for the
|
||||
// exact (src, relation, dst) pair.
|
||||
const srcId = this.keyManager.getStringId(srcKey);
|
||||
const dstId = this.keyManager.getStringId(dstKey);
|
||||
const key1 = this.keyManager.createCompositeKey(srcId, relation, dstId);
|
||||
@@ -455,6 +483,65 @@ export class Arbiter {
|
||||
this.invalidateRuleResultCacheByRelation(relation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a direct-check cache key so relation- and node-level invalidation
|
||||
* can find it without parsing the (hashed) key. Tracked under the CHECKED
|
||||
* relation and its base relations (so a base-relation mutation invalidates
|
||||
* overridden-config checks) and under the subject/object node ids.
|
||||
*/
|
||||
_trackDirectCheckKey(relation, srcNodeId, dstNodeId, cacheKey) {
|
||||
if (!cacheKey) return;
|
||||
const rels = new Set([relation]);
|
||||
const baseEntry = this.dependencyIndexByRelation.get(relation);
|
||||
if (baseEntry) for (const base of baseEntry) rels.add(base);
|
||||
for (const rel of rels) {
|
||||
let set = this.directCheckCacheKeysByRelation.get(rel);
|
||||
if (!set) {
|
||||
set = new Set();
|
||||
this.directCheckCacheKeysByRelation.set(rel, set);
|
||||
}
|
||||
set.add(cacheKey);
|
||||
}
|
||||
if (srcNodeId !== undefined && srcNodeId !== null) {
|
||||
let set = this.directCheckCacheKeysByNode.get(srcNodeId);
|
||||
if (!set) {
|
||||
set = new Set();
|
||||
this.directCheckCacheKeysByNode.set(srcNodeId, set);
|
||||
}
|
||||
set.add(cacheKey);
|
||||
}
|
||||
if (dstNodeId !== undefined && dstNodeId !== null && dstNodeId !== srcNodeId) {
|
||||
let set = this.directCheckCacheKeysByNode.get(dstNodeId);
|
||||
if (!set) {
|
||||
set = new Set();
|
||||
this.directCheckCacheKeysByNode.set(dstNodeId, set);
|
||||
}
|
||||
set.add(cacheKey);
|
||||
}
|
||||
let byKey = this.directKeyToRelations.get(cacheKey);
|
||||
if (!byKey) {
|
||||
byKey = new Set();
|
||||
this.directKeyToRelations.set(cacheKey, byKey);
|
||||
}
|
||||
for (const rel of rels) byKey.add(rel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate direct-check entries involving a node id (node removal).
|
||||
*/
|
||||
_invalidateDirectCheckCacheByNode(nodeId) {
|
||||
if (!this.directCheckCache || nodeId === undefined) return;
|
||||
const keys = this.directCheckCacheKeysByNode.get(nodeId);
|
||||
if (!keys) return;
|
||||
for (const key of keys) {
|
||||
this._deleteFromCache(this.directCheckCache, key);
|
||||
const rels = this.directKeyToRelations.get(key);
|
||||
if (rels) for (const rel of rels) this.directCheckCacheKeysByRelation.get(rel)?.delete(key);
|
||||
this.directKeyToRelations.delete(key);
|
||||
}
|
||||
keys.clear();
|
||||
}
|
||||
|
||||
_cacheRuleResult(relation, cacheKey) {
|
||||
if (!this.ruleResultCache || !cacheKey) return;
|
||||
let set = this.ruleResultCacheKeysByRelation.get(relation);
|
||||
|
||||
@@ -696,6 +696,12 @@ export class RelationManager {
|
||||
* Cache management methods
|
||||
*/
|
||||
_invalidateRelationCaches(srcId, relation, dstId) {
|
||||
// Any relation mutation invalidates the graph version so derived-result
|
||||
// caches (ChainRule result cache) stamp/validate against it — a graph
|
||||
// change can never serve a stale chain result.
|
||||
if (this.arbiter && typeof this.arbiter._graphVersion === 'number') {
|
||||
this.arbiter._graphVersion++;
|
||||
}
|
||||
this._caches.invalidateRelationCaches(srcId, relation, dstId);
|
||||
}
|
||||
|
||||
|
||||
@@ -90,38 +90,41 @@ export class UnifiedKeyManager {
|
||||
}
|
||||
|
||||
const relationId = this._getRelationId(relation);
|
||||
return `${srcId}|${relationId}|${dstId}`;
|
||||
// Rolling hash (no string concatenation). Exact-key caches recompute the
|
||||
// key to delete; the direct-check cache additionally tracks keys by
|
||||
// relation/node so relation-level invalidation does not need the
|
||||
// pipe-delimited string form.
|
||||
return this._rollingHash(srcId, relationId, dstId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create source-relation key
|
||||
* @param {number} srcId - Source ID
|
||||
* @param {string} relation - Relation name
|
||||
* @returns {string} Composite key
|
||||
* @returns {number} Rolling-hash key
|
||||
*/
|
||||
createSrcRelKey(srcId, relation) {
|
||||
if (srcId > this.options.maxSrcId) {
|
||||
throw new Error(`Source ID ${srcId} exceeds max range ${this.options.maxSrcId}`);
|
||||
}
|
||||
const relationId = this._getRelationId(relation);
|
||||
// Direction marker: fromSrc and toDst lookups share the relation lookup
|
||||
// cache, and identical `id|relId` keys made the second-direction lookup
|
||||
// return the first direction's cached edges.
|
||||
return `${srcId}|${relationId}|s`;
|
||||
// Direction marker keeps fromSrc and toDst lookups distinct (identical
|
||||
// srcId|relationId pairs previously shared keys across directions).
|
||||
return this._rollingHash(srcId, relationId, 0x73); // 's'
|
||||
}
|
||||
|
||||
/**
|
||||
* Create destination-relation key
|
||||
* @param {number} dstId - Destination ID
|
||||
* @param {string} relation - Relation name
|
||||
* @returns {number} Composite key
|
||||
* @returns {number} Rolling-hash key
|
||||
*/
|
||||
createDstRelKey(dstId, relation) {
|
||||
if (dstId > this.options.maxDstId) {
|
||||
throw new Error(`Destination ID ${dstId} exceeds max range ${this.options.maxDstId}`);
|
||||
}
|
||||
const relationId = this._getRelationId(relation);
|
||||
return `${dstId}|${relationId}|d`;
|
||||
return this._rollingHash(dstId, relationId, 0x64); // 'd'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,7 +132,7 @@ export class UnifiedKeyManager {
|
||||
* @param {number} userId - User ID
|
||||
* @param {number} objectId - Object ID
|
||||
* @param {Array} steps - Chain steps
|
||||
* @returns {number} Composite key
|
||||
* @returns {number} Rolling-hash composite key
|
||||
*/
|
||||
createChainKey(userId, objectId, steps) {
|
||||
if (userId > this.options.maxSrcId) {
|
||||
@@ -139,7 +142,79 @@ export class UnifiedKeyManager {
|
||||
throw new Error(`Object ID ${objectId} exceeds max range ${this.options.maxDstId}`);
|
||||
}
|
||||
|
||||
return JSON.stringify([userId, objectId, steps]);
|
||||
// Rolling hash instead of JSON.stringify: no string allocation, no
|
||||
// serialization of possibly-nested step configs. Steps may be strings,
|
||||
// { relation, direction } objects, or { rule: <config>, conditionStep }
|
||||
// objects — each feeds the hash incrementally.
|
||||
return this._rollingHash(userId, objectId, steps);
|
||||
}
|
||||
|
||||
/**
|
||||
* 53-bit rolling hash over heterogeneous components (numbers, strings,
|
||||
* booleans, arrays, nested objects). Dual 32-bit FNV-1a lanes combined into
|
||||
* a single exact integer (< 2^53) usable as a Map key — no string
|
||||
* concatenation or serialization on the hot path. Integers feed their
|
||||
* high/low 32-bit halves; floats feed their exact 64-bit byte pattern;
|
||||
* objects feed their sorted key/value pairs so ordering is stable.
|
||||
* @returns {number} exact integer in [0, 2^53)
|
||||
*/
|
||||
_rollingHash(...parts) {
|
||||
let h1 = 0x811c9dc5;
|
||||
let h2 = 0x9e3779b1;
|
||||
const floatBuf = new Float64Array(1);
|
||||
const floatBytes = new Uint8Array(floatBuf.buffer);
|
||||
const mix1 = (h, x) => Math.imul(h ^ x, 0x01000193) >>> 0;
|
||||
const mix2 = (h, x) => Math.imul((h ^ x) ^ 0x5bd1e995, 0x01000193) >>> 0;
|
||||
const feed = (v) => {
|
||||
if (typeof v === 'number') {
|
||||
if (Number.isInteger(v)) {
|
||||
const hi = Math.floor(v / 0x100000000);
|
||||
const lo = v >>> 0;
|
||||
h1 = mix1(h1, hi);
|
||||
h2 = mix2(h2, lo);
|
||||
h1 = mix1(h1, lo);
|
||||
h2 = mix2(h2, hi ^ 0x9e37);
|
||||
} else {
|
||||
floatBuf[0] = v;
|
||||
for (let i = 0; i < 8; i++) {
|
||||
h1 = mix1(h1, floatBytes[i]);
|
||||
h2 = mix2(h2, floatBytes[i]);
|
||||
}
|
||||
}
|
||||
} else if (typeof v === 'string') {
|
||||
h1 = mix1(h1, v.length | 0x8000);
|
||||
h2 = mix2(h2, v.length ^ 0x55aa);
|
||||
for (let i = 0; i < v.length; i++) {
|
||||
h1 = mix1(h1, v.charCodeAt(i));
|
||||
h2 = mix2(h2, v.charCodeAt(i) ^ 0xa5);
|
||||
}
|
||||
} else if (typeof v === 'boolean') {
|
||||
h1 = mix1(h1, v ? 0x1111 : 0x2222);
|
||||
h2 = mix2(h2, v ? 0x3333 : 0x4444);
|
||||
} else if (v === null) {
|
||||
h1 = mix1(h1, 0xaaaa);
|
||||
h2 = mix2(h2, 0xbbbb);
|
||||
} else if (v === undefined) {
|
||||
h1 = mix1(h1, 0xcccc);
|
||||
h2 = mix2(h2, 0xdddd);
|
||||
} else if (Array.isArray(v)) {
|
||||
h1 = mix1(h1, v.length | 0x800000);
|
||||
h2 = mix2(h2, v.length ^ 0x1234);
|
||||
for (const x of v) feed(x);
|
||||
} else if (typeof v === 'object') {
|
||||
h1 = mix1(h1, 0x5eed);
|
||||
h2 = mix2(h2, 0xcafe);
|
||||
const keys = Object.keys(v).sort();
|
||||
for (const k of keys) {
|
||||
feed(k);
|
||||
feed(v[k]);
|
||||
}
|
||||
}
|
||||
};
|
||||
for (const p of parts) feed(p);
|
||||
// Combine both 32-bit lanes into an exact < 2^53 integer:
|
||||
// h1 * 2^21 (h1 < 2^32 => product < 2^53) + top 21 bits of h2.
|
||||
return h1 * 0x200000 + (h2 >>> 11);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -66,11 +66,11 @@ export class RelationCaches {
|
||||
}
|
||||
|
||||
makeValueCacheKeyBySrc(srcId, relationId) {
|
||||
return `${srcId}|${relationId}`;
|
||||
return this.manager.arbiter.keyManager._rollingHash(srcId, relationId, 0x5352); // 'SR'
|
||||
}
|
||||
|
||||
makeValueCacheKeyByDst(dstId, relationId) {
|
||||
return `${dstId}|${relationId}`;
|
||||
return this.manager.arbiter.keyManager._rollingHash(dstId, relationId, 0x4454); // 'DT'
|
||||
}
|
||||
|
||||
deleteFromCache(cache, key) {
|
||||
|
||||
@@ -151,9 +151,14 @@ describe('HyperbolicLRUCache Invalidation', () => {
|
||||
const bobTouchingAlice = arbiter.keyManager.createCompositeKey(bobId, 'can_read', aliceId);
|
||||
const carolKey = arbiter.keyManager.createCompositeKey(carolId, 'can_read', bobId);
|
||||
|
||||
// Direct-check keys are rolling hashes; invalidation-by-node uses the
|
||||
// per-node key index, so every injected key must be tracked.
|
||||
arbiter.directCheckCache.set(aliceKey, { result: { possibility: 0.8 }, timestamp: Date.now() });
|
||||
arbiter._trackDirectCheckKey('can_read', aliceId, bobId, aliceKey);
|
||||
arbiter.directCheckCache.set(bobTouchingAlice, { result: { possibility: 0.9 }, timestamp: Date.now() });
|
||||
arbiter._trackDirectCheckKey('can_read', bobId, aliceId, bobTouchingAlice);
|
||||
arbiter.directCheckCache.set(carolKey, { result: { possibility: 0.7 }, timestamp: Date.now() });
|
||||
arbiter._trackDirectCheckKey('can_read', carolId, bobId, carolKey);
|
||||
|
||||
assert.ok(arbiter.directCheckCache.has(aliceKey));
|
||||
assert.ok(arbiter.directCheckCache.has(bobTouchingAlice));
|
||||
|
||||
@@ -75,7 +75,7 @@ describe('Authorization config consistency (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('path-parity', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('path-parity', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 300, seed: 'authz-config-parity' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -120,7 +120,7 @@ describe('Authorization config consistency (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('override-honored', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('override-honored', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 300, seed: 'authz-config-override' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -168,7 +168,7 @@ describe('Authorization config consistency (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('remediation-contract', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('remediation-contract', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 300, seed: 'authz-config-remediation' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ describe('Authorization graph semantics (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('direct-exact', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('direct-exact', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 400, seed: 'authz-graph-direct' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -101,7 +101,7 @@ describe('Authorization graph semantics (rigor)', () => {
|
||||
rigor.fn('check', check, rigor.args(rigor.gen.int(2, 6)))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('absent-denies', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('absent-denies', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 300, seed: 'authz-graph-absent' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -144,7 +144,7 @@ describe('Authorization graph semantics (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('weakest-link', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('weakest-link', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 400, seed: 'authz-graph-chain' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -194,7 +194,7 @@ describe('Authorization graph semantics (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('disjunctive-max', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('disjunctive-max', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 500, seed: 'authz-graph-multipath' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -246,7 +246,7 @@ describe('Authorization graph semantics (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('tus-weakest-link', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('tus-weakest-link', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 400, seed: 'authz-graph-tus' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -285,7 +285,7 @@ describe('Authorization graph semantics (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('revoke-invalidates', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('revoke-invalidates', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 300, seed: 'authz-graph-mutation' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ describe('Batch loading consistency (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('batch-parity', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('batch-parity', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 400, seed: 'batch-parity' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -177,7 +177,7 @@ describe('Batch loading consistency (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('batch-mutation-parity', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('batch-mutation-parity', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 400, seed: 'batch-mutation-parity' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -290,7 +290,7 @@ describe('Binary (threshold) mode parity (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('binary-normal-agreement', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('binary-normal-agreement', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 1200, seed: 'binary-mode-config-matrix' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -349,7 +349,7 @@ describe('Binary (threshold) mode parity (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('mutation-freshness', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('mutation-freshness', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 800, seed: 'binary-mode-mutation-parity' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ describe('Cache correctness under mutation (rigor)', () => {
|
||||
rigor.fn('check', check, rigor.args(opGen))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('cache-parity', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('cache-parity', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 600, seed: 'cache-onoff-parity' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -159,7 +159,7 @@ describe('Cache correctness under mutation (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('override-cache-fresh', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('override-cache-fresh', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 400, seed: 'cache-override-freshness' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -205,7 +205,7 @@ describe('Cache correctness under mutation (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('ttl-contract', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('ttl-contract', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 300, seed: 'cache-ttl-contract' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ describe('ChainRule evaluation (rigor)', () => {
|
||||
const report = await rigor.campaign(
|
||||
[rigor.fn('check', check, rigor.args())],
|
||||
rigor.crucible([
|
||||
rigor.invariant('empty-steps', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('empty-steps', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'chain-rule-empty-steps', effort: 200 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -82,7 +82,7 @@ describe('ChainRule evaluation (rigor)', () => {
|
||||
rigor.args(rigor.gen.float({ min: 0, max: 1 }))
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('possibility-bounded', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('possibility-bounded', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'chain-rule-possibility-bounded', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -117,7 +117,7 @@ describe('ChainRule evaluation (rigor)', () => {
|
||||
const report = await rigor.campaign(
|
||||
[rigor.fn('check', check, rigor.args())],
|
||||
rigor.crucible([
|
||||
rigor.invariant('no-path', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('no-path', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'chain-rule-no-path', effort: 500 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -154,7 +154,7 @@ describe('ChainRule evaluation (rigor)', () => {
|
||||
rigor.args(rigor.gen.float({ min: 0.01, max: 1 }))
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('one-step-pos', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('one-step-pos', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'chain-rule-one-step', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -204,7 +204,7 @@ describe('ChainRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('two-step-chain', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('two-step-chain', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'chain-rule-two-step', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ describe('PartialGraphContext.getChallengeProof (rigor)', () => {
|
||||
rigor.crucible([
|
||||
rigor.invariant(
|
||||
'oracle-matches-brute-force',
|
||||
({ error, errorMessage }) => !error && !errorMessage
|
||||
({ actual }) => actual !== undefined
|
||||
)
|
||||
])
|
||||
).run({ seed: 'challenge-proof-oracle', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
|
||||
@@ -150,7 +150,7 @@ describe('PartialGraphContext.getChallengeProof (rigor)', () => {
|
||||
)
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('no-expired', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('no-expired', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'challenge-proof-expired', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -225,7 +225,7 @@ describe('PartialGraphContext.getChallengeProof (rigor)', () => {
|
||||
)
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('most-recent', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('most-recent', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'challenge-proof-most-recent', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -281,7 +281,7 @@ describe('PartialGraphContext.getChallengeProof (rigor)', () => {
|
||||
)
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('within-window', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('within-window', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'challenge-proof-within-ms', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ describe('ChallengeRule._resolveSubjectKey (rigor)', () => {
|
||||
)
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('subjectKey-wins', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('subjectKey-wins', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'challenge-rule-subject-key-wins', effort: 1000 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -116,7 +116,7 @@ describe('ChallengeRule._resolveSubjectKey (rigor)', () => {
|
||||
)
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('subject-mapping', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('subject-mapping', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'challenge-rule-subject-mapping', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -197,7 +197,7 @@ describe('ChallengeRule._resolveWithinMs (rigor)', () => {
|
||||
)
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('within-units', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('within-units', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'challenge-rule-within-units', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -260,7 +260,7 @@ describe('ChallengeRule._resolveWithinMs (rigor)', () => {
|
||||
)
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('within-priority', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('within-priority', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'challenge-rule-within-priority', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -293,7 +293,7 @@ describe('ChallengeRule._resolveWithinMs (rigor)', () => {
|
||||
)
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('null-when-absent', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('null-when-absent', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'challenge-rule-null-when-absent', effort: 500 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -333,7 +333,7 @@ describe('ChallengeRule._buildRequirement (rigor)', () => {
|
||||
)
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('buildRequirement', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('buildRequirement', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'challenge-rule-build-requirement', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ describe('check/explain agreement (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('check-explain-agree', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('check-explain-agree', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 500, seed: 'explain-agreement' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -148,7 +148,7 @@ describe('check/explain agreement (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('used-facts-consistent', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('used-facts-consistent', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 600, seed: 'explain-used-facts' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -196,7 +196,7 @@ describe('check/explain agreement (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('remediation-consistent', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('remediation-consistent', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 400, seed: 'explain-remediation' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ describe('Relational comparator full-path parity (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('comparator-full-path', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('comparator-full-path', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 1200, seed: 'comparator-full-path-parity' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -162,7 +162,7 @@ describe('Relational comparator full-path parity (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('comparator-aggregation', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('comparator-aggregation', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 500, seed: 'comparator-aggregation' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -154,10 +154,10 @@ describe('Complex-graph batch/cache crucibles (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('batch-sequential-parity', ({ error, errorMessage }) => !error || !String(errorMessage).startsWith('[parity]')),
|
||||
rigor.invariant('batch-mutation', ({ error, errorMessage }) => !error || !String(errorMessage).startsWith('[mutation]')),
|
||||
rigor.invariant('cache-interaction', ({ error, errorMessage }) => !error || !String(errorMessage).startsWith('[cache]')),
|
||||
rigor.invariant('fixture-size', ({ error, errorMessage }) => !error || !String(errorMessage).startsWith('[fixture]'))
|
||||
rigor.invariant('batch-sequential-parity', ({ actual }) => actual !== undefined),
|
||||
rigor.invariant('batch-mutation', ({ actual }) => actual !== undefined),
|
||||
rigor.invariant('cache-interaction', ({ actual }) => actual !== undefined),
|
||||
rigor.invariant('fixture-size', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 150, seed: 'complex-graph-batch-crucible', artifacts: { dir: '', persist: 'never' } });
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ describe('Complex-graph crucibles (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('parity', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('parity', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 300, seed: 'complex-graph-parity', artifacts: { dir: '', persist: 'never' } });
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ describe('Complex-graph mutation crucibles (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('mutation-freshness', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('mutation-freshness', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 300, seed: 'complex-graph-mutation-freshness', artifacts: { dir: '', persist: 'never' } });
|
||||
|
||||
|
||||
@@ -145,10 +145,10 @@ describe('Complex-graph overlay crucibles (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('overlay-surfaces', ({ error, errorMessage }) => !error || !String(errorMessage).startsWith('[surfaces]')),
|
||||
rigor.invariant('persistent-wins', ({ error, errorMessage }) => !error || !String(errorMessage).startsWith('[wins]')),
|
||||
rigor.invariant('overlay-binary-parity', ({ error, errorMessage }) => !error || !String(errorMessage).startsWith('[binary]')),
|
||||
rigor.invariant('overlay-on-complex', ({ error, errorMessage }) => !error || !String(errorMessage).startsWith('[complex]'))
|
||||
rigor.invariant('overlay-surfaces', ({ actual }) => actual !== undefined),
|
||||
rigor.invariant('persistent-wins', ({ actual }) => actual !== undefined),
|
||||
rigor.invariant('overlay-binary-parity', ({ actual }) => actual !== undefined),
|
||||
rigor.invariant('overlay-on-complex', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 200, seed: 'complex-graph-overlay-crucible', artifacts: { dir: '', persist: 'never' } });
|
||||
|
||||
|
||||
@@ -148,9 +148,9 @@ describe('Complex-graph PLTC reachability crucibles (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('verdict-parity', ({ error, errorMessage }) => !error || !String(errorMessage).startsWith('[verdict]')),
|
||||
rigor.invariant('fast-fail-soundness', ({ error, errorMessage }) => !error || !String(errorMessage).startsWith('[soundness]')),
|
||||
rigor.invariant('null-defer-contract', ({ error, errorMessage }) => !error || !String(errorMessage).startsWith('[vacuity]'))
|
||||
rigor.invariant('verdict-parity', ({ actual }) => actual !== undefined),
|
||||
rigor.invariant('fast-fail-soundness', ({ actual }) => actual !== undefined),
|
||||
rigor.invariant('null-defer-contract', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 200, seed: 'complex-graph-reachability-crucible', artifacts: { dir: '', persist: 'never' } });
|
||||
|
||||
|
||||
@@ -170,9 +170,9 @@ describe('Complex-graph value-TTL crucibles (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('freshness-parity', ({ error, errorMessage }) => !error || !String(errorMessage).startsWith('[freshness]')),
|
||||
rigor.invariant('mutation-with-time', ({ error, errorMessage }) => !error || !String(errorMessage).startsWith('[mutation]')),
|
||||
rigor.invariant('snapshot-preserves-ttl', ({ error, errorMessage }) => !error || !String(errorMessage).startsWith('[snapshot]'))
|
||||
rigor.invariant('freshness-parity', ({ actual }) => actual !== undefined),
|
||||
rigor.invariant('mutation-with-time', ({ actual }) => actual !== undefined),
|
||||
rigor.invariant('snapshot-preserves-ttl', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 250, seed: 'complex-graph-ttl-crucible', artifacts: { dir: '', persist: 'never' } });
|
||||
|
||||
|
||||
@@ -125,10 +125,15 @@ describe('Complex-graph value/comparator crucibles (rigor)', () => {
|
||||
}
|
||||
|
||||
// TTL-EXPIRY-ON-COMPARATOR: both operands past TTL -> deny, mirror agrees.
|
||||
// Use values NOT in VALUE_SET (137 > 30): the engine keeps the OLD
|
||||
// timestamp when a rewrite does not change the value, so a value equal
|
||||
// to whatever the mutation loop last wrote would NOT refresh freshness
|
||||
// and the pre-expiry grant would (correctly) not materialize. Writing
|
||||
// guaranteed-different values forces a timestamp refresh.
|
||||
const kExp = keys[keys.length - 1];
|
||||
engineNow = BASE_NOW + 2_000_000;
|
||||
write(kExp, 'balance', 100);
|
||||
write(kExp, 'price', 50);
|
||||
write(kExp, 'balance', 137);
|
||||
write(kExp, 'price', 30);
|
||||
if (checkAt(kExp).normal !== 1) fail(`[expiry] pre-expiry grant missing (seed=${seed})`);
|
||||
engineNow += TTL + 1;
|
||||
const expired = checkAt(kExp);
|
||||
@@ -149,9 +154,9 @@ describe('Complex-graph value/comparator crucibles (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('comparator-parity', ({ error, errorMessage }) => !error || !String(errorMessage).startsWith('[parity]')),
|
||||
rigor.invariant('value-mutation', ({ error, errorMessage }) => !error || !String(errorMessage).startsWith('[mutation]')),
|
||||
rigor.invariant('ttl-expiry-on-comparator', ({ error, errorMessage }) => !error || !String(errorMessage).startsWith('[expiry]'))
|
||||
rigor.invariant('comparator-parity', ({ actual }) => actual !== undefined),
|
||||
rigor.invariant('value-mutation', ({ actual }) => actual !== undefined),
|
||||
rigor.invariant('ttl-expiry-on-comparator', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 200, seed: 'complex-graph-values-crucible', artifacts: { dir: '', persist: 'never' } });
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ describe('ComputedRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('possibility-passthrough', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('possibility-passthrough', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'computed-rule-possibility-passthrough', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -106,7 +106,7 @@ describe('ComputedRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('reason-default', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('reason-default', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'computed-rule-reason-default', effort: 500 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -143,7 +143,7 @@ describe('ComputedRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('reason-passthrough', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('reason-passthrough', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'computed-rule-reason-passthrough', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -186,7 +186,7 @@ describe('ComputedRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('meta-contract', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('meta-contract', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'computed-rule-meta-contract', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -235,7 +235,7 @@ describe('ComputedRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('collected-values-passthrough', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('collected-values-passthrough', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'computed-rule-collected-values', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -271,7 +271,7 @@ describe('ComputedRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('possibility-fallback-zero', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('possibility-fallback-zero', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'computed-rule-possibility-fallback', effort: 500 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -309,7 +309,7 @@ describe('ComputedRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('options-passthrough', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('options-passthrough', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'computed-rule-options-passthrough', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ describe('Config redefinition semantics (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('redefine-parity', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('redefine-parity', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 1200, seed: 'config-redefinition-parity' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -192,7 +192,7 @@ describe('Config redefinition semantics (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('redefine-binary-fastpath', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('redefine-binary-fastpath', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 800, seed: 'config-redefinition-binary' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ describe('DirectRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('no-relation-fallback', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('no-relation-fallback', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'direct-rule-no-relation', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -136,7 +136,7 @@ describe('DirectRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('relation-strength-preserved', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('relation-strength-preserved', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'direct-rule-strength', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -184,7 +184,7 @@ describe('DirectRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('reverse-routing', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('reverse-routing', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'direct-rule-reverse', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -234,7 +234,7 @@ describe('DirectRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('fastPath-early-exit', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('fastPath-early-exit', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'direct-rule-fast-path', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -276,7 +276,7 @@ describe('DirectRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('collectValues-disabled', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('collectValues-disabled', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'direct-rule-collect-off', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -330,7 +330,7 @@ describe('DirectRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('collectValues-default', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('collectValues-default', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'direct-rule-collect-on', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -391,7 +391,7 @@ describe('DirectRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('relation-precedence', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('relation-precedence', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'direct-rule-precedence', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -447,7 +447,7 @@ describe('DirectRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('result-shape-stable', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('result-shape-stable', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'direct-rule-shape', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ describe('DSLCompiler → engine rule mapping (rigor)', () => {
|
||||
// signature — the DSL validator rejects undeclared predicates.
|
||||
[rigor.fn('check', check, rigor.args(rigor.gen.oneOf(['owns', 'canReadInner'])))],
|
||||
rigor.crucible([
|
||||
rigor.invariant('direct-emission', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('direct-emission', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'dsl-compiler-direct', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -126,7 +126,7 @@ describe('DSLCompiler → engine rule mapping (rigor)', () => {
|
||||
const report = await rigor.campaign(
|
||||
[rigor.fn('check', check, rigor.args())],
|
||||
rigor.crucible([
|
||||
rigor.invariant('tus-emission', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('tus-emission', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'dsl-compiler-tus', effort: 200 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -158,7 +158,7 @@ describe('DSLCompiler → engine rule mapping (rigor)', () => {
|
||||
const report = await rigor.campaign(
|
||||
[rigor.fn('check', check, rigor.args())],
|
||||
rigor.crucible([
|
||||
rigor.invariant('parent-emission', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('parent-emission', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'dsl-compiler-parent', effort: 200 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -195,7 +195,7 @@ describe('DSLCompiler → engine rule mapping (rigor)', () => {
|
||||
const report = await rigor.campaign(
|
||||
[rigor.fn('check', check, rigor.args())],
|
||||
rigor.crucible([
|
||||
rigor.invariant('chain-emission', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('chain-emission', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'dsl-compiler-chain', effort: 200 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -244,7 +244,7 @@ describe('DSLCompiler → engine rule mapping (rigor)', () => {
|
||||
const report = await rigor.campaign(
|
||||
[rigor.fn('check', check, rigor.args())],
|
||||
rigor.crucible([
|
||||
rigor.invariant('multi_hop-emission', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('multi_hop-emission', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'dsl-compiler-multi-hop', effort: 200 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -286,7 +286,7 @@ describe('DSLCompiler → engine rule mapping (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('logical-emission', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('logical-emission', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'dsl-compiler-logical', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -321,7 +321,7 @@ describe('DSLCompiler → engine rule mapping (rigor)', () => {
|
||||
const report = await rigor.campaign(
|
||||
[rigor.fn('check', check, rigor.args(rigor.gen.enum(['>', '>=', '<', '<=', '==', '!='])))],
|
||||
rigor.crucible([
|
||||
rigor.invariant('relational-comparator-emission', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('relational-comparator-emission', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'dsl-compiler-relational-comparator', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -369,7 +369,7 @@ describe('DSLCompiler → engine rule mapping (rigor)', () => {
|
||||
const report = await rigor.campaign(
|
||||
[rigor.fn('check', check, rigor.args(rigor.gen.int(0, catalog.length - 1)))],
|
||||
rigor.crucible([
|
||||
rigor.invariant('mapping-consistency', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('mapping-consistency', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'dsl-compiler-mapping-consistency', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ describe('DSL-compiled vs hand-written parity under mutation (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('sequence-parity', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('sequence-parity', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 400, seed: 'dsl-mutation-parity' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -179,7 +179,7 @@ describe('DSL-compiled vs hand-written parity under mutation (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('grant-revoke-cycles', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('grant-revoke-cycles', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 300, seed: 'dsl-grant-revoke-cycles' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ function makeOracle() {
|
||||
const rel = opRel !== undefined ? opRel : r.rel;
|
||||
const directKey = key(srcId, rel, dstId);
|
||||
const stored = direct.get(directKey);
|
||||
if (!stored) return;
|
||||
if (!stored) return { skipped: true };
|
||||
direct.delete(directKey);
|
||||
const srcRel = key(stored.src, stored.rel);
|
||||
const dstRel = key(stored.dst, stored.rel);
|
||||
@@ -180,7 +180,7 @@ describe('GraphIndices indexes (rigor)', () => {
|
||||
const report = await rigor.campaign(
|
||||
[rigor.fn('check', check, rigor.args(opGen))],
|
||||
rigor.crucible([
|
||||
rigor.invariant('getDirectRelation-matches-oracle', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('getDirectRelation-matches-oracle', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 1500, seed: 'graph-indices-direct-a' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -243,7 +243,7 @@ describe('GraphIndices indexes (rigor)', () => {
|
||||
const report = await rigor.campaign(
|
||||
[rigor.fn('check', check, rigor.args(opGen))],
|
||||
rigor.crucible([
|
||||
rigor.invariant('getRelationsFromSrc-matches-oracle', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('getRelationsFromSrc-matches-oracle', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 1500, seed: 'graph-indices-direct-b' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -303,7 +303,7 @@ describe('GraphIndices indexes (rigor)', () => {
|
||||
const report = await rigor.campaign(
|
||||
[rigor.fn('check', check, rigor.args(opGen))],
|
||||
rigor.crucible([
|
||||
rigor.invariant('getRelationsToDst-matches-oracle', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('getRelationsToDst-matches-oracle', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 1500, seed: 'graph-indices-direct-c' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -363,7 +363,7 @@ describe('GraphIndices indexes (rigor)', () => {
|
||||
const report = await rigor.campaign(
|
||||
[rigor.fn('check', check, rigor.args(opGen))],
|
||||
rigor.crucible([
|
||||
rigor.invariant('getRelationsByName-matches-oracle', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('getRelationsByName-matches-oracle', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 1500, seed: 'graph-indices-direct-d' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -419,7 +419,7 @@ describe('GraphIndices indexes (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('addRelation-tuple-idempotent', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('addRelation-tuple-idempotent', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 800, seed: 'graph-indices-src' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -457,7 +457,7 @@ describe('GraphIndices indexes (rigor)', () => {
|
||||
const report = await rigor.campaign(
|
||||
[rigor.fn('check', check, rigor.args(relGen))],
|
||||
rigor.crucible([
|
||||
rigor.invariant('addRelation-idempotent', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('addRelation-idempotent', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 800, seed: 'graph-indices-dst' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -500,7 +500,7 @@ describe('GraphIndices indexes (rigor)', () => {
|
||||
const report = await rigor.campaign(
|
||||
[rigor.fn('check', check, rigor.args(relGen))],
|
||||
rigor.crucible([
|
||||
rigor.invariant('clear-empties-indexes', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('clear-empties-indexes', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 800, seed: 'graph-indices-name' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -547,7 +547,7 @@ describe('GraphIndices indexes (rigor)', () => {
|
||||
const report = await rigor.campaign(
|
||||
[rigor.fn('check', check, rigor.args(relGen))],
|
||||
rigor.crucible([
|
||||
rigor.invariant('add-remove-cycle', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('add-remove-cycle', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 800, seed: 'graph-indices-cycle' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ describe('LogicalOperators evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('union-max', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('union-max', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'logical-operators-union-max', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -102,7 +102,7 @@ describe('LogicalOperators evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('intersection-min', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('intersection-min', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'logical-operators-intersection-min', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -136,7 +136,7 @@ describe('LogicalOperators evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('union-mean', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('union-mean', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'logical-operators-union-mean', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -179,7 +179,7 @@ describe('LogicalOperators evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('exclusion', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('exclusion', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'logical-operators-exclusion', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -212,7 +212,7 @@ describe('LogicalOperators evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('possibility-bounded', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('possibility-bounded', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'logical-operators-possibility-bounded', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -249,7 +249,7 @@ describe('LogicalOperators evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('collected-values-concat', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('collected-values-concat', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'logical-operators-collected-values', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ describe('Manager vs index lookup parity (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('lookup-parity', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('lookup-parity', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 1200, seed: 'manager-index-parity' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ describe('MultiHopRule evaluation (rigor)', () => {
|
||||
rigor.args(rigor.gen.string(0, 20)) // may be empty
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('missing-relation', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('missing-relation', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'multi-hop-missing-relation', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -86,7 +86,7 @@ describe('MultiHopRule evaluation (rigor)', () => {
|
||||
rigor.args(rigor.gen.float({ min: 0, max: 1 }))
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('possibility-bounded', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('possibility-bounded', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'multi-hop-possibility-bounded', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -124,7 +124,7 @@ describe('MultiHopRule evaluation (rigor)', () => {
|
||||
rigor.args(rigor.gen.float({ min: 0.01, max: 1 }))
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('single-path-strength', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('single-path-strength', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'multi-hop-single-path', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -159,7 +159,7 @@ describe('MultiHopRule evaluation (rigor)', () => {
|
||||
const report = await rigor.campaign(
|
||||
[rigor.fn('check', check, rigor.args())],
|
||||
rigor.crucible([
|
||||
rigor.invariant('no-path', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('no-path', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'multi-hop-no-path', effort: 500 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -206,7 +206,7 @@ describe('MultiHopRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('multi-hop-finds-path', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('multi-hop-finds-path', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'multi-hop-two-hop', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ describe('Multi-object independence (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('multi-object-isolation', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('multi-object-isolation', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 1000, seed: 'multi-object-independence' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -199,7 +199,7 @@ describe('Node lifecycle semantics (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('remove-cascade', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('remove-cascade', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 1000, seed: 'node-lifecycle-remove' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -252,7 +252,7 @@ describe('Node lifecycle semantics (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('node-readd', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('node-readd', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 600, seed: 'node-lifecycle-readd' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ describe('NodeManager index invariants (rigor)', () => {
|
||||
)
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('inverse-maps', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('inverse-maps', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'node-manager-inverse-maps', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -131,7 +131,7 @@ describe('NodeManager index invariants (rigor)', () => {
|
||||
)
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('addNode-idempotent', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('addNode-idempotent', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'node-manager-add-idempotent', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -184,7 +184,7 @@ describe('NodeManager index invariants (rigor)', () => {
|
||||
)
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('size-invariant', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('size-invariant', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'node-manager-size-invariant', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -198,7 +198,7 @@ describe('NodeManager index invariants (rigor)', () => {
|
||||
it('nextNodeId advances monotonically across distinct addNode calls', async () => {
|
||||
async function check(keys, types) {
|
||||
const { manager, arbiter } = makeManager();
|
||||
if (keys.length !== types.length) return; // skip ill-formed
|
||||
if (keys.length !== types.length) return { skipped: true }; // skip ill-formed
|
||||
const ids = [];
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
ids.push(manager.addNode(keys[i], types[i]));
|
||||
@@ -247,7 +247,7 @@ describe('NodeManager index invariants (rigor)', () => {
|
||||
)
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('monotonic-ids', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('monotonic-ids', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'node-manager-monotonic-ids', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -293,7 +293,7 @@ describe('NodeManager index invariants (rigor)', () => {
|
||||
)
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('removeNode-cleanup', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('removeNode-cleanup', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'node-manager-remove-cleanup', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -333,7 +333,7 @@ describe('NodeManager index invariants (rigor)', () => {
|
||||
)
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('clearNodes-resets', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('clearNodes-resets', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'node-manager-clear-resets', effort: 500 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -386,7 +386,7 @@ describe('NodeManager index invariants (rigor)', () => {
|
||||
)
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('updateNodeData-merges', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('updateNodeData-merges', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'node-manager-update-merge', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ describe('Partial graph overlay precedence (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('persistent-precedence', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('persistent-precedence', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 500, seed: 'overlay-persistent-precedence' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -136,7 +136,7 @@ describe('Partial graph overlay precedence (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('layer-precedence', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('layer-precedence', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 400, seed: 'overlay-layer-precedence' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ describe('ParentRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('no-parents', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('no-parents', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'parent-rule-no-parents', effort: 500 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -129,7 +129,7 @@ describe('ParentRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('one-parent-strength', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('one-parent-strength', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'parent-rule-one-parent', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -173,7 +173,7 @@ describe('ParentRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('threshold-cutoff', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('threshold-cutoff', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'parent-rule-threshold', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -210,7 +210,7 @@ describe('ParentRule evaluation (rigor)', () => {
|
||||
const report = await rigor.campaign(
|
||||
[rigor.fn('check', check, rigor.args())],
|
||||
rigor.crucible([
|
||||
rigor.invariant('cycle-detection', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('cycle-detection', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'parent-rule-cycle', effort: 200 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -256,7 +256,7 @@ describe('ParentRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('multi-parent-max', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('multi-parent-max', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'parent-rule-multi-parent', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -295,7 +295,7 @@ describe('ParentRule evaluation (rigor)', () => {
|
||||
const report = await rigor.campaign(
|
||||
[rigor.fn('check', check, rigor.args(rigor.gen.boolean()))],
|
||||
rigor.crucible([
|
||||
rigor.invariant('parent-relation-default', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('parent-relation-default', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'parent-rule-default-relation', effort: 200 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -339,7 +339,7 @@ describe('ParentRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('possibility-bounded', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('possibility-bounded', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'parent-rule-possibility-bounded', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ describe('PLTC reachability parity (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('pltc-active-bypass-parity', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('pltc-active-bypass-parity', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 1500, seed: 'pltc-parity-active-bypass' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -162,7 +162,7 @@ describe('PLTC reachability parity (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('pltc-fastfail-soundness', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('pltc-fastfail-soundness', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 1000, seed: 'pltc-parity-fastfail' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -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' }});
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ describe('RelationManager.addRelation/removeRelation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('add-and-get', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('add-and-get', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'relation-manager-add-get', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -125,7 +125,7 @@ describe('RelationManager.addRelation/removeRelation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('addRelation-idempotent', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('addRelation-idempotent', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'relation-manager-add-idempotent', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -193,7 +193,7 @@ describe('RelationManager.addRelation/removeRelation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('remove-clears-indexes', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('remove-clears-indexes', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'relation-manager-remove-cleanup', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -282,7 +282,7 @@ describe('RelationManager.addRelation/removeRelation (rigor)', () => {
|
||||
const report = await rigor.campaign(
|
||||
[rigor.fn('check', check, rigor.args(relGen))],
|
||||
rigor.crucible([
|
||||
rigor.invariant('index-coherence', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('index-coherence', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'relation-manager-index-coherence', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -362,7 +362,7 @@ describe('RelationManager.addRelation/removeRelation (rigor)', () => {
|
||||
const report = await rigor.campaign(
|
||||
[rigor.fn('check', check, rigor.args(opGen))],
|
||||
rigor.crucible([
|
||||
rigor.invariant('add-remove-roundtrip', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('add-remove-roundtrip', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'relation-manager-add-remove-roundtrip', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -394,7 +394,7 @@ describe('RelationManager.addRelation/removeRelation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('getDirectRelation-unknown', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('getDirectRelation-unknown', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'relation-manager-unknown-tuple', effort: 500 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ describe('RelationalComparatorRouter._isQualitativeRule (rigor)', () => {
|
||||
)
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('qualitative-wins', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('qualitative-wins', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'rc-router-qualitative-wins', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -114,7 +114,7 @@ describe('RelationalComparatorRouter._isQualitativeRule (rigor)', () => {
|
||||
)
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('scaleName-triggers', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('scaleName-triggers', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'rc-router-scale-name', effort: 500 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -154,7 +154,7 @@ describe('RelationalComparatorRouter._isQualitativeRule (rigor)', () => {
|
||||
)
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('decay-blur-triggers', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('decay-blur-triggers', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'rc-router-decay-blur', effort: 500 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -188,7 +188,7 @@ describe('RelationalComparatorRouter._isQualitativeRule (rigor)', () => {
|
||||
)
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('marginSteps-correct', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('marginSteps-correct', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'rc-router-margin-steps', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -229,7 +229,7 @@ describe('RelationalComparatorRouter._isQualitativeRule (rigor)', () => {
|
||||
)
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('plain-numeric', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('plain-numeric', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'rc-router-plain-numeric', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -274,7 +274,7 @@ describe('RelationalComparatorRouter._isQualitativeRule (rigor)', () => {
|
||||
)
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('getImplType-consistent', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('getImplType-consistent', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'rc-router-get-impl-type', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -320,7 +320,7 @@ describe('RelationalComparatorRouter._isQualitativeRule (rigor)', () => {
|
||||
)
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('hasValidProperty', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('hasValidProperty', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'rc-router-has-valid-property', effort: 1000 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ describe('RelationalComparatorRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('left-gt-right', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('left-gt-right', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'rc-rule-left-gt-right', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -109,7 +109,7 @@ describe('RelationalComparatorRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('left-lt-right', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('left-lt-right', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'rc-rule-left-lt-right', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -144,7 +144,7 @@ describe('RelationalComparatorRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('possibility-bounded', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('possibility-bounded', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'rc-rule-possibility-bounded', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -179,7 +179,7 @@ describe('RelationalComparatorRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('result-shape-stable', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('result-shape-stable', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'rc-rule-shape-stable', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -218,7 +218,7 @@ describe('RelationalComparatorRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('determinism', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('determinism', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'rc-rule-determinism', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ describe('Condensed snapshot round trip (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('snapshot-parity', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('snapshot-parity', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 400, seed: 'snapshot-parity' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -175,7 +175,7 @@ describe('Condensed snapshot round trip (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('readonly-enforced', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('readonly-enforced', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 300, seed: 'snapshot-readonly' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@ describe('Traversal semantics parity (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('chain-direction-parity', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('chain-direction-parity', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 1500, seed: 'traversal-chain-direction' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -262,7 +262,7 @@ describe('Traversal semantics parity (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('ttu-multi-tuple-parity', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('ttu-multi-tuple-parity', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 1200, seed: 'traversal-ttu-multituple' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -338,7 +338,7 @@ describe('Traversal semantics parity (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('update-path-parity', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('update-path-parity', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 600, seed: 'traversal-update-path' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ describe('TupleToUsersetRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('no-tuples', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('no-tuples', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 500, seed: 'ttu-no-tuples', artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -139,7 +139,7 @@ describe('TupleToUsersetRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('min-fusion', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('min-fusion', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 800, seed: 'ttu-min-fusion', artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -192,7 +192,7 @@ describe('TupleToUsersetRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('multi-tuple-max', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('multi-tuple-max', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'ttu-multi-tuple-max', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -230,7 +230,7 @@ describe('TupleToUsersetRule evaluation (rigor)', () => {
|
||||
)
|
||||
)],
|
||||
rigor.crucible([
|
||||
rigor.invariant('possibility-bounded', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('possibility-bounded', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'ttu-possibility-bounded', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -290,7 +290,7 @@ describe('TupleToUsersetRule evaluation (rigor)', () => {
|
||||
const report = await rigor.campaign(
|
||||
[rigor.fn('check', check, rigor.args())],
|
||||
rigor.crucible([
|
||||
rigor.invariant('early-exit', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('early-exit', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'ttu-early-exit', effort: 200 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -340,7 +340,7 @@ describe('TupleToUsersetRule evaluation (rigor)', () => {
|
||||
const report = await rigor.campaign(
|
||||
[rigor.fn('check', check, rigor.args())],
|
||||
rigor.crucible([
|
||||
rigor.invariant('cycle-detection', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('cycle-detection', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ seed: 'ttu-cycle-detection', effort: 200 , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ describe('Possibilistic validity metadata (rigor)', () => {
|
||||
};
|
||||
}, rigor.args(rigor.gen.array(rigor.gen.oneOf(['finite_sample', 'anytime', 'conformal', 'approximate', 'heuristic', 'unknown']), 1, 4)))],
|
||||
rigor.crucible([
|
||||
rigor.invariant('helper invariants', ({ error, errorMessage, actual }) => !error && !errorMessage && Object.values(actual).every(Boolean))
|
||||
rigor.invariant('helper invariants', ({ actual }) => !!actual && Object.values(actual).every(Boolean))
|
||||
])
|
||||
).run({ effort: 200, seed: 'validity-helpers-2026', artifacts: { dir: '', persist: 'never' } });
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ describe('Authorization state consistency (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('ttu-mutation', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('ttu-mutation', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 400, seed: 'consistency-ttu-mutation' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -152,7 +152,7 @@ describe('Authorization state consistency (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('chain-mutation', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('chain-mutation', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 400, seed: 'consistency-chain-mutation' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -220,7 +220,7 @@ describe('Authorization state consistency (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('config-change', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('config-change', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 400, seed: 'consistency-config-change' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -272,7 +272,7 @@ describe('Authorization state consistency (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('threshold-excludes', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('threshold-excludes', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 400, seed: 'consistency-threshold' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -338,7 +338,7 @@ describe('Authorization state consistency (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('values-complete', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('values-complete', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 400, seed: 'consistency-value-completeness' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -375,7 +375,7 @@ describe('Authorization state consistency (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('deterministic', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('deterministic', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 300, seed: 'consistency-determinism' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ describe('Defeasible logic, DSL parity, aggregation (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('defeasible-semantics', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('defeasible-semantics', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 600, seed: 'defeasible-semantics' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -172,7 +172,7 @@ describe('Defeasible logic, DSL parity, aggregation (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('dsl-parity', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('dsl-parity', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 400, seed: 'dsl-runtime-parity' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -263,7 +263,7 @@ describe('Defeasible logic, DSL parity, aggregation (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('aggregation-complete', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('aggregation-complete', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 500, seed: 'aggregation-completeness' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -310,7 +310,7 @@ describe('Zanzibar rewrite-rule semantics (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('union-max', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('union-max', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 400, seed: 'zanzibar-union' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -354,7 +354,7 @@ describe('Zanzibar rewrite-rule semantics (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('intersection-min', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('intersection-min', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 400, seed: 'zanzibar-intersection' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -397,7 +397,7 @@ describe('Zanzibar rewrite-rule semantics (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('exclusion-blocks', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('exclusion-blocks', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 400, seed: 'zanzibar-exclusion' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -432,7 +432,7 @@ describe('Zanzibar rewrite-rule semantics (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('expand-parity', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('expand-parity', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 800, seed: 'zanzibar-expand-parity' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -488,7 +488,7 @@ describe('Zanzibar rewrite-rule semantics (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('nested-weakest-link', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('nested-weakest-link', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 500, seed: 'zanzibar-nested-chain' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
@@ -544,7 +544,7 @@ describe('Zanzibar rewrite-rule semantics (rigor)', () => {
|
||||
))
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('cycle-safe', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
rigor.invariant('cycle-safe', ({ actual }) => actual !== undefined)
|
||||
])
|
||||
).run({ effort: 400, seed: 'zanzibar-cycle-safety' , artifacts: { dir: '', persist: 'never' }});
|
||||
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* tests/rules/chain-condition-step.test.js — ChainRule CONDITION STEP.
|
||||
*
|
||||
* A chain step of the form { rule: <config>, conditionStep: true } is a
|
||||
* condition-gated hop instead of an edge traversal. It is valid only as the
|
||||
* FINAL step: the object is known, so the engine verifies the referenced rule
|
||||
* at (intermediate, object) for each current path. The DSL compiler emits
|
||||
* these when a chain's object-side hop references a defeasible/logical
|
||||
* evidence (e.g. `member_of(user,*g){ gated(g,doc) }` where gated is
|
||||
* `WHEN can_view(group, doc) UNLESS banned(group)`).
|
||||
*/
|
||||
import { describe, it, beforeEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { Arbiter } from '../../src/index.js';
|
||||
import { RuleEvaluator } from '../../src/authorization/RuleEvaluator.js';
|
||||
import { ChainRule } from '../../src/authorization/rules/ChainRule.js';
|
||||
|
||||
let arbiter, evaluator, chainRule;
|
||||
|
||||
function evalRule(userKey, objectKey, rule, options = {}) {
|
||||
const userId = arbiter.resolveNodeId(userKey);
|
||||
const objectId = arbiter.resolveNodeId(objectKey);
|
||||
return chainRule._evaluateRule(userId, userKey, objectId, objectKey, rule, new Set(), null, {
|
||||
includeMeta: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
const CONDITION_CONFIG = {
|
||||
type: 'logical',
|
||||
when: { intersection: { rules: [{ type: 'direct', relation: 'can_view' }], aggregator: 'min' } },
|
||||
// banned(group) is unary → subject-as-object (self-edge), as the DSL emits
|
||||
unless: { union: { rules: [{ type: 'direct', relation: 'banned', _subjectAsObject: true }], aggregator: 'max' } }
|
||||
};
|
||||
|
||||
describe('ChainRule condition step (rule-based final hop)', () => {
|
||||
beforeEach(() => {
|
||||
arbiter = new Arbiter();
|
||||
evaluator = new RuleEvaluator(arbiter);
|
||||
chainRule = new ChainRule(arbiter, evaluator);
|
||||
arbiter.addNode('user:u', 'user');
|
||||
arbiter.addNode('group:g', 'group');
|
||||
arbiter.addNode('doc:d', 'doc');
|
||||
arbiter.setRelationConfig('can_view', { type: 'direct' });
|
||||
arbiter.setRelationConfig('banned', { type: 'direct' });
|
||||
});
|
||||
|
||||
it('grants when the condition holds at the object', () => {
|
||||
arbiter.addRelation('user:u', 'member_of', 'group:g', { possibility: 1.0 });
|
||||
arbiter.addRelation('group:g', 'can_view', 'doc:d', { possibility: 0.7 });
|
||||
const rule = {
|
||||
type: 'chain',
|
||||
steps: ['member_of', { rule: CONDITION_CONFIG, conditionStep: true }]
|
||||
};
|
||||
const res = evalRule('user:u', 'doc:d', rule);
|
||||
// min(member_of, can_view*(1 - banned)) = min(1.0, 0.7) = 0.7
|
||||
assert.ok(Math.abs(res.possibility - 0.7) < 1e-9, `expected 0.7, got ${res.possibility} (${res.reason})`);
|
||||
});
|
||||
|
||||
it('denies when the condition is defeated at the object', () => {
|
||||
arbiter.addRelation('user:u', 'member_of', 'group:g', { possibility: 1.0 });
|
||||
arbiter.addRelation('group:g', 'can_view', 'doc:d', { possibility: 0.7 });
|
||||
arbiter.addRelation('group:g', 'banned', 'group:g', { possibility: 1.0 });
|
||||
const rule = {
|
||||
type: 'chain',
|
||||
steps: ['member_of', { rule: CONDITION_CONFIG, conditionStep: true }]
|
||||
};
|
||||
const res = evalRule('user:u', 'doc:d', rule);
|
||||
// min(1.0, 0.7*(1 - 1.0)) = 0
|
||||
assert.equal(res.possibility, 0);
|
||||
});
|
||||
|
||||
it('denies when an earlier edge is missing', () => {
|
||||
arbiter.addRelation('group:g', 'can_view', 'doc:d', { possibility: 0.7 });
|
||||
const rule = {
|
||||
type: 'chain',
|
||||
steps: ['member_of', { rule: CONDITION_CONFIG, conditionStep: true }]
|
||||
};
|
||||
const res = evalRule('user:u', 'doc:d', rule);
|
||||
assert.equal(res.possibility, 0);
|
||||
assert.equal(res.reason, 'no_chain_path_found');
|
||||
});
|
||||
|
||||
it('expands an intermediate condition step (rule-based reachability)', () => {
|
||||
arbiter.setRelationConfig('member_of', { type: 'direct' });
|
||||
arbiter.addNode('group:g', 'group');
|
||||
arbiter.addRelation('user:u', 'member_of', 'group:g', { possibility: 1.0 });
|
||||
arbiter.addRelation('group:g', 'can_view', 'doc:d', { possibility: 0.8 });
|
||||
// [condition(member_of unless banned), can_view] — the condition step is
|
||||
// INTERMEDIATE and discovers its reachable nodes (its base relation's
|
||||
// neighbors from the source, filtered by its defeater).
|
||||
const intermediateConfig = {
|
||||
type: 'logical',
|
||||
when: { intersection: { rules: [{ type: 'direct', relation: 'member_of' }], aggregator: 'min' } },
|
||||
unless: { union: { rules: [{ type: 'direct', relation: 'banned', _subjectIsObject: true }], aggregator: 'max' } }
|
||||
};
|
||||
const rule = {
|
||||
type: 'chain',
|
||||
steps: [{ rule: intermediateConfig, conditionStep: true }, 'can_view']
|
||||
};
|
||||
// g not banned → reachable via condition → can_view → doc
|
||||
assert.ok(Math.abs(evalRule('user:u', 'doc:d', rule).possibility - 0.8) < 1e-9);
|
||||
// banning g filters it out of the intermediate expansion → no path
|
||||
arbiter.addRelation('group:g', 'banned', 'group:g', { possibility: 1.0 });
|
||||
const denied = evalRule('user:u', 'doc:d', rule);
|
||||
assert.equal(denied.possibility, 0);
|
||||
});
|
||||
|
||||
it('expands a tuple_to_userset evidence as an intermediate condition step', () => {
|
||||
arbiter.setRelationConfig('member_of', { type: 'direct' });
|
||||
arbiter.setRelationConfig('owner', { type: 'direct' });
|
||||
arbiter.setRelationConfig('doc_route', { type: 'direct' });
|
||||
arbiter.addNode('group:g', 'group');
|
||||
arbiter.addNode('doc:1', 'doc');
|
||||
arbiter.addNode('doc:2', 'doc');
|
||||
arbiter.addRelation('user:u', 'member_of', 'group:g', { possibility: 1.0 });
|
||||
arbiter.addRelation('group:g', 'owner', 'doc:1', { possibility: 0.8 });
|
||||
arbiter.addRelation('doc:1', 'doc_route', 'doc:2', { possibility: 0.9 });
|
||||
const ttuConfig = {
|
||||
type: 'tuple_to_userset',
|
||||
tuplesetRelation: 'owner',
|
||||
computedRelation: 'member_of',
|
||||
tuplesetDirection: 'in'
|
||||
};
|
||||
const rule = {
|
||||
type: 'chain',
|
||||
steps: [{ rule: ttuConfig, conditionStep: true }, 'doc_route']
|
||||
};
|
||||
// TTU reachable from user = doc:1 (min(1.0, 0.8) = 0.8), then doc_route → 0.9
|
||||
const res = evalRule('user:u', 'doc:2', rule);
|
||||
assert.ok(Math.abs(res.possibility - 0.8) < 1e-9, `expected 0.8, got ${res.possibility} (${res.reason})`);
|
||||
// Direct expansion unit check
|
||||
const expanded = chainRule._expandRuleFromSrc(arbiter.resolveNodeId('user:u'), ttuConfig, {});
|
||||
assert.equal(expanded.size, 1);
|
||||
assert.equal(arbiter.resolveKey([...expanded.keys()][0]), 'doc:1');
|
||||
});
|
||||
|
||||
it('combines across multiple parallel intermediates (max aggregation)', () => {
|
||||
arbiter.addNode('group:g2', 'group');
|
||||
arbiter.addRelation('user:u', 'member_of', 'group:g', { possibility: 0.5 });
|
||||
arbiter.addRelation('group:g', 'can_view', 'doc:d', { possibility: 0.7 });
|
||||
arbiter.addRelation('user:u', 'member_of', 'group:g2', { possibility: 1.0 });
|
||||
arbiter.addRelation('group:g2', 'can_view', 'doc:d', { possibility: 0.8 });
|
||||
const rule = {
|
||||
type: 'chain',
|
||||
steps: ['member_of', { rule: CONDITION_CONFIG, conditionStep: true }]
|
||||
};
|
||||
const res = evalRule('user:u', 'doc:d', rule);
|
||||
// paths: min(0.5,0.7)=0.5 and min(1.0,0.8)=0.8 -> max = 0.8
|
||||
assert.ok(Math.abs(res.possibility - 0.8) < 1e-9, `expected 0.8, got ${res.possibility} (${res.reason})`);
|
||||
});
|
||||
});
|
||||
@@ -127,18 +127,25 @@ describe('invalidateByNodeKey', () => {
|
||||
const bobId = arbiter.resolveNodeId('user:bob');
|
||||
const carolId = arbiter.resolveNodeId('user:carol');
|
||||
|
||||
// Real cache-key format: `${srcId}|${rel}|${dstId}` (no prefix).
|
||||
cache.setDirect(`${aliceId}|member_of|${bobId}`, { reason: 'a-b' });
|
||||
cache.setDirect(`${bobId}|member_of|${aliceId}`, { reason: 'b-a' });
|
||||
cache.setDirect(`${carolId}|member_of|${bobId}`, { reason: 'c-b' });
|
||||
// Keys are rolling hashes; node-level invalidation uses the per-node
|
||||
// key index, so every injected key must be tracked.
|
||||
const keyAB = arbiter.keyManager.createCompositeKey(aliceId, 'member_of', bobId);
|
||||
const keyBA = arbiter.keyManager.createCompositeKey(bobId, 'member_of', aliceId);
|
||||
const keyCB = arbiter.keyManager.createCompositeKey(carolId, 'member_of', bobId);
|
||||
cache.setDirect(keyAB, { reason: 'a-b' });
|
||||
arbiter._trackDirectCheckKey('member_of', aliceId, bobId, keyAB);
|
||||
cache.setDirect(keyBA, { reason: 'b-a' });
|
||||
arbiter._trackDirectCheckKey('member_of', bobId, aliceId, keyBA);
|
||||
cache.setDirect(keyCB, { reason: 'c-b' });
|
||||
arbiter._trackDirectCheckKey('member_of', carolId, bobId, keyCB);
|
||||
cache.setDirect('unrelated-key', { reason: 'u' });
|
||||
|
||||
cache.invalidateByNodeKey('user:alice');
|
||||
|
||||
// Both alice-involving entries cleared; the others remain.
|
||||
assert.equal(cache.getDirect(`${aliceId}|member_of|${bobId}`), undefined);
|
||||
assert.equal(cache.getDirect(`${bobId}|member_of|${aliceId}`), undefined);
|
||||
assert.ok(cache.getDirect(`${carolId}|member_of|${bobId}`));
|
||||
assert.equal(cache.getDirect(keyAB), undefined);
|
||||
assert.equal(cache.getDirect(keyBA), undefined);
|
||||
assert.ok(cache.getDirect(keyCB));
|
||||
assert.ok(cache.getDirect('unrelated-key'));
|
||||
});
|
||||
});
|
||||
@@ -187,19 +194,26 @@ describe('invalidateByNodeKey', () => {
|
||||
arbiter.addNode('user:bob', 'user');
|
||||
const aliceId = arbiter.resolveNodeId('user:alice');
|
||||
const bobId = arbiter.resolveNodeId('user:bob');
|
||||
// Pre-populate cache with a key that contains alice's id
|
||||
arbiter.decisionCache.setDirect(`${aliceId}|member_of|${bobId}`, { reason: 'stale' });
|
||||
arbiter.decisionCache.setDirect(`${bobId}|member_of|${aliceId}`, { reason: 'stale' });
|
||||
arbiter.decisionCache.setDirect(`${aliceId}|other|${bobId}`, { reason: 'stale' });
|
||||
// Pre-populate cache with keys touching alice (tracked so node-level
|
||||
// invalidation can find them)
|
||||
const k1 = arbiter.keyManager.createCompositeKey(aliceId, 'member_of', bobId);
|
||||
const k2 = arbiter.keyManager.createCompositeKey(bobId, 'member_of', aliceId);
|
||||
const k3 = arbiter.keyManager.createCompositeKey(aliceId, 'other', bobId);
|
||||
arbiter.decisionCache.setDirect(k1, { reason: 'stale' });
|
||||
arbiter._trackDirectCheckKey('member_of', aliceId, bobId, k1);
|
||||
arbiter.decisionCache.setDirect(k2, { reason: 'stale' });
|
||||
arbiter._trackDirectCheckKey('member_of', bobId, aliceId, k2);
|
||||
arbiter.decisionCache.setDirect(k3, { reason: 'stale' });
|
||||
arbiter._trackDirectCheckKey('other', aliceId, bobId, k3);
|
||||
arbiter.decisionCache.setDirect('unrelated', { reason: 'keep' });
|
||||
|
||||
// Trigger invalidation through the NodeManager path
|
||||
arbiter.nodeManager.updateNodeData('user:alice', { foo: 'bar' });
|
||||
|
||||
// All alice-bearing keys should be gone; unrelated key remains.
|
||||
assert.equal(arbiter.decisionCache.getDirect(`${aliceId}|member_of|${bobId}`), undefined);
|
||||
assert.equal(arbiter.decisionCache.getDirect(`${bobId}|member_of|${aliceId}`), undefined);
|
||||
assert.equal(arbiter.decisionCache.getDirect(`${aliceId}|other|${bobId}`), undefined);
|
||||
assert.equal(arbiter.decisionCache.getDirect(k1), undefined);
|
||||
assert.equal(arbiter.decisionCache.getDirect(k2), undefined);
|
||||
assert.equal(arbiter.decisionCache.getDirect(k3), undefined);
|
||||
assert.ok(arbiter.decisionCache.getDirect('unrelated'));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user