1.13.0: measure + evidence compose; value-graph now a registry dep
CI / test (push) Successful in 24s
CI / publish (push) Failing after 10s

- DSLValueGraph integrates the evidence DSL with @arbiter/value-graph: measures
  become typed value-graph nodes; attach() wires runtime.measure through the graph.
- DSLRuntime.check() now retrieves required MEASURE values and injects them as
  value-carrying self-edges, so an evidence comparator over a measure (e.g.
  budget_used(user) <= budget_limit(user)) evaluates — measure and evidence
  compose (provider-sourced AND value-graph-sourced), with 3 new composition tests.
- BUILTIN_TYPES: bigint → buffer (the value-graph wire is JSON-free/bigint-free).
- @arbiter/value-graph: file:../value-graph → ^0.1.0 (registry); CI auth adds
  @push-stream-std registry for the transitive dep.
- rigor core ^3.1.2 / probe ^0.0.8.
This commit is contained in:
2026-08-04 17:51:13 -07:00
parent b145c979ab
commit 7898a7990a
14 changed files with 1421 additions and 87 deletions
@@ -0,0 +1,338 @@
/**
* tests/rigor/dsl-value-graph-robustness.test.js — js-rigor invariants for the
* DSLValueGraph wrapper's robustness: attach() semantics, resolver failure modes,
* stored-value lifecycle (overwrite / invalidation isolation / TTL), binding
* edge cases, strict-mode behavior, and concurrent retrieval.
*/
import { describe, it } from 'node:test';
import { rigor, reducers } from '@rigor/core';
import { Arbiter } from '@arbiter/core';
import { ValueGraph } from '@arbiter/value-graph';
import { DSLRuntime, DSLValueGraph } from '../../src/index.js';
const DSL = `
definition Tenant { id: string }
measure budget(tenant: Tenant, feature: string) { } PROVIDES number
measure label(user: string) { } PROVIDES string
`;
function makeDvg() {
const rt = new DSLRuntime(new Arbiter()).compile(DSL, 'vg-robust');
return new DSLValueGraph(rt);
}
async function expectPass(name, actions, checks, config = {}) {
const report = await rigor.campaign(actions, rigor.crucible(checks))
.run({ seed: `dsl-vg-robust-${name}`, effort: 100, artifacts: { dir: '', persist: 'never' }, ...config });
if (report.status !== 'passed') {
const detail = (report.failures || []).slice(0, 5)
.map((f) => JSON.stringify({ action: f.actionName || f.action, inv: f.name, args: f.args, msg: f.message }));
throw new Error(`campaign '${name}' failed.\n${detail.join('\n')}`);
}
return report;
}
describe('DSLValueGraph robustness (rigor)', () => {
it('attach() is idempotent and a manual registerMeasure overrides it', async () => {
const actions = [
rigor.fn('attach_idempotent', (value, cb) => {
const rt = new DSLRuntime(new Arbiter()).compile(DSL, 'r');
const dvg = new DSLValueGraph(rt);
dvg.attach();
dvg.attach(); // idempotent
dvg.setValue('budget', { __subject: 't', tenant: 't', feature: 'x' }, value);
rt.measure('budget', { __subject: 't', tenant: 't', feature: 'x' }).then(
(r) => cb(null, r.value), (e) => cb(e)
);
}, rigor.args(rigor.gen.int(0, 100), rigor.handler(reducers.first()))),
rigor.fn('attach_override', (cb) => {
const rt = new DSLRuntime(new Arbiter()).compile(DSL, 'r');
const dvg = new DSLValueGraph(rt);
dvg.attach();
rt.registerMeasure('budget', async () => ({ value: 12345, unit: 'manual' }));
rt.measure('budget', { __subject: 't', tenant: 't', feature: 'x' }).then(
(r) => cb(null, r.value), (e) => cb(e)
);
}, rigor.args(rigor.handler(reducers.first()))),
];
const checks = [
rigor.after('attach_idempotent', (ctx) => ctx.error == null && ctx.actual === ctx.args[0]),
rigor.after('attach_override', (ctx) => ctx.error == null && ctx.actual === 12345),
];
await expectPass('attach', actions, checks);
});
it('resolver failure modes: sync-return, error propagation, wrong-typed result', async () => {
const actions = [
rigor.fn('resolver_sync', (value, cb) => {
const dvg = makeDvg();
dvg.resolve('budget', (s, p) => value * 2); // sync return
dvg.measure('budget', { __subject: 't', tenant: 't', feature: 'x' }).then(
(r) => cb(null, r.value), (e) => cb(e)
);
}, rigor.args(rigor.gen.int(0, 50), rigor.handler(reducers.first()))),
rigor.fn('resolver_error', (cb) => {
const dvg = makeDvg();
dvg.resolve('budget', (s, p, ctx, done) => { done(new Error('provider down')); });
dvg.measure('budget', { __subject: 't', tenant: 't', feature: 'x' }).then(
(r) => cb(null, { ok: true, v: r.value }),
(e) => cb(null, { ok: false, msg: e.message })
);
}, rigor.args(rigor.handler(reducers.first()))),
rigor.fn('resolver_wrong_type', (cb) => {
const dvg = makeDvg();
dvg.resolve('budget', (s, p, ctx, done) => { done(null, 'not-a-number'); });
dvg.measure('budget', { __subject: 't', tenant: 't', feature: 'x' }).then(
(r) => cb(null, { ok: true, v: r.value }),
(e) => cb(null, { ok: false, msg: e.message })
);
}, rigor.args(rigor.handler(reducers.first()))),
];
const checks = [
rigor.after('resolver_sync', (ctx) => ctx.error == null && ctx.actual === ctx.args[0] * 2),
rigor.after('resolver_error', (ctx) => ctx.actual.ok === false && /provider down/.test(ctx.actual.msg)),
rigor.after('resolver_wrong_type', (ctx) => ctx.actual.ok === false && /must match declared type 'number'/.test(ctx.actual.msg)),
];
await expectPass('resolvers', actions, checks);
});
it('stored-value lifecycle: overwrite wins, cross-measure invalidation is isolated, TTL expires', async () => {
const actions = [
rigor.fn('overwrite', (a, b) => {
const dvg = makeDvg();
dvg.setValue('budget', { __subject: 't', tenant: 't', feature: 'x' }, a);
dvg.setValue('budget', { __subject: 't', tenant: 't', feature: 'x' }, b); // last-write-wins
return dvg.getValue('budget', { __subject: 't', tenant: 't', feature: 'x' }).value;
}, rigor.args(rigor.gen.int(0, 100), rigor.gen.int(0, 100))),
rigor.fn('cross_measure_invalidate', (a, b) => {
const dvg = makeDvg();
dvg.setValue('budget', { __subject: 't', tenant: 't', feature: 'x' }, a);
dvg.setValue('label', { __subject: 't', user: 'u' }, String(b));
dvg.vg.invalidate('t', 'budget', { tenant: 't', feature: 'x' });
const budget = dvg.getValue('budget', { __subject: 't', tenant: 't', feature: 'x' });
const label = dvg.getValue('label', { __subject: 't', user: 'u' });
return { budget: budget === null ? 'null' : budget.value, label: label.value };
}, rigor.args(rigor.gen.int(0, 100), rigor.gen.int(0, 100))),
rigor.fn('ttl_expiry', (value, cb) => {
let t = 0;
const rt = new DSLRuntime(new Arbiter()).compile(DSL, 'r');
const dvg = new DSLValueGraph(rt, {
valueGraph: new ValueGraph({ clock: () => t, defaultTTL: 100 })
});
dvg.setValue('budget', { __subject: 't', tenant: 't', feature: 'x' }, value);
const before = dvg.getValue('budget', { __subject: 't', tenant: 't', feature: 'x' }).value;
t = 200;
const after = dvg.getValue('budget', { __subject: 't', tenant: 't', feature: 'x' });
cb(null, { before, after: after === null ? 'null' : after.value });
}, rigor.args(rigor.gen.int(0, 100), rigor.handler(reducers.first()))),
];
const checks = [
rigor.after('overwrite', (ctx) => ctx.actual === ctx.args[1]),
rigor.after('cross_measure_invalidate', (ctx) => ctx.actual.budget === 'null' && ctx.actual.label === String(ctx.args[1])),
rigor.after('ttl_expiry', (ctx) => ctx.actual.before === ctx.args[0] && ctx.actual.after === 'null'),
];
await expectPass('lifecycle', actions, checks);
});
it('binding edge cases: array params, extra keys preserved, positional arity, unit/source', async () => {
const actions = [
rigor.fn('array_param', (arr, cb) => {
const rt = new DSLRuntime(new Arbiter()).compile(`measure tags(user: string, xs: array) { } PROVIDES number`, 'r');
const dvg = new DSLValueGraph(rt);
dvg.setValue('tags', { __subject: 't', user: 'u', xs: arr }, 7);
const e = dvg.getValue('tags', { __subject: 't', user: 'u', xs: arr });
cb(null, e ? e.value : 'MISSING');
}, rigor.args(rigor.gen.array(rigor.gen.int(0, 9), 0, 4), rigor.handler(reducers.first()))),
rigor.fn('extra_keys', (v) => {
const dvg = makeDvg();
dvg.setValue('budget', { __subject: 't', tenant: 't', feature: 'x', extra: 'zzz' }, v);
return dvg.getValue('budget', { __subject: 't', tenant: 't', feature: 'x', extra: 'zzz' }).value;
}, rigor.args(rigor.gen.int(0, 100))),
rigor.fn('positional_arity', (v) => {
const dvg = makeDvg();
dvg.setValue('budget', ['t', 'x'], v);
return dvg.getValue('budget', ['t', 'x']).value;
}, rigor.args(rigor.gen.int(0, 100))),
rigor.fn('unit_preserved', (v) => {
const dvg = makeDvg();
dvg.setValue('budget', { __subject: 't', tenant: 't', feature: 'x' }, v, { unit: 'tokens', source: 'ledger' });
const e = dvg.getValue('budget', { __subject: 't', tenant: 't', feature: 'x' });
return { value: e.value, unit: e.unit, source: e.source };
}, rigor.args(rigor.gen.int(0, 100))),
];
const checks = [
rigor.after('array_param', (ctx) => ctx.error == null && ctx.actual === 7),
rigor.after('extra_keys', (ctx) => ctx.actual === ctx.args[0]),
rigor.after('positional_arity', (ctx) => ctx.actual === ctx.args[0]),
rigor.after('unit_preserved', (ctx) => ctx.actual.value === ctx.args[0] && ctx.actual.unit === 'tokens' && ctx.actual.source === 'ledger'),
];
await expectPass('bindings', actions, checks);
});
it('strict:false degrades gracefully for unknown measures; strict mode throws', async () => {
const actions = [
rigor.fn('strict_false', (cb) => {
const rt = new DSLRuntime(new Arbiter()).compile(DSL, 'r');
const dvg = new DSLValueGraph(rt, { strict: false });
dvg.setValue('nope', {}, 1); // no-op
dvg.resolve('nope', () => 1); // no-op
const g = dvg.getValue('nope', {}); // null
dvg.measure('nope', {}).then(
(r) => cb(null, { get: g, measure: r.value }),
() => cb(null, { get: g, measure: 'rejected' })
);
}, rigor.args(rigor.handler(reducers.first()))),
rigor.fn('no_value_null', () => {
const dvg = makeDvg();
return dvg.getValue('budget', { __subject: 't', tenant: 't', feature: 'x' }) === null ? 'null' : 'value';
}, rigor.args()),
];
const checks = [
rigor.after('strict_false', (ctx) => ctx.actual.get === null && ctx.actual.measure === null),
rigor.after('no_value_null', (ctx) => ctx.actual === 'null'),
];
await expectPass('strict', actions, checks);
});
it('concurrent measure retrievals all resolve to the same stored value', async () => {
const actions = [
rigor.fn('concurrent_measure', (value, cb) => {
const dvg = makeDvg();
dvg.setValue('budget', { __subject: 't', tenant: 't', feature: 'x' }, value);
const N = 5;
const calls = [];
for (let i = 0; i < N; i++) {
calls.push(dvg.measure('budget', { __subject: 't', tenant: 't', feature: 'x' }).then((r) => r.value));
}
Promise.all(calls).then((values) => cb(null, values), (e) => cb(e));
}, rigor.args(rigor.gen.int(0, 100), rigor.handler(reducers.first()))),
];
const checks = [
rigor.after('concurrent_measure', (ctx) =>
ctx.error == null && Array.isArray(ctx.actual) && ctx.actual.length === 5 && ctx.actual.every((v) => v === ctx.args[0])),
];
await expectPass('concurrent', actions, checks);
});
it('stored values and resolvers coexist per subject (stored wins on its own subject)', async () => {
const actions = [
rigor.fn('subject_resolver_mix', (a, b, cb) => {
const dvg = makeDvg();
dvg.setValue('budget', { __subject: 'A', tenant: 'A', feature: 'x' }, a);
dvg.resolve('budget', (s) => b);
dvg.measure('budget', { __subject: 'A', tenant: 'A', feature: 'x' }).then(
(r1) => dvg.measure('budget', { __subject: 'B', tenant: 'B', feature: 'x' }).then(
(r2) => cb(null, { a: r1.value, b: r2.value }),
(e) => cb(e)
),
(e) => cb(e)
);
}, rigor.args(rigor.gen.int(0, 100), rigor.gen.int(0, 100), rigor.handler(reducers.first()))),
];
const checks = [
rigor.after('subject_resolver_mix', (ctx) =>
ctx.error == null && ctx.actual.a === ctx.args[0] && ctx.actual.b === ctx.args[1]),
];
await expectPass('subject-resolver-mix', actions, checks);
});
it('final confirmation: positional arity enforced, zero-param measures, control keys stripped', async () => {
const actions = [
rigor.fn('arity_too_many', (v) => {
const dvg = makeDvg();
try { dvg.setValue('budget', ['t', 'x', 'extra'], v); return { ok: true }; }
catch (e) { return { ok: false, msg: e.message }; }
}, rigor.args(rigor.gen.int(0, 100))),
rigor.fn('arity_too_few', (v) => {
const dvg = makeDvg();
try { dvg.setValue('budget', ['t'], v); return { ok: true }; }
catch (e) { return { ok: false, msg: e.message }; }
}, rigor.args(rigor.gen.int(0, 100))),
rigor.fn('arity_exact', (v) => {
const dvg = makeDvg();
dvg.setValue('budget', ['t', 'x'], v);
return dvg.getValue('budget', ['t', 'x']).value;
}, rigor.args(rigor.gen.int(0, 100))),
rigor.fn('zero_param_measure', (v) => {
const rt = new DSLRuntime(new Arbiter()).compile(`measure heartbeat() { } PROVIDES number`, 'r');
const dvg = new DSLValueGraph(rt);
dvg.setValue('heartbeat', {}, v);
const e = dvg.getValue('heartbeat', {});
return e ? e.value : 'MISSING';
}, rigor.args(rigor.gen.int(0, 100))),
rigor.fn('actor_stripped', (v) => {
const dvg = makeDvg();
// __actor is a control key → stripped from the cache key, so differing
// actors hit the SAME stored entry.
dvg.setValue('budget', { __actor: 'actor:1', __subject: 't', tenant: 't', feature: 'x' }, v);
return dvg.getValue('budget', { __actor: 'actor:2', __subject: 't', tenant: 't', feature: 'x' }).value;
}, rigor.args(rigor.gen.int(0, 100))),
];
const checks = [
rigor.after('arity_too_many', (ctx) => ctx.actual.ok === false && /expects 2 positional argument\(s\), got 3/.test(ctx.actual.msg)),
rigor.after('arity_too_few', (ctx) => ctx.actual.ok === false && /expects 2 positional argument\(s\), got 1/.test(ctx.actual.msg)),
rigor.after('arity_exact', (ctx) => ctx.actual === ctx.args[0]),
rigor.after('zero_param_measure', (ctx) => ctx.actual === ctx.args[0]),
rigor.after('actor_stripped', (ctx) => ctx.actual === ctx.args[0]),
];
await expectPass('final-confirm', actions, checks);
});
it('hardening: retrieval paths, sync-throw resolvers, unregister, param-driven resolvers, NaN', async () => {
const actions = [
rigor.fn('get_sync_vs_measure_async', (value, cb) => {
const dvg = makeDvg();
dvg.resolve('budget', (s, p, ctx, done) => { setTimeout(() => done(null, value), 1); });
const syncPath = dvg.getValue('budget', { __subject: 't', tenant: 't', feature: 'x' });
dvg.measure('budget', { __subject: 't', tenant: 't', feature: 'x' }).then(
(r) => cb(null, { sync: syncPath === null ? 'null' : syncPath.value, async: r.value }),
(e) => cb(e)
);
}, rigor.args(rigor.gen.int(0, 100), rigor.handler(reducers.first()))),
rigor.fn('resolver_throws_sync', (cb) => {
const dvg = makeDvg();
dvg.resolve('budget', () => { throw new Error('boom'); });
dvg.measure('budget', { __subject: 't', tenant: 't', feature: 'x' }).then(
(r) => cb(null, { ok: true, v: r.value }),
(e) => cb(null, { ok: false, msg: e.message })
);
}, rigor.args(rigor.handler(reducers.first()))),
rigor.fn('unregister_after_attach', (cb) => {
const rt = new DSLRuntime(new Arbiter()).compile(DSL, 'r');
const dvg = new DSLValueGraph(rt);
dvg.attach();
rt.unregisterMeasure('budget');
rt.measure('budget', { __subject: 't', tenant: 't', feature: 'x' }).then(
(r) => cb(null, { ok: true, v: r.value }),
(e) => cb(null, { ok: false, msg: e.message })
);
}, rigor.args(rigor.handler(reducers.first()))),
rigor.fn('resolver_from_params', (x, y, cb) => {
const dvg = makeDvg();
dvg.resolve('budget', (s, p, ctx, done) => done(null, p.feature === 'a' ? x : y));
dvg.measure('budget', { __subject: 't', tenant: 't', feature: 'a' }).then(
(r1) => dvg.measure('budget', { __subject: 't', tenant: 't', feature: 'b' }).then(
(r2) => cb(null, { a: r1.value, b: r2.value }),
(e) => cb(e)
),
(e) => cb(e)
);
}, rigor.args(rigor.gen.int(0, 100), rigor.gen.int(0, 100), rigor.handler(reducers.first()))),
rigor.fn('nan_rejected', () => {
const dvg = makeDvg();
try { dvg.setValue('budget', { __subject: 't', tenant: 't', feature: 'x' }, Number.NaN); return { ok: true }; }
catch (e) { return { ok: false, msg: e.message }; }
}, rigor.args()),
];
const checks = [
rigor.after('get_sync_vs_measure_async', (ctx) =>
ctx.error == null && ctx.actual.sync === 'null' && ctx.actual.async === ctx.args[0]),
rigor.after('resolver_throws_sync', (ctx) => ctx.actual.ok === false && /boom/.test(ctx.actual.msg)),
rigor.after('unregister_after_attach', (ctx) => ctx.actual.ok === false && /no provider registered/.test(ctx.actual.msg)),
rigor.after('resolver_from_params', (ctx) =>
ctx.error == null && ctx.actual.a === ctx.args[0] && ctx.actual.b === ctx.args[1]),
rigor.after('nan_rejected', (ctx) => ctx.actual.ok === false && /must match declared type 'number'/.test(ctx.actual.msg)),
];
await expectPass('hardening', actions, checks);
});
});