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
+22 -1
View File
@@ -1,3 +1,4 @@
import { buildValidity, normalizeValidity, mergeValidity, DEFAULT_VALIDITY } from '../core/validity.js';
import { Arbiter } from '../core/Arbiter.js';
import { RuleEvaluator } from './RuleEvaluator.js';
import { RuleCollector } from './RuleCollector.js';
@@ -162,6 +163,9 @@ export class AuthorizationChecker {
} else {
result = {
possibility: directRel.possibility,
validity: directRel.validity !== undefined
? buildValidity('identity', [effectiveRelation], [normalizeValidity(directRel.validity)], 1, directRel.possibility)
: DEFAULT_VALIDITY,
// A denied decision (possibility 0) must not leak the
// relation's reliability — the rule-collection path zeroes it.
reliability: directRel.possibility > 0
@@ -344,6 +348,7 @@ export class AuthorizationChecker {
const finalResult = {
possibility: resPossibility || 0,
reliability: res.reliability !== undefined ? res.reliability : 1.0,
validity: res.validity || DEFAULT_VALIDITY,
...(collectValues && res.collectedValues && Array.isArray(res.collectedValues) && { collectedValues: res.collectedValues }),
...(includeMeta && {
meta: {
@@ -373,6 +378,8 @@ export class AuthorizationChecker {
let bestDenyReliability = 0;
let bestAllow = null;
let bestDeny = null;
let bestAllowValidity = null;
let ruleValidityBlocks = [];
let reason = undefined;
let allRuleResults = shouldTrackEvaluation ? [] : null;
let allCollectedValues = collectValues ? [] : null; // Collect values from all evaluated rules
@@ -457,7 +464,9 @@ export class AuthorizationChecker {
maxAllow = resAllowPossibility;
bestAllow = resMeta;
bestAllowReliability = res.reliability !== undefined ? res.reliability : 1.0;
bestAllowValidity = res.validity || null;
}
if (res.validity) ruleValidityBlocks.push(res.validity);
if (resDenyPossibility > maxDeny) {
maxDeny = resDenyPossibility;
@@ -543,9 +552,21 @@ export class AuthorizationChecker {
const remediation = maxAllow === 0
? buildRemediation(remediationOptions.length > 0 ? { status: 'required', options: remediationOptions } : null, { status: 'required' })
: null;
const result = {
// Possibilistic validity of the decision: the winning rule's block when
// a single rule drove it, otherwise the weakest-merge of all
// contributors (a heuristic/banned contributor downgrades the whole).
let finalValidity = DEFAULT_VALIDITY;
if (ruleValidityBlocks.length === 1) {
finalValidity = ruleValidityBlocks[0];
} else if (ruleValidityBlocks.length > 1) {
finalValidity = mergeValidity(ruleValidityBlocks);
} else if (maxAllow > 0 && bestAllowValidity) {
finalValidity = bestAllowValidity;
}
const result = {
possibility: maxAllow,
reliability: maxAllow > 0 ? bestAllowReliability : maxDeny > 0 ? bestDenyReliability : 0,
validity: finalValidity,
...(includeMeta && {
meta: {
allow: bestAllow,