js-rigor: possibilistic validity layer (Cella FVN labels, conflict mass, validification)

Adds an epistemic validity layer in the spirit of the zig-contour fusion
spec: every check result now carries a validity block {label, operator,
regime, sources, conflictMass, validifiedPossibility, nonMaxitive}.

- Relations accept a validity label (default heuristic = unlabeled input).
- Labels propagate through fusion: identity/max preserve the weakest
  source label (max is already valid under arbitrary dependence); min
  (conjunctive: intersection, chain, TTU, multi_hop, parent) is
  approximate at best, surfaces the conflict mass (1 - possibility) that
  was previously dropped, and exposes the arbitrary-regime validification
  min(1, K*gamma); product-style operators (exclusion, defeasible) and
  interior OWA averaging are always heuristic, with nonMaxitive flagged.
- Reliability and validity are now explicitly distinct: reliability stays
  the scalar confidence adaptation; validity tracks the epistemic label.
- The hottest paths attach a shared frozen default block instead of
  allocating (perf A/B shows no regression: ~300k ops/s direct both ways).
- Pre-existing fixes surfaced while wiring: the array-form logical config
  dropped top-level aggregator/owaWeights (average union compiled as max),
  and _createStandardResult dropped unknown fields (validity never
  survived rule results).

New campaign validity-parity.test.js pins the label taxonomy, conflict
mass, validification, weakest-propagation, and the reliability/validity
separation. Suites: rigor 203/0, full 803/741/0.
This commit is contained in:
John Dvorak
2026-08-02 08:57:05 -07:00
parent fb258035f9
commit 58e8b0e030
14 changed files with 408 additions and 11 deletions
+145
View File
@@ -0,0 +1,145 @@
/**
* rigor/validity-parity.test.js — possibilistic validity metadata.
*
* Pins the Cella-FVN-inspired validity layer:
* - every check result carries a validity block {label, operator, regime,
* sources, conflictMass, validifiedPossibility, nonMaxitive}
* - unlabeled relations default to heuristic; a labeled relation
* propagates its label through identity/max fusion
* - max (disjunctive) fusion preserves the weakest source label
* - interior OWA/averaging is non-maxitive and always heuristic
* - min (conjunctive) fusion is approximate at best, surfaces the
* conflict mass (1 - possibility), and exposes the arbitrary-regime
* validification min(1, K*possibility)
* - product-style operators (exclusion, defeasible) are always heuristic
* - reliability and validity are distinct: reliability stays the scalar
* confidence; validity tracks the epistemic label
*/
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { rigor } from '@rigor/core';
import { Arbiter } from '../../src/index.js';
import { buildValidity, mergeValidity, weakestValidity, DEFAULT_VALIDITY } from '../../src/core/validity.js';
const child = (r) => ({ type: 'direct', relation: r });
function mk() {
const a = new Arbiter();
a.addNode('u:0', 'user');
a.addNode('d:0', 'doc');
a.addNode('g:0', 'group');
return a;
}
describe('Possibilistic validity metadata (rigor)', () => {
it('FIXED: labels, operators, conflict mass, validification per kind', () => {
// direct unlabeled -> heuristic identity
{
const a = mk();
a.setRelationConfig('t', { type: 'direct', relation: 'r1' });
a.addRelation('u:0', 'r1', 'd:0', { possibility: 0.8 });
const r = a.check('u:0', 't', 'd:0');
assert.equal(r.validity.label, 'heuristic');
assert.equal(r.validity.operator, 'identity');
assert.deepEqual(r.validity.sources, ['r1']);
assert.equal(r.validity.conflictMass, 0);
}
// labeled direct propagates its label
{
const a = mk();
a.setRelationConfig('t', { type: 'direct', relation: 'r1' });
a.addRelation('u:0', 'r1', 'd:0', { possibility: 0.8, validity: 'finite_sample' });
assert.equal(a.check('u:0', 't', 'd:0').validity.label, 'finite_sample');
}
// max fusion preserves the weakest source label
{
const a = mk();
a.setRelationConfig('t', { union: [child('r1'), child('r2')] });
a.addRelation('u:0', 'r1', 'd:0', { possibility: 0.8, validity: 'finite_sample' });
a.addRelation('u:0', 'r2', 'd:0', { possibility: 0.5, validity: 'conformal' });
const r = a.check('u:0', 't', 'd:0');
assert.equal(r.validity.label, 'conformal', 'weakest label wins through max');
assert.equal(r.validity.operator, 'max');
assert.equal(r.validity.nonMaxitive, false);
}
// interior OWA is non-maxitive heuristic
{
const a = mk();
a.setRelationConfig('t', { union: [child('r1'), child('r2')], aggregator: 'average' });
a.addRelation('u:0', 'r1', 'd:0', { possibility: 0.8, validity: 'finite_sample' });
a.addRelation('u:0', 'r2', 'd:0', { possibility: 0.5, validity: 'finite_sample' });
const r = a.check('u:0', 't', 'd:0');
assert.equal(r.validity.operator, 'owa');
assert.equal(r.validity.nonMaxitive, true);
assert.equal(r.validity.label, 'heuristic', 'averaging never claims validity');
}
// min fusion: approximate, conflict mass, validification
{
const a = mk();
a.setRelationConfig('t', { intersection: [child('r1'), child('r2')] });
a.addRelation('u:0', 'r1', 'd:0', { possibility: 0.8, validity: 'finite_sample' });
a.addRelation('u:0', 'r2', 'd:0', { possibility: 0.5, validity: 'finite_sample' });
const r = a.check('u:0', 't', 'd:0');
assert.equal(r.validity.operator, 'min');
assert.equal(r.validity.label, 'approximate', 'unvalidified conjunctive is approximate at best');
assert.ok(Math.abs(r.validity.conflictMass - (1 - 0.5)) < 1e-9, 'conflict mass = 1 - possibility');
assert.equal(r.validity.validifiedPossibility, 1, 'min(1, K*gamma) with K=2, gamma=0.5');
}
// product operators are heuristic even with labeled sources
{
const a = mk();
a.setRelationConfig('t', { exclusion: [child('r1'), child('r2')] });
a.addRelation('u:0', 'r1', 'd:0', { possibility: 0.8, validity: 'finite_sample' });
a.addRelation('u:0', 'r2', 'd:0', { possibility: 0.5, validity: 'finite_sample' });
assert.equal(a.check('u:0', 't', 'd:0').validity.label, 'heuristic');
a.setRelationConfig('t2', { type: 'defeasible', when: child('r1'), unless: child('r2') });
assert.equal(a.check('u:0', 't2', 'd:0').validity.label, 'heuristic');
}
// chain: conjunctive ranking with conflict surfacing
{
const a = mk();
a.setRelationConfig('t', { type: 'chain', steps: [{ relation: 'member_of', direction: 'out' }, { relation: 'viewer', direction: 'out' }] });
a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.9 });
a.addRelation('g:0', 'viewer', 'd:0', { possibility: 0.6 });
const r = a.check('u:0', 't', 'd:0');
assert.equal(r.validity.operator, 'min');
assert.ok(Math.abs(r.validity.conflictMass - 0.4) < 1e-9, 'chain conflict mass');
}
// reliability and validity stay distinct
{
const a = mk();
a.setRelationConfig('t', { type: 'direct', relation: 'r1' });
a.addRelation('u:0', 'r1', 'd:0', { possibility: 0.8, reliability: 0.42, validity: 'finite_sample' });
const r = a.check('u:0', 't', 'd:0');
assert.equal(r.reliability, 0.42, 'reliability unchanged');
assert.equal(r.validity.label, 'finite_sample', 'validity independent of reliability');
}
});
it('PROPERTY CAMPAIGN: helper semantics (weakest, merge, default identity)', async () => {
const result = await rigor.campaign(
[rigor.fn('helpers', (labels) => {
const weakest = weakestValidity(labels);
const merged = mergeValidity(labels.map(l => ({
label: l, operator: 'max', regime: 'arbitrary', sources: ['r'], nonMaxitive: false, conflictMass: 0, validifiedPossibility: null
})));
const single = mergeValidity([{
label: labels[0], operator: 'max', regime: 'arbitrary', sources: ['r'], nonMaxitive: false, conflictMass: 0, validifiedPossibility: null
}]);
return {
weakest: weakestValidity([labels[0], weakest]),
mergedWeakest: merged.label === weakest,
singlePass: single === undefined ? false : single.label === labels[0],
defaultIsFrozen: Object.isFrozen(DEFAULT_VALIDITY),
buildPositional: buildValidity('min', ['a'], ['finite_sample'], 2, 0.5).validifiedPossibility === 1
};
}, 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))
])
).run({ effort: 200, seed: 'validity-helpers-2026', artifacts: { dir: '', persist: 'never' } });
const inv = result.crucibleVerdict?.invariants?.find(i => i.name === 'helper invariants');
assert.ok(inv && inv.passed, `validity helpers violated in ${inv?.failureCount} cases`);
});
});