js-rigor: security affordances — gated provenance, audit hook, DoS hardening, explicit semantics
Per the trust-boundary direction (the caller owns evidence validation):
- Explicit possibilistic semantics module (src/core/possibility.js): the
single authoritative home for what each operator means (max = disjunctive
already-valid; min = unvalidified conjunctive ranking with the K-
validification and surfaced conflict mass; product = Thm-4 heuristic;
interior OWA = non-maxitive heuristic; reliability = adaptation, never
conflated with plausibility).
- Provenance is opt-in (re-entrant tracing practice): default check
results carry only {label, operator, regime}; conflictMass,
validifiedPossibility, sources, and nonMaxitive appear only under
includeMeta and on the explain surface. The direct-check cache now
caches only the meta-less form — includeMeta callers always get a fresh
full evaluation (previously a cached minimal result was served for
includeMeta requests, silently stripping detail).
- Audit affordance: new Arbiter({ audit }) emits one record per check
(decision, possibility, binary, partialGraphUsed, validityLabel,
sources). The engine stores nothing — the caller owns persistence;
zero cost when the hook is absent (and the full validity is forced only
on audit-enabled deployments).
- DoS hardening: partial-graph size limits are enforced BEFORE the
context allocation (the caller-supplied overlay is the per-check
allocation point); the CondensedGraphBinary reader gained full bounds
guards so malformed snapshot buffers fail with clean errors instead of
RangeError crashes or oversized allocations.
- New security-affordance pins: gating, audit records, and pre-allocation
limits.
This commit is contained in:
@@ -33,7 +33,8 @@ function mk() {
|
||||
|
||||
describe('Possibilistic validity metadata (rigor)', () => {
|
||||
it('FIXED: labels, operators, conflict mass, validification per kind', () => {
|
||||
// direct unlabeled -> heuristic identity
|
||||
// direct unlabeled -> heuristic identity; default carries the minimal
|
||||
// public block (label/operator), the full detail is includeMeta-only
|
||||
{
|
||||
const a = mk();
|
||||
a.setRelationConfig('t', { type: 'direct', relation: 'r1' });
|
||||
@@ -41,8 +42,9 @@ describe('Possibilistic validity metadata (rigor)', () => {
|
||||
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);
|
||||
const r2 = a.check('u:0', 't', 'd:0', { includeMeta: true });
|
||||
assert.deepEqual(r2.validity.sources, ['r1']);
|
||||
assert.equal(r2.validity.conflictMass, 0);
|
||||
}
|
||||
// labeled direct propagates its label
|
||||
{
|
||||
@@ -60,7 +62,7 @@ describe('Possibilistic validity metadata (rigor)', () => {
|
||||
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);
|
||||
assert.equal(a.check('u:0', 't', 'd:0', { includeMeta: true }).validity.nonMaxitive, false);
|
||||
}
|
||||
// interior OWA is non-maxitive heuristic
|
||||
{
|
||||
@@ -70,7 +72,7 @@ describe('Possibilistic validity metadata (rigor)', () => {
|
||||
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(a.check('u:0', 't', 'd:0', { includeMeta: true }).validity.nonMaxitive, true);
|
||||
assert.equal(r.validity.label, 'heuristic', 'averaging never claims validity');
|
||||
}
|
||||
// min fusion: approximate, conflict mass, validification
|
||||
@@ -82,8 +84,9 @@ describe('Possibilistic validity metadata (rigor)', () => {
|
||||
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');
|
||||
const rDetail = a.check('u:0', 't', 'd:0', { includeMeta: true });
|
||||
assert.ok(Math.abs(rDetail.validity.conflictMass - (1 - 0.5)) < 1e-9, 'conflict mass = 1 - possibility');
|
||||
assert.equal(rDetail.validity.validifiedPossibility, 1, 'min(1, K*gamma) with K=2, gamma=0.5');
|
||||
}
|
||||
// product operators are heuristic even with labeled sources
|
||||
{
|
||||
@@ -103,7 +106,7 @@ describe('Possibilistic validity metadata (rigor)', () => {
|
||||
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');
|
||||
assert.ok(Math.abs(a.check('u:0', 't', 'd:0', { includeMeta: true }).validity.conflictMass - 0.4) < 1e-9, 'chain conflict mass');
|
||||
}
|
||||
// reliability and validity stay distinct
|
||||
{
|
||||
@@ -143,3 +146,70 @@ describe('Possibilistic validity metadata (rigor)', () => {
|
||||
assert.ok(inv && inv.passed, `validity helpers violated in ${inv?.failureCount} cases`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Security affordances (rigor)', () => {
|
||||
it('FIXED: default results carry only the minimal validity; detail is opt-in', () => {
|
||||
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' });
|
||||
// default: minimal block — no conflict mass, no sources, no validified value
|
||||
const r = a.check('u:0', 't', 'd:0');
|
||||
assert.equal(r.validity.label, 'approximate');
|
||||
assert.equal(r.validity.operator, 'min');
|
||||
assert.ok(!('conflictMass' in r.validity), 'conflict mass is not on the default result');
|
||||
assert.ok(!('validifiedPossibility' in r.validity), 'validified value is not on the default result');
|
||||
assert.ok(!('sources' in r.validity), 'sources are not on the default result');
|
||||
// includeMeta: full debugging detail
|
||||
const r2 = a.check('u:0', 't', 'd:0', { includeMeta: true });
|
||||
assert.equal(r2.validity.conflictMass, 0.5);
|
||||
assert.equal(r2.validity.validifiedPossibility, 1);
|
||||
assert.deepEqual(r2.validity.sources, ['r1', 'r2']);
|
||||
// explain (internal surface) carries the full block
|
||||
const e = a.explain('u:0', 't', 'd:0');
|
||||
assert.equal(e.decision?.validity?.conflictMass, 0.5, 'explain carries full validity');
|
||||
});
|
||||
|
||||
it('FIXED: audit hook emits one record per check; absent by default', () => {
|
||||
const records = [];
|
||||
const a = new Arbiter({ audit: (entry) => records.push(entry) });
|
||||
a.addNode('u:0', 'user');
|
||||
a.addNode('d:0', 'doc');
|
||||
a.setRelationConfig('t', { type: 'direct', relation: 'r1' });
|
||||
a.addRelation('u:0', 'r1', 'd:0', { possibility: 0.8, validity: 'finite_sample' });
|
||||
const r = a.check('u:0', 't', 'd:0');
|
||||
assert.equal(records.length, 1, 'one audit record per check');
|
||||
assert.equal(records[0].decision, 'allow');
|
||||
assert.equal(records[0].possibility, 0.8);
|
||||
assert.equal(records[0].validityLabel, 'finite_sample');
|
||||
assert.deepEqual(records[0].sources, ['r1']);
|
||||
assert.equal(records[0].partialGraphUsed, false);
|
||||
// overlay checks flag partial usage
|
||||
a.check('u:0', 't', 'd:0', { partialGraph: { relations: [{ src: 'u:0', relation: 'r1', dst: 'd:0', possibility: 0.9 }] } });
|
||||
assert.equal(records[1].partialGraphUsed, true);
|
||||
// no hook -> no records, no crash
|
||||
const plain = new Arbiter();
|
||||
plain.addNode('u:0', 'user');
|
||||
plain.addNode('d:0', 'doc');
|
||||
plain.setRelationConfig('t', { type: 'direct', relation: 'r1' });
|
||||
plain.addRelation('u:0', 'r1', 'd:0', { possibility: 0.8 });
|
||||
assert.equal(plain.check('u:0', 't', 'd:0').possibility, 0.8);
|
||||
});
|
||||
|
||||
it('FIXED: oversized partial graphs are rejected before allocation', () => {
|
||||
const a = new Arbiter({ partialGraphPolicy: { maxRelations: 2, maxNodes: 3 } });
|
||||
a.addNode('u:0', 'user');
|
||||
a.addNode('d:0', 'doc');
|
||||
a.setRelationConfig('t', { type: 'direct', relation: 'owner' });
|
||||
assert.throws(
|
||||
() => a.check('u:0', 't', 'd:0', { partialGraph: { relations: [{}, {}, {}, {}] } }),
|
||||
/exceeds max relations/,
|
||||
'relation limit enforced pre-allocation'
|
||||
);
|
||||
assert.throws(
|
||||
() => a.check('u:0', 't', 'd:0', { partialGraph: { nodes: [{}, {}, {}, {}] } }),
|
||||
/exceeds max nodes/,
|
||||
'node limit enforced pre-allocation'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user