contract: pin the canonical check-result shape across all outcome classes

A new rigor contract test (public-contract) asserts every check result —
allowed, denied, missing_node, no_config, cycle, threshold, binary — carries
possibility, reliability, validity, and reason with defined invariants
(possibility/reliability in [0,1], validity labels in the taxonomy, denied
decisions leak zero reliability, JSON round-trip stable).

The pin surfaced three real shape gaps in AuthorizationChecker, now fixed:
the no_relation fast-path miss and binary direct-miss emitted no validity,
and the binary direct-allow path omitted reliability. All structural and
binary return sites now emit the canonical {possibility, reason, reliability,
validity} contract.

Also pins isReachable's boolean|null deferral contract, explain enrichment,
snapshot round-trip outcome preservation, and read-idempotent node queries.
This commit is contained in:
John Dvorak
2026-08-02 11:44:57 -07:00
parent 27d04e4058
commit b6c861a10b
2 changed files with 183 additions and 0 deletions
+27
View File
@@ -108,6 +108,8 @@ export class AuthorizationChecker {
if (userId === undefined || objectId === undefined) {
const result = {
possibility: 0,
reliability: 0,
validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
...(includeMeta && { meta: { reason: 'missing_node' } }),
reason: 'missing_node'
};
@@ -128,6 +130,8 @@ export class AuthorizationChecker {
if (_visited.has(visitKey)) {
return {
possibility: 0,
reliability: 0,
validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
...(includeMeta && { meta: { reason: 'cycle' } }),
reason: 'cycle'
};
@@ -137,6 +141,8 @@ export class AuthorizationChecker {
if (visited.userKey === userKey && visited.relation === relation && visited.objectKey === objectKey) {
return {
possibility: 0,
reliability: 0,
validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
...(includeMeta && { meta: { reason: 'cycle' } }),
reason: 'cycle'
};
@@ -217,6 +223,7 @@ export class AuthorizationChecker {
result = {
possibility: 0,
reliability: 0,
validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
...(includeMeta && { meta: { reason: 'no_relation' } }),
reason: 'no_relation',
...(remediation ? { remediation } : {})
@@ -247,6 +254,8 @@ export class AuthorizationChecker {
return {
possibility: 0,
reliability: 0,
validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
...(includeMeta && {
meta: {
reason: 'missing_node',
@@ -261,6 +270,8 @@ export class AuthorizationChecker {
if (!config) {
return {
possibility: 0,
reliability: 0,
validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
...(includeMeta && { meta: { reason: 'no_config' } }),
reason: 'no_config'
};
@@ -285,6 +296,8 @@ export class AuthorizationChecker {
if (_visited.has(visitKey)) {
return {
possibility: 0,
reliability: 0,
validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
...(includeMeta && { meta: { reason: 'cycle' } }),
reason: 'cycle'
};
@@ -294,6 +307,8 @@ export class AuthorizationChecker {
if (visited.userKey === userKey && visited.relation === relation && visited.objectKey === objectKey) {
return {
possibility: 0,
reliability: 0,
validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
...(includeMeta && { meta: { reason: 'cycle' } }),
reason: 'cycle'
};
@@ -636,6 +651,8 @@ export class AuthorizationChecker {
if (userId === undefined || objectId === undefined) {
return {
possibility: 0,
reliability: 0,
validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
reason: 'missing_node',
binary: true,
...(evaluation && { evaluation })
@@ -649,6 +666,7 @@ export class AuthorizationChecker {
if (_visited.has(visitKey)) {
return {
possibility: 0,
reliability: 0,
validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
reason: 'cycle',
binary: true,
@@ -660,6 +678,7 @@ export class AuthorizationChecker {
if (visited.userKey === userKey && visited.relation === relation && visited.objectKey === objectKey) {
return {
possibility: 0,
reliability: 0,
validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
reason: 'cycle',
binary: true,
@@ -675,6 +694,8 @@ export class AuthorizationChecker {
if (!config) {
return {
possibility: 0,
reliability: 0,
validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
reason: 'no_config',
binary: true,
...(evaluation && { evaluation })
@@ -709,6 +730,9 @@ export class AuthorizationChecker {
return {
possibility: directRel.possibility,
reliability: directRel.possibility > 0
? (directRel.reliability !== undefined ? directRel.reliability : 1.0)
: 0,
validity: includeMeta
? (directRel.validity !== undefined
? buildValidity('identity', [effectiveRelation], [normalizeValidity(directRel.validity)], 1, directRel.possibility)
@@ -731,6 +755,7 @@ export class AuthorizationChecker {
return {
possibility: 0,
reliability: 0,
validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
reason: 'insufficient_confidence',
binary: true,
@@ -849,6 +874,8 @@ export class AuthorizationChecker {
return {
possibility: 0,
reliability: 0,
validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
reason: 'deny',
binary: true,
...(evaluation && { evaluation }),