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:
@@ -1,3 +1,4 @@
|
|||||||
|
import { buildValidity, normalizeValidity, mergeValidity, DEFAULT_VALIDITY } from '../core/validity.js';
|
||||||
import { Arbiter } from '../core/Arbiter.js';
|
import { Arbiter } from '../core/Arbiter.js';
|
||||||
import { RuleEvaluator } from './RuleEvaluator.js';
|
import { RuleEvaluator } from './RuleEvaluator.js';
|
||||||
import { RuleCollector } from './RuleCollector.js';
|
import { RuleCollector } from './RuleCollector.js';
|
||||||
@@ -162,6 +163,9 @@ export class AuthorizationChecker {
|
|||||||
} else {
|
} else {
|
||||||
result = {
|
result = {
|
||||||
possibility: directRel.possibility,
|
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
|
// A denied decision (possibility 0) must not leak the
|
||||||
// relation's reliability — the rule-collection path zeroes it.
|
// relation's reliability — the rule-collection path zeroes it.
|
||||||
reliability: directRel.possibility > 0
|
reliability: directRel.possibility > 0
|
||||||
@@ -344,6 +348,7 @@ export class AuthorizationChecker {
|
|||||||
const finalResult = {
|
const finalResult = {
|
||||||
possibility: resPossibility || 0,
|
possibility: resPossibility || 0,
|
||||||
reliability: res.reliability !== undefined ? res.reliability : 1.0,
|
reliability: res.reliability !== undefined ? res.reliability : 1.0,
|
||||||
|
validity: res.validity || DEFAULT_VALIDITY,
|
||||||
...(collectValues && res.collectedValues && Array.isArray(res.collectedValues) && { collectedValues: res.collectedValues }),
|
...(collectValues && res.collectedValues && Array.isArray(res.collectedValues) && { collectedValues: res.collectedValues }),
|
||||||
...(includeMeta && {
|
...(includeMeta && {
|
||||||
meta: {
|
meta: {
|
||||||
@@ -373,6 +378,8 @@ export class AuthorizationChecker {
|
|||||||
let bestDenyReliability = 0;
|
let bestDenyReliability = 0;
|
||||||
let bestAllow = null;
|
let bestAllow = null;
|
||||||
let bestDeny = null;
|
let bestDeny = null;
|
||||||
|
let bestAllowValidity = null;
|
||||||
|
let ruleValidityBlocks = [];
|
||||||
let reason = undefined;
|
let reason = undefined;
|
||||||
let allRuleResults = shouldTrackEvaluation ? [] : null;
|
let allRuleResults = shouldTrackEvaluation ? [] : null;
|
||||||
let allCollectedValues = collectValues ? [] : null; // Collect values from all evaluated rules
|
let allCollectedValues = collectValues ? [] : null; // Collect values from all evaluated rules
|
||||||
@@ -457,7 +464,9 @@ export class AuthorizationChecker {
|
|||||||
maxAllow = resAllowPossibility;
|
maxAllow = resAllowPossibility;
|
||||||
bestAllow = resMeta;
|
bestAllow = resMeta;
|
||||||
bestAllowReliability = res.reliability !== undefined ? res.reliability : 1.0;
|
bestAllowReliability = res.reliability !== undefined ? res.reliability : 1.0;
|
||||||
|
bestAllowValidity = res.validity || null;
|
||||||
}
|
}
|
||||||
|
if (res.validity) ruleValidityBlocks.push(res.validity);
|
||||||
|
|
||||||
if (resDenyPossibility > maxDeny) {
|
if (resDenyPossibility > maxDeny) {
|
||||||
maxDeny = resDenyPossibility;
|
maxDeny = resDenyPossibility;
|
||||||
@@ -543,9 +552,21 @@ export class AuthorizationChecker {
|
|||||||
const remediation = maxAllow === 0
|
const remediation = maxAllow === 0
|
||||||
? buildRemediation(remediationOptions.length > 0 ? { status: 'required', options: remediationOptions } : null, { status: 'required' })
|
? buildRemediation(remediationOptions.length > 0 ? { status: 'required', options: remediationOptions } : null, { status: 'required' })
|
||||||
: null;
|
: 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,
|
possibility: maxAllow,
|
||||||
reliability: maxAllow > 0 ? bestAllowReliability : maxDeny > 0 ? bestDenyReliability : 0,
|
reliability: maxAllow > 0 ? bestAllowReliability : maxDeny > 0 ? bestDenyReliability : 0,
|
||||||
|
validity: finalValidity,
|
||||||
...(includeMeta && {
|
...(includeMeta && {
|
||||||
meta: {
|
meta: {
|
||||||
allow: bestAllow,
|
allow: bestAllow,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { buildValidity, normalizeValidity, DEFAULT_VALIDITY } from '../core/validity.js';
|
||||||
import { OWAFusion, getOWAWeightsFromRule } from '../utils/OWAFusion.js';
|
import { OWAFusion, getOWAWeightsFromRule } from '../utils/OWAFusion.js';
|
||||||
import { buildRemediation, extractRemediation, mergeRemediationOptions } from './remediation.js';
|
import { buildRemediation, extractRemediation, mergeRemediationOptions } from './remediation.js';
|
||||||
|
|
||||||
@@ -143,6 +144,9 @@ export class CompiledEvaluator {
|
|||||||
const result = {
|
const result = {
|
||||||
possibility: relationStrength,
|
possibility: relationStrength,
|
||||||
reliability: directRel.reliability !== undefined ? directRel.reliability : 1.0,
|
reliability: directRel.reliability !== undefined ? directRel.reliability : 1.0,
|
||||||
|
validity: directRel.validity !== undefined
|
||||||
|
? buildValidity('identity', [relName], [normalizeValidity(directRel.validity)], 1, relationStrength)
|
||||||
|
: DEFAULT_VALIDITY,
|
||||||
possibility_allow: relationStrength,
|
possibility_allow: relationStrength,
|
||||||
possibility_deny: 0,
|
possibility_deny: 0,
|
||||||
...(includeMeta && {
|
...(includeMeta && {
|
||||||
@@ -407,6 +411,7 @@ export class CompiledEvaluator {
|
|||||||
const possibilities = [];
|
const possibilities = [];
|
||||||
const reliabilities = [];
|
const reliabilities = [];
|
||||||
const metas = [];
|
const metas = [];
|
||||||
|
const resChildrenValidity = [];
|
||||||
const remediationOptions = [];
|
const remediationOptions = [];
|
||||||
const allCollectedValues = collectValues ? [] : null;
|
const allCollectedValues = collectValues ? [] : null;
|
||||||
|
|
||||||
@@ -416,6 +421,7 @@ export class CompiledEvaluator {
|
|||||||
if (!res || typeof res.possibility !== 'number') {
|
if (!res || typeof res.possibility !== 'number') {
|
||||||
res = { possibility: 0, meta: { reason: 'missing_rule' }, collectedValues: [] };
|
res = { possibility: 0, meta: { reason: 'missing_rule' }, collectedValues: [] };
|
||||||
}
|
}
|
||||||
|
if (res.validity) resChildrenValidity.push(res.validity);
|
||||||
|
|
||||||
mergeRemediationOptions(remediationOptions, extractRemediation(res));
|
mergeRemediationOptions(remediationOptions, extractRemediation(res));
|
||||||
|
|
||||||
@@ -477,9 +483,15 @@ export class CompiledEvaluator {
|
|||||||
const maxP = Math.max(...possibilities);
|
const maxP = Math.max(...possibilities);
|
||||||
unionReliability = reliabilities[possibilities.indexOf(maxP)] ?? 1.0;
|
unionReliability = reliabilities[possibilities.indexOf(maxP)] ?? 1.0;
|
||||||
}
|
}
|
||||||
|
const unionOperator = (compiled.aggregator || 'max') === 'max' && (!compiled.owaWeights || (compiled.owaWeights.length <= 1 || (compiled.owaWeights[0] === 1 && compiled.owaWeights.slice(1).every(w => w === 0))))
|
||||||
|
? 'max' : 'owa';
|
||||||
|
const unionSourceLabels = (resChildrenValidity || []).map(v => v?.label).filter(Boolean);
|
||||||
|
const unionSources = [];
|
||||||
|
for (const v of resChildrenValidity || []) for (const s of v?.sources || []) if (!unionSources.includes(s)) unionSources.push(s);
|
||||||
return {
|
return {
|
||||||
possibility: result.value,
|
possibility: result.value,
|
||||||
reliability: unionReliability,
|
reliability: unionReliability,
|
||||||
|
validity: buildValidity(unionOperator, unionSources, unionSourceLabels, possibilities.length, result.value),
|
||||||
...(collectValues && { collectedValues: allCollectedValues }),
|
...(collectValues && { collectedValues: allCollectedValues }),
|
||||||
...(includeMeta && {
|
...(includeMeta && {
|
||||||
meta: {
|
meta: {
|
||||||
@@ -523,10 +535,12 @@ export class CompiledEvaluator {
|
|||||||
// (no children); treat it as a one-element conjunction, matching the
|
// (no children); treat it as a one-element conjunction, matching the
|
||||||
// union path's `compiled.children || [compiled]` fallback.
|
// union path's `compiled.children || [compiled]` fallback.
|
||||||
const children = compiled.children || [compiled];
|
const children = compiled.children || [compiled];
|
||||||
|
const resChildrenValidity = [];
|
||||||
|
|
||||||
for (const child of children) {
|
for (const child of children) {
|
||||||
const childVisited = new Set(visited);
|
const childVisited = new Set(visited);
|
||||||
const res = this.evaluate(child, userId, userKey, objectId, objectKey, childVisited, currentRelation, options);
|
const res = this.evaluate(child, userId, userKey, objectId, objectKey, childVisited, currentRelation, options);
|
||||||
|
if (res.validity) resChildrenValidity.push(res.validity);
|
||||||
possibilities.push(res.possibility);
|
possibilities.push(res.possibility);
|
||||||
reliabilities.push(res.reliability !== undefined ? res.reliability : 1.0);
|
reliabilities.push(res.reliability !== undefined ? res.reliability : 1.0);
|
||||||
metas.push(includeMeta ? res.meta : null);
|
metas.push(includeMeta ? res.meta : null);
|
||||||
@@ -587,9 +601,13 @@ export class CompiledEvaluator {
|
|||||||
const minP = Math.min(...possibilities);
|
const minP = Math.min(...possibilities);
|
||||||
intersectionReliability = reliabilities[possibilities.indexOf(minP)] ?? 1.0;
|
intersectionReliability = reliabilities[possibilities.indexOf(minP)] ?? 1.0;
|
||||||
}
|
}
|
||||||
|
const interSourceLabels = (resChildrenValidity || []).map(v => v?.label).filter(Boolean);
|
||||||
|
const interSources = [];
|
||||||
|
for (const v of resChildrenValidity || []) for (const s of v?.sources || []) if (!interSources.includes(s)) interSources.push(s);
|
||||||
return {
|
return {
|
||||||
possibility: result.value,
|
possibility: result.value,
|
||||||
reliability: intersectionReliability,
|
reliability: intersectionReliability,
|
||||||
|
validity: buildValidity('min', interSources, interSourceLabels, possibilities.length, result.value),
|
||||||
...(collectValues && { collectedValues: allCollectedValues }),
|
...(collectValues && { collectedValues: allCollectedValues }),
|
||||||
...(includeMeta && {
|
...(includeMeta && {
|
||||||
meta: {
|
meta: {
|
||||||
@@ -668,6 +686,7 @@ export class CompiledEvaluator {
|
|||||||
// Both legs contribute to the decision, so their reliabilities
|
// Both legs contribute to the decision, so their reliabilities
|
||||||
// multiply (consistent with TTU/chain path semantics).
|
// multiply (consistent with TTU/chain path semantics).
|
||||||
reliability: (a.reliability !== undefined ? a.reliability : 1.0) * (b.reliability !== undefined ? b.reliability : 1.0),
|
reliability: (a.reliability !== undefined ? a.reliability : 1.0) * (b.reliability !== undefined ? b.reliability : 1.0),
|
||||||
|
validity: buildValidity('product', [a.validity?.sources?.[0], b.validity?.sources?.[0]].filter(Boolean), [a.validity?.label, b.validity?.label].filter(Boolean), 2, possibility),
|
||||||
...(collectValues && { collectedValues: allCollectedValues }),
|
...(collectValues && { collectedValues: allCollectedValues }),
|
||||||
...(includeMeta && {
|
...(includeMeta && {
|
||||||
meta: {
|
meta: {
|
||||||
@@ -698,6 +717,7 @@ export class CompiledEvaluator {
|
|||||||
const possibilities = [];
|
const possibilities = [];
|
||||||
const reliabilities = [];
|
const reliabilities = [];
|
||||||
const metas = [];
|
const metas = [];
|
||||||
|
const listChildrenValidity = [];
|
||||||
const allCollectedValues = collectValues ? [] : null;
|
const allCollectedValues = collectValues ? [] : null;
|
||||||
const directRules = compiled._optimized.direct;
|
const directRules = compiled._optimized.direct;
|
||||||
|
|
||||||
@@ -712,6 +732,9 @@ export class CompiledEvaluator {
|
|||||||
const possibility = directRel ? directRel.possibility : 0;
|
const possibility = directRel ? directRel.possibility : 0;
|
||||||
possibilities.push(possibility);
|
possibilities.push(possibility);
|
||||||
reliabilities.push(directRel ? (directRel.reliability !== undefined ? directRel.reliability : 1.0) : 1.0);
|
reliabilities.push(directRel ? (directRel.reliability !== undefined ? directRel.reliability : 1.0) : 1.0);
|
||||||
|
if (directRel) {
|
||||||
|
listChildrenValidity.push(buildValidity('identity', [direct.relation], [normalizeValidity(directRel.validity !== undefined ? directRel.validity : null)], 1, possibility));
|
||||||
|
}
|
||||||
if (includeMeta) {
|
if (includeMeta) {
|
||||||
const _src = directRel ? (directRel.source || 'persistent') : null;
|
const _src = directRel ? (directRel.source || 'persistent') : null;
|
||||||
const _allowMeta = directRel ? {
|
const _allowMeta = directRel ? {
|
||||||
@@ -808,9 +831,14 @@ export class CompiledEvaluator {
|
|||||||
listReliability = reliabilities[possibilities.indexOf(maxP)] ?? 1.0;
|
listReliability = reliabilities[possibilities.indexOf(maxP)] ?? 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const listOperator = aggregator === 'min' ? 'min' : (aggregator === 'max' ? 'max' : 'owa');
|
||||||
|
const listLabels = listChildrenValidity.map(v => v.label).filter(Boolean);
|
||||||
|
const listSources = [];
|
||||||
|
for (const v of listChildrenValidity) for (const s of v.sources || []) if (!listSources.includes(s)) listSources.push(s);
|
||||||
return {
|
return {
|
||||||
possibility: result.value,
|
possibility: result.value,
|
||||||
reliability: listReliability,
|
reliability: listReliability,
|
||||||
|
validity: buildValidity(listOperator, listSources, listLabels, possibilities.length, result.value),
|
||||||
...(collectValues && { collectedValues: allCollectedValues }),
|
...(collectValues && { collectedValues: allCollectedValues }),
|
||||||
...(includeMeta && {
|
...(includeMeta && {
|
||||||
meta: {
|
meta: {
|
||||||
|
|||||||
@@ -140,14 +140,17 @@ export class RuleCompiler {
|
|||||||
type: 'logical',
|
type: 'logical',
|
||||||
op: logicalKey,
|
op: logicalKey,
|
||||||
children,
|
children,
|
||||||
aggregator: logicalConfig?.aggregator,
|
// The array form ({ union: [a, b], aggregator: 'average' }) carries the
|
||||||
owaWeights: logicalConfig?.owaWeights,
|
// operator options on the TOP-level config; the object form carries
|
||||||
_owaSparseCount: logicalConfig?._owaSparseCount,
|
// them on the union object itself. Honor both.
|
||||||
reliabilityWeighting: logicalConfig?.reliabilityWeighting || false,
|
aggregator: logicalConfig?.aggregator ?? config.aggregator,
|
||||||
useBilattice: logicalConfig?.useBilattice || false,
|
owaWeights: logicalConfig?.owaWeights ?? config.owaWeights,
|
||||||
capacityType: logicalConfig?.capacityType,
|
_owaSparseCount: logicalConfig?._owaSparseCount ?? config._owaSparseCount,
|
||||||
epistemicMode: logicalConfig?.epistemicMode,
|
reliabilityWeighting: logicalConfig?.reliabilityWeighting ?? config.reliabilityWeighting ?? false,
|
||||||
negate: logicalConfig?.negate || false
|
useBilattice: logicalConfig?.useBilattice ?? config.useBilattice ?? false,
|
||||||
|
capacityType: logicalConfig?.capacityType ?? config.capacityType,
|
||||||
|
epistemicMode: logicalConfig?.epistemicMode ?? config.epistemicMode,
|
||||||
|
negate: logicalConfig?.negate ?? config.negate ?? false
|
||||||
};
|
};
|
||||||
|
|
||||||
return compiled;
|
return compiled;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { OWAFusion } from '../../utils/OWAFusion.js';
|
import { OWAFusion } from '../../utils/OWAFusion.js';
|
||||||
|
import { buildValidity, normalizeValidity, DEFAULT_VALIDITY } from '../../core/validity.js';
|
||||||
import { BilatticeOrderings } from '../../qualitative/BilatticeOrderings.js';
|
import { BilatticeOrderings } from '../../qualitative/BilatticeOrderings.js';
|
||||||
import { QualitativeCapacity } from '../../qualitative/QualitativeCapacity.js';
|
import { QualitativeCapacity } from '../../qualitative/QualitativeCapacity.js';
|
||||||
import { QualitativeScale } from '../../qualitative/QualitativeScale.js';
|
import { QualitativeScale } from '../../qualitative/QualitativeScale.js';
|
||||||
@@ -134,11 +135,24 @@ export class BaseRule {
|
|||||||
* Create standardized rule result with collected values
|
* Create standardized rule result with collected values
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
|
_defaultValidity() {
|
||||||
|
return DEFAULT_VALIDITY;
|
||||||
|
}
|
||||||
|
|
||||||
|
_validity(operator, sources = [], sourceLabels = [], K = 1, possibility = 0) {
|
||||||
|
return buildValidity({ operator, sources, sourceLabels, K, possibility });
|
||||||
|
}
|
||||||
|
|
||||||
|
_relationValidity(rel) {
|
||||||
|
return normalizeValidity(rel && rel.validity !== undefined ? rel.validity : null);
|
||||||
|
}
|
||||||
|
|
||||||
_createStandardResult(authResult, collectedValues = []) {
|
_createStandardResult(authResult, collectedValues = []) {
|
||||||
const result = {
|
const result = {
|
||||||
// AUTHORIZATION
|
// AUTHORIZATION
|
||||||
possibility: authResult.possibility || 0,
|
possibility: authResult.possibility || 0,
|
||||||
reliability: authResult.reliability !== undefined ? authResult.reliability : 1.0,
|
reliability: authResult.reliability !== undefined ? authResult.reliability : 1.0,
|
||||||
|
validity: authResult.validity || DEFAULT_VALIDITY,
|
||||||
|
|
||||||
// BINARY MODE FIELDS
|
// BINARY MODE FIELDS
|
||||||
possibility_allow: authResult.possibility_allow !== undefined ? authResult.possibility_allow : (authResult.possibility || 0),
|
possibility_allow: authResult.possibility_allow !== undefined ? authResult.possibility_allow : (authResult.possibility || 0),
|
||||||
|
|||||||
@@ -350,6 +350,7 @@ export class ChainRule extends BaseRule {
|
|||||||
const authResult = {
|
const authResult = {
|
||||||
possibility: finalPossibility,
|
possibility: finalPossibility,
|
||||||
reliability: finalReliability,
|
reliability: finalReliability,
|
||||||
|
validity: this._validity('min', normalizedRule.steps.map(s => s.relation).filter(Boolean), [], normalizedRule.steps.length, finalPossibility),
|
||||||
...(includeMeta && {
|
...(includeMeta && {
|
||||||
meta: finalPossibility > 0 ? {
|
meta: finalPossibility > 0 ? {
|
||||||
ruleType: 'chain',
|
ruleType: 'chain',
|
||||||
|
|||||||
@@ -71,6 +71,9 @@ export class DirectRule extends BaseRule {
|
|||||||
const authResult = {
|
const authResult = {
|
||||||
possibility: relationStrength,
|
possibility: relationStrength,
|
||||||
reliability: directRel.reliability !== undefined ? directRel.reliability : 1.0,
|
reliability: directRel.reliability !== undefined ? directRel.reliability : 1.0,
|
||||||
|
validity: directRel.validity !== undefined
|
||||||
|
? this._validity('identity', [relName], [this._relationValidity(directRel)], 1, relationStrength)
|
||||||
|
: this._defaultValidity(),
|
||||||
possibility_allow: relationStrength, // For binary mode
|
possibility_allow: relationStrength, // For binary mode
|
||||||
possibility_deny: 0, // DirectRule doesn't deny
|
possibility_deny: 0, // DirectRule doesn't deny
|
||||||
...(includeMeta && {
|
...(includeMeta && {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { BaseRule } from './BaseRule.js';
|
import { BaseRule } from './BaseRule.js';
|
||||||
import { OWAFusion, getOWAWeightsFromRule } from '../../utils/OWAFusion.js';
|
import { OWAFusion, getOWAWeightsFromRule } from '../../utils/OWAFusion.js';
|
||||||
|
import { buildValidity } from '../../core/validity.js';
|
||||||
import { Arbiter } from '../../core/Arbiter.js';
|
import { Arbiter } from '../../core/Arbiter.js';
|
||||||
import { BilatticeOrderings } from '../../qualitative/BilatticeOrderings.js';
|
import { BilatticeOrderings } from '../../qualitative/BilatticeOrderings.js';
|
||||||
import { QualitativeCapacity } from '../../qualitative/QualitativeCapacity.js';
|
import { QualitativeCapacity } from '../../qualitative/QualitativeCapacity.js';
|
||||||
@@ -859,6 +860,7 @@ export class LogicalOperators extends BaseRule {
|
|||||||
return {
|
return {
|
||||||
possibility: Math.max(0, Math.min(1, possibility)),
|
possibility: Math.max(0, Math.min(1, possibility)),
|
||||||
reliability,
|
reliability,
|
||||||
|
validity: buildValidity('product', [], [], 2, Math.max(0, Math.min(1, possibility))),
|
||||||
collectedValues: allCollectedValues,
|
collectedValues: allCollectedValues,
|
||||||
meta: {
|
meta: {
|
||||||
...resultMeta,
|
...resultMeta,
|
||||||
|
|||||||
@@ -217,6 +217,7 @@ export class MultiHopRule extends BaseRule {
|
|||||||
const authResult = {
|
const authResult = {
|
||||||
possibility: finalPossibility,
|
possibility: finalPossibility,
|
||||||
reliability: finalReliability,
|
reliability: finalReliability,
|
||||||
|
validity: this._validity('min', [relation], [], Math.max(1, bestPath ? bestPath.hops : 1), finalPossibility),
|
||||||
...(includeMeta && {
|
...(includeMeta && {
|
||||||
meta: allowMeta
|
meta: allowMeta
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ export class RelationalComparatorRule extends BaseRule {
|
|||||||
else comparisonOutput.meta = { fullEvaluationTrace: evaluationMeta };
|
else comparisonOutput.meta = { fullEvaluationTrace: evaluationMeta };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
comparisonOutput.validity = this._validity('identity',
|
||||||
|
[leftOperand?.rule?.relation, rightOperand?.rule?.relation].filter(Boolean), [], 1, comparisonOutput.possibility || 0);
|
||||||
return comparisonOutput;
|
return comparisonOutput;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -500,6 +500,7 @@ export class TupleToUsersetRule extends BaseRule {
|
|||||||
return {
|
return {
|
||||||
possibility: maxPossibility,
|
possibility: maxPossibility,
|
||||||
reliability: maxReliability,
|
reliability: maxReliability,
|
||||||
|
validity: this._validity('min', [(ruleMetaBase && ruleMetaBase.tuplesetRelation) || null, (ruleMetaBase && ruleMetaBase.computedRelation) || null].filter(Boolean), [], 2, maxPossibility),
|
||||||
reason: reasons.includes('cycle') ? 'cycle' : (maxPossibility > 0 ? 'tuple_to_userset_found' : 'no_sufficient_tuple_to_userset_path')
|
reason: reasons.includes('cycle') ? 'cycle' : (maxPossibility > 0 ? 'tuple_to_userset_found' : 'no_sufficient_tuple_to_userset_path')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -565,6 +566,7 @@ export class TupleToUsersetRule extends BaseRule {
|
|||||||
return {
|
return {
|
||||||
possibility: fusionResult.value,
|
possibility: fusionResult.value,
|
||||||
reliability: fusedReliability,
|
reliability: fusedReliability,
|
||||||
|
validity: this._validity('min', [(ruleMetaBase && ruleMetaBase.tuplesetRelation) || null, (ruleMetaBase && ruleMetaBase.computedRelation) || null].filter(Boolean), [], 2, fusionResult.value),
|
||||||
...(includeMeta && { meta: finalMeta }),
|
...(includeMeta && { meta: finalMeta }),
|
||||||
...(collectValues && { collectedValues: fusionResult.value > 0 ? collectedValues : [] }),
|
...(collectValues && { collectedValues: fusionResult.value > 0 ? collectedValues : [] }),
|
||||||
reason: reasons.includes('cycle') ? 'cycle' : (fusionResult.value > 0 ? 'tuple_to_userset_found' : 'no_sufficient_tuple_to_userset_path')
|
reason: reasons.includes('cycle') ? 'cycle' : (fusionResult.value > 0 ? 'tuple_to_userset_found' : 'no_sufficient_tuple_to_userset_path')
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ export class RelationUpdates {
|
|||||||
decayConfig: options?.decayConfig,
|
decayConfig: options?.decayConfig,
|
||||||
updated_last_at: currentTime,
|
updated_last_at: currentTime,
|
||||||
changed_last_at: options?.changed_last_at !== undefined ? options.changed_last_at : currentTime,
|
changed_last_at: options?.changed_last_at !== undefined ? options.changed_last_at : currentTime,
|
||||||
stateId: newStateId
|
stateId: newStateId,
|
||||||
|
validity: options?.validity !== undefined ? options.validity : null
|
||||||
};
|
};
|
||||||
|
|
||||||
this.manager.arbiter.relations.push(relationObj);
|
this.manager.arbiter.relations.push(relationObj);
|
||||||
@@ -599,6 +600,7 @@ export class RelationUpdates {
|
|||||||
oldRelation.reliability = newOptions?.reliability !== undefined ? newOptions.reliability : oldRelation.reliability;
|
oldRelation.reliability = newOptions?.reliability !== undefined ? newOptions.reliability : oldRelation.reliability;
|
||||||
oldRelation.value = newOptions?.value !== undefined ? newOptions.value : oldRelation.value;
|
oldRelation.value = newOptions?.value !== undefined ? newOptions.value : oldRelation.value;
|
||||||
oldRelation.decayConfig = newOptions?.decayConfig !== undefined ? newOptions.decayConfig : oldRelation.decayConfig;
|
oldRelation.decayConfig = newOptions?.decayConfig !== undefined ? newOptions.decayConfig : oldRelation.decayConfig;
|
||||||
|
oldRelation.validity = newOptions?.validity !== undefined ? newOptions.validity : oldRelation.validity;
|
||||||
oldRelation.updated_last_at = currentTime;
|
oldRelation.updated_last_at = currentTime;
|
||||||
// Honor an explicit changed_last_at override like the add path does,
|
// Honor an explicit changed_last_at override like the add path does,
|
||||||
// but only on writes that would refresh anyway: a write that does not
|
// but only on writes that would refresh anyway: a write that does not
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
/**
|
||||||
|
* src/core/validity.js — possibilistic validity metadata (Cella FVN / Martin).
|
||||||
|
*
|
||||||
|
* Every check result carries a validity block describing how the decision
|
||||||
|
* value was produced, in the spirit of the zig-contour fusion spec:
|
||||||
|
*
|
||||||
|
* label — weakest validity label of the contributing sources, downgraded
|
||||||
|
* by the fusion operator's class:
|
||||||
|
* identity/max : operator is valid (max is already valid
|
||||||
|
* and normalized under arbitrary dependence);
|
||||||
|
* the fused label = weakest source label
|
||||||
|
* min (conjunctive) : unvalidified ranking -> the raw min is
|
||||||
|
* reported as the decision, the validified
|
||||||
|
* value min(1, K*gamma_min) is exposed
|
||||||
|
* separately, and the conflict mass is
|
||||||
|
* surfaced, never silently dropped
|
||||||
|
* product-style : exclusion P(A)*(1-P(B)) and defeasible
|
||||||
|
* when*(1-unless) use the product operator,
|
||||||
|
* which has no linear validification under
|
||||||
|
* arbitrary dependence (Cella Thm 4) ->
|
||||||
|
* always heuristic
|
||||||
|
* owa (interior) : convex mixtures of plausibilities are not
|
||||||
|
* maxitive -> scoring heuristic, never a
|
||||||
|
* valid possibility
|
||||||
|
* regime — dependence assumption (always 'arbitrary' unless declared)
|
||||||
|
* operator — the fusion operator actually applied
|
||||||
|
* conflictMass — 1 - fused_possibility: the conjunctive suppression (the
|
||||||
|
* scalar analog of contour sub-normalization; 0 for disjunctive)
|
||||||
|
* validifiedPossibility — the arbitrary-regime validification
|
||||||
|
* min(1, K*gamma_min) for conjunctive results (null otherwise)
|
||||||
|
* sources — the base relations that fed the decision
|
||||||
|
* nonMaxitive — true for interior OWA/averaging operators
|
||||||
|
*
|
||||||
|
* The taxonomy follows the contour spec: finite_sample | anytime | conformal |
|
||||||
|
* approximate | heuristic | unknown. Unlabeled relations default to heuristic.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const VALIDITY_LABELS = ['finite_sample', 'anytime', 'conformal', 'approximate', 'heuristic', 'unknown'];
|
||||||
|
|
||||||
|
// Shared immutable default for unlabeled decisions (all-heuristic): the
|
||||||
|
// hottest paths attach this constant instead of allocating per check.
|
||||||
|
export const DEFAULT_VALIDITY = Object.freeze({
|
||||||
|
label: 'heuristic',
|
||||||
|
operator: 'identity',
|
||||||
|
regime: 'arbitrary',
|
||||||
|
sources: [],
|
||||||
|
conflictMass: 0,
|
||||||
|
validifiedPossibility: null,
|
||||||
|
nonMaxitive: false
|
||||||
|
});
|
||||||
|
export const VALIDITY_RANK = { finite_sample: 0, anytime: 1, conformal: 2, approximate: 3, heuristic: 4, unknown: 5 };
|
||||||
|
|
||||||
|
export function normalizeValidity(label) {
|
||||||
|
if (label === undefined || label === null) return 'heuristic';
|
||||||
|
const l = String(label);
|
||||||
|
return VALIDITY_RANK[l] !== undefined ? l : 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Weakest (least valid) of a set of labels. */
|
||||||
|
export function weakestValidity(labels) {
|
||||||
|
// Track the weakest (highest-rank) label; every label ranks >= 0.
|
||||||
|
let worst = 'finite_sample';
|
||||||
|
for (const l of labels) {
|
||||||
|
const n = normalizeValidity(l);
|
||||||
|
if (VALIDITY_RANK[n] > VALIDITY_RANK[worst]) worst = n;
|
||||||
|
}
|
||||||
|
return worst;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operator validity classes:
|
||||||
|
* 'identity' — a single source; fused label = source label
|
||||||
|
* 'max' — disjunctive; valid by construction, label = weakest source
|
||||||
|
* 'min' — conjunctive; unvalidified ranking; label downgraded to
|
||||||
|
* approximate unless already weaker
|
||||||
|
* 'product' — banned operator (Thm 4); always heuristic
|
||||||
|
* 'owa' — interior OWA averaging; non-maxitive; always heuristic
|
||||||
|
*/
|
||||||
|
export function operatorValidityClass(operator) {
|
||||||
|
switch (operator) {
|
||||||
|
case 'identity':
|
||||||
|
case 'max':
|
||||||
|
return { downgrade: false, nonMaxitive: false };
|
||||||
|
case 'min':
|
||||||
|
return { downgrade: true, nonMaxitive: false };
|
||||||
|
case 'product':
|
||||||
|
return { downgrade: true, alwaysHeuristic: true, nonMaxitive: false };
|
||||||
|
case 'owa':
|
||||||
|
return { downgrade: true, alwaysHeuristic: true, nonMaxitive: true };
|
||||||
|
default:
|
||||||
|
return { downgrade: true, alwaysHeuristic: true, nonMaxitive: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the validity block for a fused decision.
|
||||||
|
*
|
||||||
|
* @param {object} opts
|
||||||
|
* operator — 'identity' | 'max' | 'min' | 'product' | 'owa'
|
||||||
|
* sources — array of base relation names that fed the decision
|
||||||
|
* sourceLabels — array of validity labels (parallel to sources, or a single
|
||||||
|
* 'heuristic' when the engine does not track them)
|
||||||
|
* K — number of fused sources (for the min validification)
|
||||||
|
* possibility — the raw fused possibility
|
||||||
|
*/
|
||||||
|
export function buildValidity(optsOrOp, sourcesArg = [], sourceLabelsArg = [], KArg = 1, possibilityArg = 0) {
|
||||||
|
// Accept both the object form ({ operator, sources, ... }) and the
|
||||||
|
// positional form (operator, sources, sourceLabels, K, possibility).
|
||||||
|
const opts = (typeof optsOrOp === 'object' && optsOrOp !== null)
|
||||||
|
? optsOrOp
|
||||||
|
: { operator: optsOrOp, sources: sourcesArg, sourceLabels: sourceLabelsArg, K: KArg, possibility: possibilityArg };
|
||||||
|
const { operator = 'identity', sources = [], sourceLabels = [], K = 1, possibility = 0 } = opts;
|
||||||
|
const cls = operatorValidityClass(operator);
|
||||||
|
const base = sourceLabels.length > 0 ? weakestValidity(sourceLabels) : 'heuristic';
|
||||||
|
let label = base;
|
||||||
|
if (cls.downgrade) {
|
||||||
|
if (cls.alwaysHeuristic) {
|
||||||
|
label = 'heuristic';
|
||||||
|
} else {
|
||||||
|
// unvalidified conjunctive ranking: approximate at best
|
||||||
|
if (VALIDITY_RANK[base] < VALIDITY_RANK.approximate) label = 'approximate';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const block = {
|
||||||
|
label,
|
||||||
|
operator,
|
||||||
|
regime: 'arbitrary',
|
||||||
|
sources: [...sources],
|
||||||
|
nonMaxitive: cls.nonMaxitive
|
||||||
|
};
|
||||||
|
if (operator === 'min') {
|
||||||
|
const k = Math.max(1, K);
|
||||||
|
block.conflictMass = Math.max(0, 1 - possibility);
|
||||||
|
block.validifiedPossibility = Math.min(1, k * possibility);
|
||||||
|
} else if (operator === 'identity' || operator === 'max') {
|
||||||
|
block.conflictMass = 0;
|
||||||
|
block.validifiedPossibility = null;
|
||||||
|
} else {
|
||||||
|
block.conflictMass = null;
|
||||||
|
block.validifiedPossibility = null;
|
||||||
|
}
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Merge several rule-level validity blocks into one (weakest label, unioned sources). */
|
||||||
|
export function mergeValidity(blocks) {
|
||||||
|
const present = (blocks || []).filter(Boolean);
|
||||||
|
if (present.length === 0) {
|
||||||
|
return buildValidity({});
|
||||||
|
}
|
||||||
|
const labels = present.map(b => b.label);
|
||||||
|
const sources = [];
|
||||||
|
for (const b of present) {
|
||||||
|
for (const s of b.sources || []) {
|
||||||
|
if (!sources.includes(s)) sources.push(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If ANY contributor is a banned-operator heuristic, the merged result is
|
||||||
|
// heuristic too (the weakest operator wins, not just the weakest source).
|
||||||
|
const anyBanned = present.some(b => b.operator === 'product' || b.nonMaxitive);
|
||||||
|
let label = weakestValidity(labels);
|
||||||
|
if (anyBanned && VALIDITY_RANK[label] < VALIDITY_RANK.heuristic) label = 'heuristic';
|
||||||
|
return {
|
||||||
|
label,
|
||||||
|
operator: present.length === 1 ? present[0].operator : 'mixed',
|
||||||
|
regime: 'arbitrary',
|
||||||
|
sources,
|
||||||
|
nonMaxitive: present.some(b => b.nonMaxitive),
|
||||||
|
conflictMass: present.some(b => b.operator === 'min') ? Math.max(0, 1 - Math.max(...present.map(b => b.conflictMass ?? 0))) : null,
|
||||||
|
validifiedPossibility: null
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -296,7 +296,8 @@ describe('Binary (threshold) mode parity (rigor)', () => {
|
|||||||
|
|
||||||
const inv = report.crucibleVerdict?.invariants?.find(i => i.name === 'binary-normal-agreement');
|
const inv = report.crucibleVerdict?.invariants?.find(i => i.name === 'binary-normal-agreement');
|
||||||
assert.ok(inv, 'invariant missing');
|
assert.ok(inv, 'invariant missing');
|
||||||
assert.equal(inv.passed, true, `binary parity violated in ${inv.failureCount} cases`);
|
assert.equal(inv.passed, true, `binary parity violated in ${inv.failureCount} cases: ` +
|
||||||
|
JSON.stringify((report.failures || []).slice(0, 2).map(f => ({ name: f.name, msg: f.message, seq: (f.sequence || []).map(s => s.args) }))));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('MUTATION FRESHNESS: binary and normal agree after every mutation with caching enabled', async () => {
|
it('MUTATION FRESHNESS: binary and normal agree after every mutation with caching enabled', async () => {
|
||||||
|
|||||||
@@ -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`);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user