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
+36
View File
@@ -194,4 +194,40 @@ describe('DSLRuntime extended', () => {
assert.ok(!rt.arbiter.relationConfigs.has('owns'));
assert.ok(rt.arbiter.relationConfigs.has('can_share'));
});
it('indexes measures and resolves them via a registered provider', async () => {
const rt = new DSLRuntime(new Arbiter()).compile(`
definition Employee { id: string? }
definition Project { id: string? }
measure budget_available(tenant: Employee, feature: string) { } PROVIDES number
measure clearance(user: Employee) { } PROVIDES string
`, 'rt-measures');
// Schema exposes measures with their return type.
const schema = rt.getSchema();
const budget = schema.measures.find(m => m.name === 'budget_available');
assert.ok(budget);
assert.equal(budget.returnType, 'number');
assert.deepEqual(budget.params.map(p => p.name), ['tenant', 'feature']);
assert.ok(rt.relationNames().includes('clearance'));
// No provider yet → resolution fails loudly.
await assert.rejects(() => rt.measure('budget_available', { tenant: 'tenant:acme', feature: 'tokens_in:gpt-4' }), /no provider registered/);
// Register a provider (the ARRA adapter would bridge the value-graph).
rt.registerMeasure('budget_available', async ({ tenant, feature }) => {
assert.equal(tenant, 'tenant:acme');
assert.equal(feature, 'tokens_in:gpt-4');
return { value: 1250, unit: 'tokens' };
});
const resolved = await rt.measure('budget_available', { tenant: 'tenant:acme', feature: 'tokens_in:gpt-4' });
assert.equal(resolved.value, 1250);
assert.equal(resolved.unit, 'tokens');
// Positional args bind by param order for unary measures.
rt.registerMeasure('clearance', async () => ({ value: 'secret' }));
assert.equal((await rt.measure('clearance', ['user:alice'])).value, 'secret');
// registerMeasure rejects undeclared names.
assert.throws(() => rt.registerMeasure('nope', async () => 1), /not a declared measure/);
});
});
+201
View File
@@ -0,0 +1,201 @@
/**
* DSLValueGraph — evidence DSL declares the value-graph's typing/structures;
* measures retrieve through the graph (partial-graph purposes), and attributes
* that are neither cached nor computed are stored directly via setValue.
*/
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { Arbiter } from '@arbiter/core';
import { ValueGraph } from '@arbiter/value-graph';
import { DSLRuntime, DSLValueGraph } from '../src/index.js';
function makeRuntime(dsl) {
return new DSLRuntime(new Arbiter()).compile(dsl, 'vg-test');
}
const DSL = `
definition Employee { id: string? }
definition Project { id: string? }
measure budget_available(tenant: Employee, feature: string) { } PROVIDES number
measure clearance(user: Employee) { } PROVIDES string
measure is_active(user: Employee) { } PROVIDES boolean
`;
describe('DSLValueGraph integration', () => {
it('derives the value-graph node schema from the DSL measures', () => {
const dvg = new DSLValueGraph(makeRuntime(DSL));
const schema = dvg.schema();
assert.equal(schema.budget_available.returnType, 'number');
assert.deepEqual(schema.budget_available.params.map((p) => p.name), ['tenant', 'feature']);
assert.equal(schema.clearance.returnType, 'string');
assert.equal(schema.is_active.returnType, 'boolean');
});
it('stores attributes that are neither cached nor computed, and reads them back', () => {
const dvg = new DSLValueGraph(makeRuntime(DSL));
dvg.setValue('budget_available', { __subject: 'tenant:acme', tenant: 'tenant:acme', feature: 'tokens_in:gpt-4' }, 1250, { unit: 'tokens' });
const entry = dvg.getValue('budget_available', { __subject: 'tenant:acme', tenant: 'tenant:acme', feature: 'tokens_in:gpt-4' });
assert.equal(entry.value, 1250);
assert.equal(entry.unit, 'tokens');
assert.equal(entry.source, 'dsl');
assert.equal(entry.fresh, true);
});
it('measure() retrieves through the value-graph (partial-graph purposes)', async () => {
const dvg = new DSLValueGraph(makeRuntime(DSL));
dvg.setValue('clearance', { __subject: 'user:alice', user: 'user:alice' }, 'secret');
const resolved = await dvg.measure('clearance', { __subject: 'user:alice', user: 'user:alice' });
assert.equal(resolved.value, 'secret');
});
it('attach() wires runtime.measure() through the value-graph', async () => {
const rt = makeRuntime(DSL);
const dvg = new DSLValueGraph(rt);
dvg.setValue('is_active', { __subject: 'user:alice', user: 'user:alice' }, true);
dvg.attach();
const resolved = await rt.measure('is_active', { __subject: 'user:alice', user: 'user:alice' });
assert.equal(resolved.value, true);
// A measure with nothing stored resolves to null — no provider explosion.
const missing = await rt.measure('clearance', { __subject: 'user:bob', user: 'user:bob' });
assert.equal(missing.value, null);
});
it('resolves a value via an external callback resolver (the compute substrate)', async () => {
const dvg = new DSLValueGraph(makeRuntime(DSL));
dvg.resolve('budget_available', (subject, params, ctx, cb) => {
cb(null, { value: 999, unit: 'tokens', source: 'overlay:balances' });
});
const entry = await dvg.measure('budget_available', { __subject: 'tenant:acme', tenant: 'tenant:acme', feature: 'x' });
assert.equal(entry.value, 999);
assert.equal(entry.unit, 'tokens');
assert.equal(entry.source, 'overlay:balances');
});
it('validates stored values against the declared PROVIDES type', () => {
const dvg = new DSLValueGraph(makeRuntime(DSL));
assert.throws(() => dvg.setValue('budget_available', { feature: 'x' }, 'not-a-number'), /must match declared type 'number'/);
assert.throws(() => dvg.setValue('clearance', { user: 'u' }, 42), /must match declared type 'string'/);
});
it('validates parameter bindings against the declared parameter types', () => {
const dvg = new DSLValueGraph(makeRuntime(DSL));
assert.throws(() => dvg.setValue('budget_available', { tenant: 't', feature: 42 }, 5),
/parameter 'feature' of 'budget_available' must match declared type 'string'/);
});
it('rejects unknown measure names', async () => {
const dvg = new DSLValueGraph(makeRuntime(DSL));
assert.throws(() => dvg.setValue('nope', {}, 1), /not a declared measure/);
assert.throws(() => dvg.getValue('nope', {}), /not a declared measure/);
assert.throws(() => dvg.resolve('nope', () => 1), /not a declared measure/);
await assert.rejects(() => dvg.measure('nope', {}), /not a declared measure/);
});
it('binds positional args by declared parameter order', () => {
const dvg = new DSLValueGraph(makeRuntime(DSL));
dvg.setValue('budget_available', ['tenant:acme', 'tokens_in:gpt-4'], 50);
const entry = dvg.getValue('budget_available', ['tenant:acme', 'tokens_in:gpt-4']);
assert.equal(entry.value, 50);
});
it('the value-graph itself enforces declared return types on resolver results', async () => {
const dvg = new DSLValueGraph(makeRuntime(DSL));
dvg.resolve('clearance', (s, p, ctx, cb) => cb(null, 12345)); // number, but PROVIDES string
const err = await dvg.measure('clearance', { __subject: 'user:alice', user: 'user:alice' }).then(() => null, (e) => e);
assert.ok(err, 'a wrong-typed resolver result must be rejected');
assert.match(err.message, /must match declared type 'string'/);
});
it('shares a caller-supplied value graph and supports subjectOf', () => {
const vg = new ValueGraph();
const dvg = new DSLValueGraph(makeRuntime(DSL), {
valueGraph: vg,
subjectOf: (name, args) => args.tenant || args.user || 'global'
});
assert.equal(dvg.vg, vg);
dvg.setValue('clearance', { user: 'user:alice' }, 'top-secret');
assert.equal(dvg.getValue('clearance', { user: 'user:alice' }).value, 'top-secret');
});
it('rigor: setValue/getValue round-trips always return the stored value', async () => {
const { rigor } = await import('@rigor/core');
const report = await rigor.campaign(
[rigor.fn('roundtrip', (value) => {
const dvg = new DSLValueGraph(makeRuntime(DSL));
dvg.setValue('budget_available', { __subject: 'tenant:acme', tenant: 'tenant:acme', feature: 'f' }, value);
const e = dvg.getValue('budget_available', { __subject: 'tenant:acme', tenant: 'tenant:acme', feature: 'f' });
return e ? e.value : 'MISSING';
}, rigor.args(rigor.gen.int(-100000, 100000)))],
rigor.crucible([rigor.invariant('roundtrip', (ctx) => ctx.actual === ctx.args[0])])
).run({ effort: 150, seed: 'dsl-vg-roundtrip' });
if (report.status !== 'passed') {
throw new Error(`DSLValueGraph rigor roundtrip failed: ${JSON.stringify((report.failures || []).slice(0, 3))}`);
}
});
it('rigor: setValue rejects wrong-typed values for every generated value', async () => {
const { rigor } = await import('@rigor/core');
const report = await rigor.campaign(
[rigor.fn('typed-reject', (value) => {
const dvg = new DSLValueGraph(makeRuntime(DSL));
try {
dvg.setValue('clearance', { user: 'u' }, value); // PROVIDES string
return { ok: true };
} catch (e) {
return { ok: false, error: e.message };
}
}, rigor.args(rigor.gen.int(0, 100)))],
rigor.crucible([rigor.invariant('rejects', (ctx) => ctx.actual.ok === false && /must match declared type 'string'/.test(ctx.actual.error))])
).run({ effort: 150, seed: 'dsl-vg-typecheck' });
if (report.status !== 'passed') {
throw new Error(`DSLValueGraph rigor typecheck failed: ${JSON.stringify((report.failures || []).slice(0, 3))}`);
}
});
it('the aligned type vocabulary round-trips buffer/interval/any/duration', () => {
const dvg = new DSLValueGraph(makeRuntime(`
definition T { id: string }
measure quota(user: string) { } PROVIDES buffer
measure span(user: string) { } PROVIDES interval
measure whatever(user: string) { } PROVIDES any
measure window(user: string) { } PROVIDES duration
`));
const bytes = new Uint8Array([1, 2, 3, 254, 255]);
dvg.setValue('quota', { __subject: 'u', user: 'u' }, bytes);
assert.deepEqual([...dvg.getValue('quota', { __subject: 'u', user: 'u' }).value], [...bytes]);
dvg.setValue('span', { __subject: 'u', user: 'u' }, { lower: 100, upper: 200 });
assert.deepEqual(dvg.getValue('span', { __subject: 'u', user: 'u' }).value, { lower: 100, upper: 200 });
dvg.setValue('whatever', { __subject: 'u', user: 'u' }, 42); // any → accepted
assert.equal(dvg.getValue('whatever', { __subject: 'u', user: 'u' }).value, 42);
dvg.setValue('window', { __subject: 'u', user: 'u' }, '1h');
assert.equal(dvg.getValue('window', { __subject: 'u', user: 'u' }).value, '1h');
});
it('sync() re-registers measures after the runtime recompiles', () => {
const rt = new DSLRuntime(new Arbiter()).compile('measure a() { } PROVIDES number', 'v1');
const dvg = new DSLValueGraph(rt);
rt.compile('measure b() { } PROVIDES string', 'v2'); // a removed, b added
// Before sync: stale schema.
assert.throws(() => dvg.setValue('b', {}, 'x'), /not a declared measure/);
// After sync: b is usable, a is gone.
dvg.sync();
dvg.setValue('b', {}, 'x');
assert.equal(dvg.getValue('b', {}).value, 'x');
assert.throws(() => dvg.setValue('a', {}, 1), /not a declared measure/);
// schema() reflects the current measure set
assert.ok(dvg.schema().b);
assert.ok(!dvg.schema().a);
});
it('attach() re-syncs and unregisters providers for removed measures after recompile', async () => {
const rt = new DSLRuntime(new Arbiter()).compile('measure a() { } PROVIDES number', 'v1');
const dvg = new DSLValueGraph(rt);
dvg.attach();
rt.compile('measure b() { } PROVIDES string', 'v2');
dvg.attach(); // re-sync + re-wire
dvg.setValue('b', { __subject: 't' }, 'hello');
assert.equal((await rt.measure('b', { __subject: 't' })).value, 'hello');
// a is no longer declared after the recompile → the runtime rejects it
await assert.rejects(() => rt.measure('a', { __subject: 't' }), /unknown measure/);
});
});
+62
View File
@@ -12,6 +12,8 @@ import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { Arbiter } from '@arbiter/core';
import { DSLCompiler } from '../src/DSLCompiler.js';
import { DSLRuntime } from '../src/runtime/DSLRuntime.js';
import { DSLValueGraph } from '../src/value-graph/DSLValueGraph.js';
const DEFS = `
definition Employee { id: string }
@@ -151,4 +153,64 @@ describe('Evidence composition', () => {
assert.equal(arb.check('u:1', 'can_read', 'doc:9').possibility, 0.5);
assert.equal(arb.check('u:1', 'can_browse', 'doc:9').possibility, 0.5);
});
describe('measure + evidence composition', () => {
const M_DEFS = `
definition Employee { id: string }
measure budget_used(user: Employee) { } PROVIDES number
measure budget_limit(user: Employee) { } PROVIDES number
evidence can_use(user: Employee, doc: string) { budget_used(user) <= budget_limit(user) }
`;
it('compiles an evidence comparator over measure valueRelations', () => {
const { arb, result } = compile(M_DEFS, 'm+e-compile');
assert.ok(result.success, JSON.stringify(result.errors));
const cfg = arb.relationConfigs.get('can_use');
assert.equal(cfg.type, 'relational_comparator');
assert.equal(cfg.left.valueRelation, 'budget_used');
assert.equal(cfg.right.valueRelation, 'budget_limit');
assert.deepEqual(cfg.dependsOn, ['budget_used', 'budget_limit']);
assert.equal(cfg._needsValues, true);
});
it('evaluates with measure values from registered providers (measure → evidence)', async () => {
const rt = new DSLRuntime(new Arbiter()).compile(M_DEFS, 'm+e-provider');
const dvg = new DSLValueGraph(rt);
dvg.attach(); // wires runtime.measure through the value-graph
rt.registerMeasure('budget_used', async () => ({ value: 900 }));
rt.registerMeasure('budget_limit', async () => ({ value: 1000 }));
rt.arbiter.addNode('u:1', 'Employee');
// Both systems live: the measure resolves standalone...
assert.equal((await rt.measure('budget_used', { user: 'u:1' })).value, 900);
// ...and composes into the evidence comparator.
const allow = await rt.check('u:1', 'can_use', 'doc:9');
assert.equal(allow.possibility, 1);
assert.equal(allow.reason, 'allow_rule_matched');
assert.ok(allow.providedFacts.includes('budget_used'), 'measure injected as a partial-graph edge');
});
it('evaluates with measure values stored in the value-graph (setValue)', async () => {
const rt = new DSLRuntime(new Arbiter()).compile(M_DEFS, 'm+e-vg');
const dvg = new DSLValueGraph(rt);
dvg.attach();
dvg.setValue('budget_used', { __subject: 'u:1', user: 'u:1' }, 900);
dvg.setValue('budget_limit', { __subject: 'u:1', user: 'u:1' }, 1000);
rt.arbiter.addNode('u:1', 'Employee');
const allow = await rt.check('u:1', 'can_use', 'doc:9');
assert.equal(allow.possibility, 1, 'in-budget evidence allowed from graph-sourced measures');
// Over budget → the comparator denies.
const rt2 = new DSLRuntime(new Arbiter()).compile(M_DEFS, 'm+e-vg2');
const dvg2 = new DSLValueGraph(rt2);
dvg2.attach();
dvg2.setValue('budget_used', { __subject: 'u:1', user: 'u:1' }, 1200);
dvg2.setValue('budget_limit', { __subject: 'u:1', user: 'u:1' }, 1000);
rt2.arbiter.addNode('u:1', 'Employee');
const deny = await rt2.check('u:1', 'can_use', 'doc:9');
assert.equal(deny.possibility, 0);
assert.equal(deny.reason, 'values_compared_comparison_false');
});
});
});
@@ -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);
});
});
+226
View File
@@ -0,0 +1,226 @@
/**
* tests/rigor/dsl-value-graph.test.js — js-rigor deep tests for the
* DSLValueGraph integration (evidence DSL declares the value-graph's typing;
* measures retrieve through the graph; attributes are stored directly).
*
* Covers, as property campaigns:
* - setValue/getValue round-trips across every declared value type
* - type enforcement (wrong-typed writes always reject)
* - parameter-binding validation (wrong-typed params always reject)
* - subject scoping (different subjects never share entries)
* - attach() parity (runtime.measure === value-graph retrieval)
* - external resolvers compute, and a stored value wins over the resolver
* - invalidation clears a stored value (next retrieval → null)
* - schema derivation from generated measure declarations
* - multi-measure independence
*/
import { describe, it } from 'node:test';
import { rigor, reducers } from '@rigor/core';
import { Arbiter } from '@arbiter/core';
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
measure active(user: string) { } PROVIDES boolean
measure tags(user: string) { } PROVIDES array
`;
function makeDvg() {
const rt = new DSLRuntime(new Arbiter()).compile(DSL, 'vg-rigor');
return new DSLValueGraph(rt);
}
async function expectPass(name, actions, checks, config = {}) {
const report = await rigor.campaign(actions, rigor.crucible(checks))
.run({ seed: `dsl-vg-${name}`, effort: 120, 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 integration (rigor)', () => {
it('setValue/getValue round-trips every declared value type', async () => {
const actions = [
rigor.fn('rt_number', (v) => {
const dvg = makeDvg();
dvg.setValue('budget', { __subject: 't', tenant: 't', feature: 'x' }, v);
const e = dvg.getValue('budget', { __subject: 't', tenant: 't', feature: 'x' });
return e ? e.value : 'MISSING';
}, rigor.args(rigor.gen.int(-100000, 100000))),
rigor.fn('rt_string', (v) => {
const dvg = makeDvg();
dvg.setValue('label', { __subject: 't', user: 'u' }, v);
const e = dvg.getValue('label', { __subject: 't', user: 'u' });
return e ? e.value : 'MISSING';
}, rigor.args(rigor.gen.asciiString())),
rigor.fn('rt_bool', (v) => {
const dvg = makeDvg();
dvg.setValue('active', { __subject: 't', user: 'u' }, v);
const e = dvg.getValue('active', { __subject: 't', user: 'u' });
return e ? e.value : 'MISSING';
}, rigor.args(rigor.gen.boolean())),
rigor.fn('rt_array', (v) => {
const dvg = makeDvg();
dvg.setValue('tags', { __subject: 't', user: 'u' }, v);
const e = dvg.getValue('tags', { __subject: 't', user: 'u' });
return e ? e.value : null;
}, rigor.args(rigor.gen.array(rigor.gen.string(), 0, 5))),
];
const checks = [
rigor.after('rt_number', (ctx) => ctx.actual === ctx.args[0]),
rigor.after('rt_string', (ctx) => ctx.actual === ctx.args[0]),
rigor.after('rt_bool', (ctx) => ctx.actual === ctx.args[0]),
rigor.after('rt_array', (ctx) => JSON.stringify(ctx.actual) === JSON.stringify(ctx.args[0])),
];
await expectPass('roundtrip', actions, checks, { effort: 200 });
});
it('type enforcement: wrong-typed writes always reject, right-typed always pass', async () => {
const actions = [
rigor.fn('type_ok', (v) => {
const dvg = makeDvg();
dvg.setValue('budget', { __subject: 't', tenant: 't', feature: 'x' }, v);
return dvg.getValue('budget', { __subject: 't', tenant: 't', feature: 'x' }).value;
}, rigor.args(rigor.gen.int(-100, 100))),
rigor.fn('type_reject', (v) => {
const dvg = makeDvg();
try { dvg.setValue('label', { user: 'u' }, v); return { ok: true }; }
catch (e) { return { ok: false, msg: e.message }; }
}, rigor.args(rigor.gen.oneOf([rigor.gen.int(0, 100), rigor.gen.boolean(), rigor.gen.array(rigor.gen.int(0, 9), 1, 3)]))),
rigor.fn('param_reject', (v) => {
const dvg = makeDvg();
try { dvg.setValue('budget', { tenant: 't', feature: v }, 1); return { ok: true }; }
catch (e) { return { ok: false, msg: e.message }; }
}, rigor.args(rigor.gen.oneOf([rigor.gen.constant(42), rigor.gen.boolean(), rigor.gen.array(rigor.gen.int(0, 9), 1, 2)]))),
];
const checks = [
rigor.after('type_ok', (ctx) => ctx.actual === ctx.args[0]),
rigor.after('type_reject', (ctx) => ctx.actual.ok === false && /must match declared type 'string'/.test(ctx.actual.msg)),
rigor.after('param_reject', (ctx) => ctx.actual.ok === false && /parameter 'feature'/.test(ctx.actual.msg)),
];
await expectPass('typecheck', actions, checks, { effort: 200 });
});
it('subjects never share entries', async () => {
const actions = [
rigor.fn('subject_isolation', (a, b) => {
const dvg = makeDvg();
dvg.setValue('budget', { __subject: 'A', tenant: 'A', feature: 'x' }, a);
dvg.setValue('budget', { __subject: 'B', tenant: 'B', feature: 'x' }, b);
const ea = dvg.getValue('budget', { __subject: 'A', tenant: 'A', feature: 'x' });
const eb = dvg.getValue('budget', { __subject: 'B', tenant: 'B', feature: 'x' });
return { a: ea ? ea.value : null, b: eb ? eb.value : null };
}, rigor.args(rigor.gen.int(-50, 50), rigor.gen.int(-50, 50))),
];
const checks = [
rigor.after('subject_isolation', (ctx) => ctx.actual.a === ctx.args[0] && ctx.actual.b === ctx.args[1]),
];
await expectPass('subjects', actions, checks, { effort: 120 });
});
it('attach() makes runtime.measure retrieve through the value-graph', async () => {
const actions = [
rigor.fn('attach_parity', (value, cb) => {
const rt = new DSLRuntime(new Arbiter()).compile(DSL, 'vg-rigor');
const dvg = new DSLValueGraph(rt);
dvg.attach();
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()))),
];
const checks = [
rigor.after('attach_parity', (ctx) => ctx.error == null && ctx.actual === ctx.args[0]),
];
await expectPass('attach', actions, checks, { effort: 120 });
});
it('external resolvers compute; a stored value wins over the resolver', async () => {
const actions = [
rigor.fn('resolver_compute', (value, cb) => {
const dvg = makeDvg();
dvg.resolve('budget', (s, p, ctx, done) => done(null, { value, unit: 'tokens', source: 'resolver' }));
dvg.measure('budget', { __subject: 't', tenant: 't', feature: 'x' }).then(
(r) => cb(null, { value: r.value, source: r.source }),
(e) => cb(e)
);
}, rigor.args(rigor.gen.int(0, 100), rigor.handler(reducers.first()))),
rigor.fn('stored_wins', (value, cb) => {
const dvg = makeDvg();
dvg.resolve('budget', (s, p, ctx, done) => done(null, 999));
dvg.setValue('budget', { __subject: 't', tenant: 't', feature: 'x' }, value);
dvg.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()))),
];
const checks = [
rigor.after('resolver_compute', (ctx) => ctx.error == null && ctx.actual.value === ctx.args[0] && ctx.actual.source === 'resolver'),
rigor.after('stored_wins', (ctx) => ctx.error == null && ctx.actual === ctx.args[0]),
];
await expectPass('resolvers', actions, checks, { effort: 120 });
});
it('invalidation clears a stored value; the next retrieval is null', async () => {
const actions = [
rigor.fn('invalidate_clears', (value, cb) => {
const dvg = makeDvg();
dvg.setValue('budget', { __subject: 't', tenant: 't', feature: 'x' }, value);
dvg.vg.invalidate('t', 'budget', { tenant: 't', feature: 'x' });
dvg.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()))),
];
const checks = [
rigor.after('invalidate_clears', (ctx) => ctx.error == null && ctx.actual === null),
];
await expectPass('invalidate', actions, checks, { effort: 120 });
});
it('schema derivation: every generated measure gets its declared returnType and params', async () => {
const actions = [
rigor.fn('schema_ok', (name, type) => {
const dsl = `definition T { id: string } measure ${name}(p: T) { } PROVIDES ${type}`;
const rt = new DSLRuntime(new Arbiter()).compile(dsl, 'gen');
const dvg = new DSLValueGraph(rt);
const spec = dvg.schema()[name];
return { returnType: spec && spec.returnType, params: spec ? spec.params.map((x) => x.name) : null };
}, rigor.args(
rigor.gen.oneOf(['balance', 'rate', 'quota', 'volume', 'meter']),
rigor.gen.oneOf(['number', 'string', 'boolean', 'array'])
)),
];
const checks = [
rigor.after('schema_ok', (ctx) => ctx.actual.returnType === ctx.args[1] && JSON.stringify(ctx.actual.params) === JSON.stringify(['p'])),
];
await expectPass('schema', actions, checks, { effort: 120 });
});
it('measures are independent (no cross-measure cache sharing)', async () => {
const actions = [
rigor.fn('multi_measure', (a, b) => {
const dvg = makeDvg();
dvg.setValue('budget', { __subject: 't', tenant: 't', feature: 'x' }, a);
dvg.setValue('label', { __subject: 't', user: 'u' }, String(b));
return {
budget: dvg.getValue('budget', { __subject: 't', tenant: 't', feature: 'x' }).value,
label: dvg.getValue('label', { __subject: 't', user: 'u' }).value
};
}, rigor.args(rigor.gen.int(0, 100), rigor.gen.int(0, 100))),
];
const checks = [
rigor.after('multi_measure', (ctx) => ctx.actual.budget === ctx.args[0] && ctx.actual.label === String(ctx.args[1])),
];
await expectPass('independence', actions, checks, { effort: 120 });
});
});