Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a875089b65 | |||
| 40fa1f8eb2 | |||
| f6e3ae1922 | |||
| bd9c74fb0e | |||
| ab0e569552 | |||
| ed34df4474 | |||
| 4da3158c63 | |||
| aa4ceff30c | |||
| 41d2547bc8 | |||
| f446750ff3 | |||
| f8f6c5cb1b | |||
| 33dd15bba5 | |||
| 817601a0f3 |
@@ -70,6 +70,10 @@ jobs:
|
|||||||
run: node --expose-gc scripts/benchmark.js --json > bench-results.json
|
run: node --expose-gc scripts/benchmark.js --json > bench-results.json
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Run complex-query benchmarks (cold traffic, normal vs binary)
|
||||||
|
run: node --expose-gc benchmarks/complex-query-bench.js --size=25000 --samples=10000 > complex-query-results.json
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Save baseline (on tag)
|
- name: Save baseline (on tag)
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
run: node --expose-gc scripts/benchmark.js --save
|
run: node --expose-gc scripts/benchmark.js --save
|
||||||
@@ -87,7 +91,9 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: bench-results
|
name: bench-results
|
||||||
path: bench-results.json
|
path: |
|
||||||
|
bench-results.json
|
||||||
|
complex-query-results.json
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
- name: Benchmark summary
|
- name: Benchmark summary
|
||||||
@@ -99,6 +105,12 @@ jobs:
|
|||||||
cat bench-results.json >> $GITHUB_STEP_SUMMARY
|
cat bench-results.json >> $GITHUB_STEP_SUMMARY
|
||||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
fi
|
fi
|
||||||
|
if [ -f complex-query-results.json ]; then
|
||||||
|
echo '### Complex-Query Results (cold traffic, normal vs binary)' >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
cat complex-query-results.json >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
+57
-57
@@ -1,140 +1,140 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"generated": "2026-08-02T19:51:19.208Z",
|
"generated": "2026-08-03T22:37:59.060Z",
|
||||||
"actions": {
|
"actions": {
|
||||||
"check[direct-hit]": {
|
"check[direct-hit]": {
|
||||||
"mostPlausible": 0.25810395981996936,
|
"mostPlausible": 0.10672237227527051,
|
||||||
"alphaCuts": {
|
"alphaCuts": {
|
||||||
"p50": {
|
"p50": {
|
||||||
"lower": 0.16434574601481775,
|
"lower": 0.105663874043709,
|
||||||
"upper": 0.40535024830520205
|
"upper": 0.10779178510155751
|
||||||
},
|
},
|
||||||
"p95": {
|
"p95": {
|
||||||
"lower": 0.10394999999999016,
|
"lower": 0.10367558838055704,
|
||||||
"upper": 1.2158777106838974
|
"upper": 0.10985914641251807
|
||||||
},
|
},
|
||||||
"p99": {
|
"p99": {
|
||||||
"lower": 0.10394999999999016,
|
"lower": 0.10273583797909441,
|
||||||
"upper": 2.040950000000002
|
"upper": 0.11086333384764993
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"check[union-ttu]": {
|
"check[union-ttu]": {
|
||||||
"mostPlausible": 0.3581109933665416,
|
"mostPlausible": 0.11249228961043199,
|
||||||
"alphaCuts": {
|
"alphaCuts": {
|
||||||
"p50": {
|
"p50": {
|
||||||
"lower": 0.07645000000001517,
|
"lower": 0.111604019277894,
|
||||||
"upper": 0.35811139703768785
|
"upper": 0.11338800600185026
|
||||||
},
|
},
|
||||||
"p95": {
|
"p95": {
|
||||||
"lower": 0.07645000000001517,
|
"lower": 0.10993048219120201,
|
||||||
"upper": 0.35811139703768785
|
"upper": 0.11511402952231622
|
||||||
},
|
},
|
||||||
"p99": {
|
"p99": {
|
||||||
"lower": 0.07645000000001517,
|
"lower": 0.1091378967617418,
|
||||||
"upper": 0.35811139703768785
|
"upper": 0.11594970606842417
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"check[denied-miss]": {
|
"check[denied-miss]": {
|
||||||
"mostPlausible": 0.21718442918988012,
|
"mostPlausible": 0.08747524001151907,
|
||||||
"alphaCuts": {
|
"alphaCuts": {
|
||||||
"p50": {
|
"p50": {
|
||||||
"lower": 0.18384889559816292,
|
"lower": 0.08668417849217541,
|
||||||
"upper": 0.2565642603295502
|
"upper": 0.08827353325683636
|
||||||
},
|
},
|
||||||
"p95": {
|
"p95": {
|
||||||
"lower": 0.13274999999997988,
|
"lower": 0.085196295997943,
|
||||||
"upper": 0.3601500000000823
|
"upper": 0.08981478022606769
|
||||||
},
|
},
|
||||||
"p99": {
|
"p99": {
|
||||||
"lower": 0.13274999999997988,
|
"lower": 0.08449268650925873,
|
||||||
"upper": 0.3601500000000823
|
"upper": 0.09056310270952082
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"check[include-meta]": {
|
"check[include-meta]": {
|
||||||
"mostPlausible": 0.9937660048895368,
|
"mostPlausible": 0.17368093893555905,
|
||||||
"alphaCuts": {
|
"alphaCuts": {
|
||||||
"p50": {
|
"p50": {
|
||||||
"lower": 0.9937660048895368,
|
"lower": 0.17235808249234505,
|
||||||
"upper": 0.9937663566453343
|
"upper": 0.17501399072522753
|
||||||
},
|
},
|
||||||
"p95": {
|
"p95": {
|
||||||
"lower": 0.9937660048895368,
|
"lower": 0.1698647154489402,
|
||||||
"upper": 0.9937663566453343
|
"upper": 0.17758260042495247
|
||||||
},
|
},
|
||||||
"p99": {
|
"p99": {
|
||||||
"lower": 0.9937660048895368,
|
"lower": 0.16868343694884977,
|
||||||
"upper": 0.9937663566453343
|
"upper": 0.178826872871734
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"check[overlay-on-top]": {
|
"check[overlay-on-top]": {
|
||||||
"mostPlausible": 0.3485105914857449,
|
"mostPlausible": 0.34658090606556824,
|
||||||
"alphaCuts": {
|
"alphaCuts": {
|
||||||
"p50": {
|
"p50": {
|
||||||
"lower": 0.26044999999998425,
|
"lower": 0.343563635087355,
|
||||||
"upper": 0.3881187132654935
|
"upper": 0.3496248120197711
|
||||||
},
|
},
|
||||||
"p95": {
|
"p95": {
|
||||||
"lower": 0.26044999999998425,
|
"lower": 0.33788494447103923,
|
||||||
"upper": 0.5286391152207875
|
"upper": 0.3555006559358586
|
||||||
},
|
},
|
||||||
"p99": {
|
"p99": {
|
||||||
"lower": 0.26044999999998425,
|
"lower": 0.33519739588838326,
|
||||||
"upper": 0.5374790233000701
|
"upper": 0.35835082770353416
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"check[binary-direct]": {
|
"check[binary-direct]": {
|
||||||
"mostPlausible": 0.2660225524868295,
|
"mostPlausible": 0.10136432040214981,
|
||||||
"alphaCuts": {
|
"alphaCuts": {
|
||||||
"p50": {
|
"p50": {
|
||||||
"lower": 0.21220996174732032,
|
"lower": 0.10038730449515523,
|
||||||
"upper": 0.3334807973509898
|
"upper": 0.10235072528492808
|
||||||
},
|
},
|
||||||
"p95": {
|
"p95": {
|
||||||
"lower": 0.16554999999993186,
|
"lower": 0.0985518364498951,
|
||||||
"upper": 0.5780022226626057
|
"upper": 0.10425698760463886
|
||||||
},
|
},
|
||||||
"p99": {
|
"p99": {
|
||||||
"lower": 0.16554999999993186,
|
"lower": 0.09768455509480933,
|
||||||
"upper": 0.8219500000000078
|
"upper": 0.10518292733500886
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"snapshot[build-binary]": {
|
"snapshot[build-binary]": {
|
||||||
"mostPlausible": 4.119453383803492,
|
"mostPlausible": 2.848037533444555,
|
||||||
"alphaCuts": {
|
"alphaCuts": {
|
||||||
"p50": {
|
"p50": {
|
||||||
"lower": 3.638457178151043,
|
"lower": 2.827413825511722,
|
||||||
"upper": 4.664037328289517
|
"upper": 2.868811889350609
|
||||||
},
|
},
|
||||||
"p95": {
|
"p95": {
|
||||||
"lower": 3.1707499999999906,
|
"lower": 2.788520370277795,
|
||||||
"upper": 6.309350037482752
|
"upper": 2.9088252291455836
|
||||||
},
|
},
|
||||||
"p99": {
|
"p99": {
|
||||||
"lower": 3.1707499999999906,
|
"lower": 2.770077133158435,
|
||||||
"upper": 7.547749999999944
|
"upper": 2.9281936091667653
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"snapshot[restore-binary]": {
|
"snapshot[restore-binary]": {
|
||||||
"mostPlausible": 6.906283652168588,
|
"mostPlausible": 2.9488926124434385,
|
||||||
"alphaCuts": {
|
"alphaCuts": {
|
||||||
"p50": {
|
"p50": {
|
||||||
"lower": 3.2581500000001067,
|
"lower": 2.9247809483999716,
|
||||||
"upper": 6.906283919208314
|
"upper": 2.9732031186198484
|
||||||
},
|
},
|
||||||
"p95": {
|
"p95": {
|
||||||
"lower": 3.2581500000001067,
|
"lower": 2.8793716893148757,
|
||||||
"upper": 6.906283919208314
|
"upper": 3.020092288118393
|
||||||
},
|
},
|
||||||
"p99": {
|
"p99": {
|
||||||
"lower": 3.2581500000001067,
|
"lower": 2.8578657882531426,
|
||||||
"upper": 6.906283919208314
|
"upper": 3.0428172805315117
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# @arbiter/core
|
# @arbiter/core
|
||||||
|
|
||||||
> Possibilistic authorization engine: graph indices, relation/reachability queries, rule evaluation over a DSL, and lossless condensed snapshots.
|
> Possibilistic authorization engine: graph indices, relation/reachability queries, rule evaluation over declarative configurations, and lossless condensed snapshots.
|
||||||
|
|
||||||
|
> The Evidence DSL (a natural-language layer that compiles to these configurations) lives in the separate [`@arbiter/evidence-dsl`](https://hub.kl1.tenere.ai/Arbiter/evidence-dsl) package.
|
||||||
|
|
||||||
## Why
|
## Why
|
||||||
|
|
||||||
@@ -72,23 +74,27 @@ Denied decisions never leak a source's reliability. `includeMeta: true` adds `me
|
|||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
The public surface is the `Arbiter` class:
|
The full reference — every export, method signature, option, result shape, configuration format, error, and reason code — is in [docs/API.md](./docs/API.md). The public surface at a glance:
|
||||||
|
|
||||||
- **Graph**: `addNode`, `addRelation`, `removeRelation`, `setRelationConfig`, `getNodeData`, `resolveNodeId`, `resolveKey`
|
- **Graph**: `addNode`, `addRelation`, `removeRelation`, `setRelationConfig`, `getNodeData`, `resolveNodeId`, `resolveKey`
|
||||||
- **Check**: `check(userKey, relation, objectKey, options)`, `explain` (enriches `meta`), `binary` mode (fast path, marks results `binary: true`)
|
- **Check**: `check(userKey, relation, objectKey, options)`, `explain` (enriches `meta`), `binary` mode (fast path, marks results `binary: true`)
|
||||||
- **Reachability**: `isReachable`, `getReachableNodes`, `getReachingNodes`, `shortestPathLength`, `estimateGraphDistance` (`isReachable` returns `null` when no PLTC index is available — a signal to defer to rule evaluation)
|
- **Reachability**: `isReachable`, `getReachableNodes`, `getReachingNodes`, `shortestPathLength`, `estimateGraphDistance` (`isReachable` returns `null` when no PLTC index is available — a signal to defer to rule evaluation)
|
||||||
- **Snapshots**: `enableCondensedSnapshot`, `toSnapshotBinary`, `Arbiter.fromSnapshotBinary` (lossless: carries relation metadata, TTLs, and validity; malformed buffers fail fast with clean errors)
|
- **Snapshots**: `enableCondensedSnapshot`, `toSnapshotBinary`, `Arbiter.fromSnapshotBinary` (lossless: carries relation metadata, TTLs, and validity; malformed buffers fail fast with clean errors)
|
||||||
- **Value context**: `valueManager` (TTL-gated evidence), `getSituationTree`, `monteCarloWalk`
|
- **Value context**: `valueManager` (TTL-gated evidence), `PartialGraphContext` (overlays), `getSituationTree`, `monteCarloWalk`
|
||||||
|
|
||||||
Check `options`:
|
## Capabilities
|
||||||
|
|
||||||
| Option | Type | Default | Meaning |
|
The engine derives authorization decisions from a relation graph. What it does, in one pass:
|
||||||
|--------|------|---------|---------|
|
|
||||||
| `includeMeta` | `boolean` | `false` | Attach full provenance in `meta` |
|
- **Ten policy kinds** compose arbitrarily: direct, tuple-to-userset (groups), chain, multi-hop, defeasible (when/unless/never/always/requires), union/intersection/exclusion (with OWA fusion), relational comparator (ABAC over values), qualitative comparator (decaying scales), challenge (proofs/MFA), parent.
|
||||||
| `explain` | `boolean` | `false` | Enrich `meta` with the evaluation trace |
|
- **Possibilistic semantics**: decisions are maxitive rankings in `[0,1]`, not booleans — with reliability, validity provenance, and reason codes on every result.
|
||||||
| `binary` | `boolean` | `false` | Binary fast path; results marked `binary: true` |
|
- **Caller-owned time**: every TTL gate, decay, and proof expiry honors the caller's pinned `{ now }`; a rerun reproduces the decision.
|
||||||
| `now` | `number` | engine clock | Pinned temporal context for TTL gates |
|
- **Overlays**: evaluate "what if this evidence existed" without mutating the graph; caller-supplied facts ride the policy's direct relations.
|
||||||
| `partialGraphContext` | `PartialGraphContext` | none | Overlay taking precedence over the base graph |
|
- **Lossless snapshots**: condensed binary serialization (~170 bytes/node) with frozen restore; malformed input fails fast and bounded.
|
||||||
|
- **Reachability**: exact PLTC index with sound fast-fail and a documented `null`-defer contract.
|
||||||
|
- **Determinism**: 251 seeded rigor campaigns — parity across normal/binary/snapshot-restored evaluation, mutation freshness, TTL contracts, complexity classes, and adversarial snapshot fuzzing.
|
||||||
|
|
||||||
|
What it does **not** do: no storage, no transport, no policy source of truth, no user/group management — it is a library that answers one question: *may user U perform relation R on object O?*
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
+559
@@ -0,0 +1,559 @@
|
|||||||
|
# @arbiter/core — API Reference
|
||||||
|
|
||||||
|
> The complete public interface of the Arbiter engine. For the package overview, quick start, and concepts, see the [README](../README.md). This document is the authoritative reference: every public export, method signature, option, result shape, and configuration format.
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
1. [Exports](#1-exports)
|
||||||
|
2. [The `Arbiter` class](#2-the-arbiter-class)
|
||||||
|
3. [`check` options and result shape](#3-check-options-and-result-shape)
|
||||||
|
4. [Relation configurations](#4-relation-configurations)
|
||||||
|
5. [The `valueManager`](#5-the-valuemanager)
|
||||||
|
6. [Partial graphs](#6-partial-graphs)
|
||||||
|
7. [Snapshots](#7-snapshots)
|
||||||
|
8. [Reachability](#8-reachability)
|
||||||
|
9. [The `OWAFusion` utility](#9-the-owafusion-utility)
|
||||||
|
10. [Audit hook](#10-audit-hook)
|
||||||
|
11. [Errors](#11-errors)
|
||||||
|
12. [Reason codes](#12-reason-codes)
|
||||||
|
13. [Validity taxonomy](#13-validity-taxonomy)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Exports
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {
|
||||||
|
Arbiter, // the engine — everything hangs off this
|
||||||
|
GraphIndices, // persistent-relation index structure
|
||||||
|
NodeManager, // node lifecycle
|
||||||
|
RelationManager, // relation lifecycle + value access
|
||||||
|
GraphData, GraphOperations, GraphAnalysis, GraphManager,
|
||||||
|
AuthorizationChecker, // the check() implementation (low-level)
|
||||||
|
RuleEvaluator, // rule dispatch
|
||||||
|
RuleCollector, // rule flattening
|
||||||
|
OWAFusion // interval-fusion utilities
|
||||||
|
} from '@arbiter/core';
|
||||||
|
```
|
||||||
|
|
||||||
|
In normal use you only need `Arbiter`. The other exports exist for advanced composition and are documented inline in source.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. The `Arbiter` class
|
||||||
|
|
||||||
|
### `new Arbiter(options?)`
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Option | Type | Default | Description |
|
||||||
|
|--------|------|---------|-------------|
|
||||||
|
| `disableCaching` | `boolean` | `false` | Disable the direct-check, rule-result, and chain caches |
|
||||||
|
| `disableChainCaching` | `boolean` | `false` | Disable only the chain result cache |
|
||||||
|
| `directCheckCacheSize` | `number` | `10000` | Direct-check cache capacity |
|
||||||
|
| `directCheckCacheTTL` | `number` (ms) | `60000` | Direct-check entry freshness |
|
||||||
|
| `ruleResultCacheTTL` | `number` (ms) | `60000` | Rule-result entry freshness |
|
||||||
|
| `enableRuleResultCache` | `boolean` | `true` | Master switch for the rule-result cache |
|
||||||
|
| `clock` | `() => number` | `Date.now` | Time source for **unpinned** cache freshness. Per-check time always comes from `{ now }` / `partialGraph.now` |
|
||||||
|
| `cacheFactory` | `function` | built-in LRU | DI hook for a custom cache implementation |
|
||||||
|
| `audit` | `(record) => void` | `null` | Audit hook, invoked once per check (see [§10](#10-audit-hook)) |
|
||||||
|
| `partialGraphPolicy` | `object` | — | Overlay admission limits (see [§6](#6-partial-graphs)) |
|
||||||
|
| `keyManager` | `object` | built-in | Advanced: custom id/key mapping |
|
||||||
|
|
||||||
|
### Graph and relation management
|
||||||
|
|
||||||
|
| Method | Signature | Description |
|
||||||
|
|--------|-----------|-------------|
|
||||||
|
| `addNode` | `(key, type, data?) => void` | Create a node. `key` is a string, `type` is a free-form string |
|
||||||
|
| `getNodeData` | `(key) => object` | Node metadata |
|
||||||
|
| `updateNodeData` | `(key, data) => void` | Patch node metadata |
|
||||||
|
| `setRelationConfig` | `(relation, config) => void` | Define how a relation derives decisions (see [§4](#4-relation-configurations)). Re-defining invalidates caches for that relation |
|
||||||
|
| `addRelation` | `(srcKey, relation, dstKey, options?) => void` | Add an edge. `options`: `{ possibility, reliability, value, validity, changed_last_at, decayConfig, source, layer_name }` |
|
||||||
|
| `removeRelation` | `(srcKey, relation, dstKey) => void` | Remove an edge. Immediately revokes grants (no stale cache) |
|
||||||
|
| `resolveNodeId` | `(key) => number \| undefined` | Key → numeric id (`undefined` when unknown) |
|
||||||
|
| `resolveKey` | `(id) => string \| undefined` | Numeric id → key |
|
||||||
|
| `invalidateRuleResultCacheByRelation` | `(relation) => void` | Drop cached rule results for a relation |
|
||||||
|
| `invalidateAllRuleResultCache` | `() => void` | Drop all cached rule results |
|
||||||
|
|
||||||
|
**Write semantics.** `addRelation` upserts: re-adding an existing `(src, relation, dst)` edge updates it. `changed_last_at` defaults to the write time when not supplied; values decay and expire against it (see [§5](#5-the-valuemanager)). All mutations invalidate the affected caches immediately — a removed edge's grant is gone on the next `check`.
|
||||||
|
|
||||||
|
### Check
|
||||||
|
|
||||||
|
| Method | Signature | Description |
|
||||||
|
|--------|-----------|-------------|
|
||||||
|
| `check` | `(userKey, relation, objectKey, options?) => result` | The core question: derive the decision |
|
||||||
|
| `explain` | `(userKey, relation, objectKey, options?) => result` | Alias for `check(..., { explain: true })` — enriches `meta` with the evaluation trace |
|
||||||
|
|
||||||
|
See [§3](#3-check-options-and-result-shape) for options and result shape.
|
||||||
|
|
||||||
|
### Graph algorithms
|
||||||
|
|
||||||
|
| Method | Signature | Description |
|
||||||
|
|--------|-----------|-------------|
|
||||||
|
| `estimateGraphDistance` | `(key1, key2) => number` | Estimated distance |
|
||||||
|
| `shortestPathLength` | `(key1, key2) => number` | Exact shortest path |
|
||||||
|
| `walk` | `(startKey, steps, stepFn?, neighborWeightsFn?) => void` | Traverse with a visitor |
|
||||||
|
| `monteCarloWalk` | `(startKey, steps, options?) => void` | Randomized traversal |
|
||||||
|
| `getSituationTree` | `(nodeId, maxDepth?) => tree` | Neighborhood snapshot |
|
||||||
|
| `getReachableNodes` | `(sourceKey, maxResults?) => string[]` | Nodes reachable from source |
|
||||||
|
| `getReachingNodes` | `(targetKey, maxResults?) => string[]` | Nodes that reach target |
|
||||||
|
|
||||||
|
### Reachability
|
||||||
|
|
||||||
|
| Method | Signature | Description |
|
||||||
|
|--------|-----------|-------------|
|
||||||
|
| `initializeReachabilityChecker` | `(options?) => Promise<void>` | Build the PLTC index (async) |
|
||||||
|
| `isReachable` | `(sourceKey, targetKey) => boolean \| null` | `true`/`false` when the PLTC index decides; `null` = deferred to rule evaluation (documented contract, see [§8](#8-reachability)) |
|
||||||
|
| `getReachabilityStats` | `() => object` | PLTC hit/miss/query statistics |
|
||||||
|
| `adaptToFalsePositive` | `(sourceKey, targetKey, falsePositiveKey, conflictingKey) => void` | PLTC adaptation |
|
||||||
|
| `getAdaptationStats` | `() => object` | Adaptation statistics |
|
||||||
|
|
||||||
|
### Snapshots
|
||||||
|
|
||||||
|
| Method | Signature | Description |
|
||||||
|
|--------|-----------|-------------|
|
||||||
|
| `enableCondensedSnapshot` | `(options?) => void` | Switch to condensed binary mode. **Permanently makes the engine read-only** (writes throw) — call it last |
|
||||||
|
| `toSnapshotBinary` | `(options?) => ArrayBuffer` | Serialize the graph |
|
||||||
|
|
||||||
|
Restore is a static: `Arbiter.fromSnapshotBinary(buffer)` (see [§7](#7-snapshots)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. `check` options and result shape
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
| Option | Type | Default | Meaning |
|
||||||
|
|--------|------|---------|---------|
|
||||||
|
| `binary` | `boolean` | `false` | Binary fast path with strict thresholds; results carry `binary: true` |
|
||||||
|
| `includeMeta` | `boolean` | `false` | Attach full provenance in `meta` |
|
||||||
|
| `explain` | `boolean` | `false` | Enrich `meta` with the evaluation trace |
|
||||||
|
| `now` | `number` (ms) | engine clock | **Pinned temporal context.** Every TTL gate, decay computation, and proof expiry honors it; a rerun with the same `now` reproduces the decision |
|
||||||
|
| `partialGraph` | `object` | `null` | Raw overlay spec — re-ingested into a `PartialGraphContext` (see [§6](#6-partial-graphs)) |
|
||||||
|
| `partialGraphContext` | `PartialGraphContext` | `null` | Pre-built overlay context (preferred when reusing one) |
|
||||||
|
| `collectValues` | `boolean` | `false` | Collect value evidence into `collectedValues` |
|
||||||
|
| `returnHints` | `boolean` | `false` | On denial, attach `hints` (alternative access paths) |
|
||||||
|
| `simpleBinary` | `boolean` | `false` | With `binary`, return the minimal `{ allow, deny, reason }` shape |
|
||||||
|
| `fastPath` | `boolean` | `false` | Enable early-exit thresholds in rule evaluation |
|
||||||
|
| `minAllowPossibility` / `maxDenyPossibility` | `number` | `0.8` / `0.8` | Binary-mode decision thresholds |
|
||||||
|
| `clientStateId` | `string` | `null` | Pass-through client identifier |
|
||||||
|
| `epsilon` / `delta` | `number` | — | Pass-through numerical tolerances |
|
||||||
|
|
||||||
|
### Result shape
|
||||||
|
|
||||||
|
Every check returns the same core shape:
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
possibility: 0.9, // number in [0,1] — the derived decision strength
|
||||||
|
reliability: 1, // number in [0,1] — 0 for denials
|
||||||
|
validity: {
|
||||||
|
label: 'heuristic', // one of the validity taxonomy (see §13)
|
||||||
|
operator: 'identity', // the fusion operator applied
|
||||||
|
regime: 'arbitrary' // dependence assumption
|
||||||
|
// includeMeta adds: sources[], conflictMass, validifiedPossibility, nonMaxitive
|
||||||
|
},
|
||||||
|
reason: 'direct_match', // outcome class (see §12)
|
||||||
|
// optional:
|
||||||
|
meta: { ... }, // includeMeta / explain
|
||||||
|
collectedValues: [...], // collectValues
|
||||||
|
hints: [...], // returnHints on denial
|
||||||
|
binary: true, // binary mode
|
||||||
|
allow: true, // binary mode
|
||||||
|
deny: false // binary mode
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Contract invariants** (enforced by the rigor suite, `tests/rigor/public-contract.test.js`):
|
||||||
|
|
||||||
|
- `possibility` and `reliability` are always finite numbers in `[0, 1]`.
|
||||||
|
- Denials (`possibility === 0`) carry `reliability === 0` — a denied decision never leaks a source's reliability.
|
||||||
|
- `reason` is always a known outcome class.
|
||||||
|
- The result is JSON-serializable — no circular references, no undefined values.
|
||||||
|
- `includeMeta` enriches `meta` but never changes the decision fields.
|
||||||
|
|
||||||
|
### Collected values
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
value: 42, // the relation's value (or an interval for blurred paths)
|
||||||
|
possibility: 0.9,
|
||||||
|
path: ['u:1', 'doc:9'], // entity keys along the derivation
|
||||||
|
source: { entityKey: 'u:1', relation: 'can_read', step: 0 },
|
||||||
|
metadata: { timestamp, reliability, ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Collected values are **TTL-gated**: an expired value is never collected (see [§5](#5-the-valuemanager)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Relation configurations
|
||||||
|
|
||||||
|
`setRelationConfig(relation, config)` defines how decisions for a relation are derived. The config object is also the DSL's AST — it is compiled (validated) on registration; compile errors land in `config._compileErrors`.
|
||||||
|
|
||||||
|
### Direct
|
||||||
|
|
||||||
|
```js
|
||||||
|
{ type: 'direct' }
|
||||||
|
```
|
||||||
|
|
||||||
|
A single edge grants with exactly its possibility. The fastest path.
|
||||||
|
|
||||||
|
### Tuple-to-userset (group membership)
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
type: 'tuple_to_userset',
|
||||||
|
tuplesetRelation: 'owner', // object → intermediate
|
||||||
|
computedRelation: 'member', // user → intermediate
|
||||||
|
reverse: false,
|
||||||
|
minPossibility: 0,
|
||||||
|
owaWeights: [1, 0, ...], // fusion weights (default = max)
|
||||||
|
earlyExitThreshold: 0.95,
|
||||||
|
maxIntermediates: 20
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
User `u` gains `relation` on object `o` when there exists an intermediate `m` with `u -computedRelation-> m` and `m -tuplesetRelation-> o`.
|
||||||
|
|
||||||
|
### Chain (multi-step traversal)
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
type: 'chain',
|
||||||
|
steps: [
|
||||||
|
{ relation: 'member_of', direction: 'out' },
|
||||||
|
{ relation: 'owner', direction: 'out' }
|
||||||
|
],
|
||||||
|
collectValues: false,
|
||||||
|
valueAggregation: 'sum', // 'sum' | 'max' | 'min' | 'average'
|
||||||
|
valueFilters: { relations: [...], minValue, maxValue }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Possibility along a chain = min of the edge possibilities (weakest link); parallel paths disjunct (max).
|
||||||
|
|
||||||
|
### Multi-hop (path search)
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
type: 'multi_hop',
|
||||||
|
relation: 'owner', // relation to traverse
|
||||||
|
maxDepth: 5,
|
||||||
|
pathAggregation: 'max', // 'max' | 'sum' | 'owa'
|
||||||
|
reverse: false,
|
||||||
|
fallbackToBasicPaths: true,
|
||||||
|
owaWeights: [...],
|
||||||
|
collectValues: true,
|
||||||
|
valueFilters: { relations: [...], minValue, maxValue },
|
||||||
|
valueAggregation: 'sum'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Unbounded-depth path search over a single relation.
|
||||||
|
|
||||||
|
### Defeasible logic
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
type: 'defeasible',
|
||||||
|
when: <rule>, // grants
|
||||||
|
unless: <rule>, // defeats when it fires
|
||||||
|
never: <rule>, // absolute veto
|
||||||
|
always: <rule>, // unconditional grant
|
||||||
|
requires: <rule> // precondition
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules compose: `{ when, unless }` = "grants unless defeated"; `{ always, unless }` = "grants unconditionally unless defeated"; `{ never }` = absolute deny.
|
||||||
|
|
||||||
|
### Logical operators
|
||||||
|
|
||||||
|
```js
|
||||||
|
{ union: [rule, rule, ...] } // disjunction — max
|
||||||
|
{ intersection: [rule, rule, ...] } // conjunction — min / conflict-aware
|
||||||
|
{ exclusion: [grantRule, denyRule] } // grant minus deny
|
||||||
|
```
|
||||||
|
|
||||||
|
Union configs additionally support OWA fusion:
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
union: { rules: [...], aggregator: 'owa', owaWeights: [0.7, 0.3], useBilattice: false, epistemicMode: 'hybrid' }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Aggregators: `max`, `min`, `owa`, `top2`, `median`, `sum`.
|
||||||
|
|
||||||
|
### Relational comparator (ABAC)
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
type: 'relational_comparator',
|
||||||
|
comparator: '>=', // any JS comparison operator
|
||||||
|
fallbackBehavior: 'deny', // 'deny' | 'allow' | 'unknown'
|
||||||
|
left: {
|
||||||
|
rule: <rule>, // e.g. { type: 'direct', relation: 'has_balance' }
|
||||||
|
extractValue: true,
|
||||||
|
valueRelation: 'has_balance',
|
||||||
|
evaluateFrom: 'user', // 'user' | 'object'
|
||||||
|
aggregator: 'owa', owaWeights: [...]
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
rule: <rule>,
|
||||||
|
extractValue: true,
|
||||||
|
valueRelation: 'min_age',
|
||||||
|
evaluateFrom: 'object'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Compares value evidence from two operands (supports nested rules and OWA fusion of multi-source values). **Value-freshness gated**: expired operands deny.
|
||||||
|
|
||||||
|
### Qualitative relational comparator
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
type: 'relational_comparator',
|
||||||
|
qualitative: true, // routes to the qualitative implementation
|
||||||
|
comparator: '>=',
|
||||||
|
left: { rule: <rule>, scaleName: 'five-point', decayPeriod: 'HOUR', decaySteps: 1, ... },
|
||||||
|
right: { rule: <rule>, ... },
|
||||||
|
marginSteps: 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The router (`RelationalComparatorRouter`) sends a comparator to the qualitative implementation when `rule.qualitative === true` or when either operand carries a `scaleName`. Qualitative scales decay: possibility decays over time periods, values blur by steps — all against the caller's pinned clock.
|
||||||
|
|
||||||
|
### Challenge (MFA-style)
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
type: 'challenge',
|
||||||
|
challenge: 'mfa', // proof name; defaults to rule name/relation
|
||||||
|
withinMs: 300000 // proof freshness window
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Grants when the partial graph carries a proof for `challenge` within `withinMs`. Missing proofs produce structured `remediation` in the result.
|
||||||
|
|
||||||
|
### Parent
|
||||||
|
|
||||||
|
```js
|
||||||
|
{ type: 'parent', parentRelation: 'parent', relation: 'owner' }
|
||||||
|
```
|
||||||
|
|
||||||
|
Grants when the object's parent (via `parentRelation`) holds `relation` for the user.
|
||||||
|
|
||||||
|
### Computed
|
||||||
|
|
||||||
|
```js
|
||||||
|
{ type: 'computed', relation: 'has_balance' }
|
||||||
|
```
|
||||||
|
|
||||||
|
Defers to the referenced relation's own config.
|
||||||
|
|
||||||
|
### Composition
|
||||||
|
|
||||||
|
Any rule position accepts a config object, so policies nest: unions of chains, exclusions of comparators, defeasible wrapping unions, etc. The `union` operator is flattened during collection (max semantics preserved); `intersection`/`exclusion` preserve their structure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. The `valueManager`
|
||||||
|
|
||||||
|
Accessible as `arbiter.valueManager`. Values are the *evidence* attached to edges; TTL and decay govern their freshness.
|
||||||
|
|
||||||
|
| Method | Signature | Description |
|
||||||
|
|--------|-----------|-------------|
|
||||||
|
| `setTTL` | `(relationType, ttlMs) => void` | Configure value freshness for a relation |
|
||||||
|
| `getTTL` | `(relationType) => number` | Current TTL (default `24h`) |
|
||||||
|
| `setDecayConfig` | `(relationType, config) => void` | Possibility/interval decay over time |
|
||||||
|
| `getDecayConfig` | `(relationType) => config` | Current decay config |
|
||||||
|
| `setDefaultDecayConfig` | `(config) => void` | Global decay default |
|
||||||
|
| `getBlurredValue` | `(relation, now?) => { interval, possibility, reliability }` | TTL-gated value interval. `interval: null` when the value is absent or **expired** |
|
||||||
|
| `getDecayedRelation` | `(relation, now?) => { pointValue, blurredInterval, currentPossibility, ... }` | Decay-aware value |
|
||||||
|
| `invalidateCache` | `(keys) => void` | Drop cached value computations |
|
||||||
|
| `refreshStaleValues` | `(count) => number` | Background recomputation of stale values |
|
||||||
|
| `aggregateCrispValues` | `(values, aggregator?) => number` | Aggregate point values |
|
||||||
|
| `aggregateBlurredValues` | `(values, aggregator?) => interval` | Aggregate intervals |
|
||||||
|
| `compareIntervals` | `(left, right, comparator, epsilon?) => number` | Interval comparison |
|
||||||
|
|
||||||
|
**TTL contract** (pinned by `tests/rigor/ttl-contract.test.js`):
|
||||||
|
|
||||||
|
- TTL is a **value-freshness** gate, not an access-expiry mechanism. A direct relation's grant is timeless — `setTTL('can_read', ...)` does not expire the grant.
|
||||||
|
- Expired values: deny comparators, drop from collected values, return `interval: null` from `getBlurredValue`.
|
||||||
|
- Expiry is evaluated against the caller's pinned `now` when provided; the wall clock only applies to unpinned callers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Partial graphs
|
||||||
|
|
||||||
|
An overlay of caller-supplied evidence evaluated *on top of* the persistent graph. The caller provides facts — relations and proofs — that the check consumes as if they were persistent.
|
||||||
|
|
||||||
|
### Raw spec form
|
||||||
|
|
||||||
|
```js
|
||||||
|
const result = arbiter.check(u, 'can_read', o, {
|
||||||
|
partialGraph: {
|
||||||
|
now: 1_700_000_000_000, // the overlay's temporal context
|
||||||
|
relations: [
|
||||||
|
{ src: 'u:1', relation: 'owner', dst: 'doc:9', possibility: 0.9, value: 42, layer_name: 'witness' }
|
||||||
|
],
|
||||||
|
nodes: [{ key: 'u:1', type: 'user' }],
|
||||||
|
challenges: [{ name: 'mfa', subject: 'u:1', issuedAt: 1_700_000_000_000, expiresAt: 1_700_000_300_000 }]
|
||||||
|
// 'challengeProofs' is accepted as an alias for 'challenges'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The raw form is validated against `partialGraphPolicy` before allocation (a DoS guard: `maxRelations` default `2000`, `maxNodes` default `1000`). `partialGraph.now`, when present, becomes the check's temporal context (it feeds `options.now`).
|
||||||
|
|
||||||
|
### Pre-built context form
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { PartialGraphContext } from '@arbiter/core';
|
||||||
|
|
||||||
|
const ctx = new PartialGraphContext(arbiter, {
|
||||||
|
now: 1_700_000_000_000,
|
||||||
|
relations: [{ src: 'u:1', relation: 'owner', dst: 'doc:9', possibility: 0.9 }]
|
||||||
|
});
|
||||||
|
const result = arbiter.check(u, 'can_read', o, { partialGraphContext: ctx });
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use `partialGraphContext` for a pre-built context.** Passing a context under `partialGraph` re-ingests it as a raw spec (it would be treated as an empty overlay).
|
||||||
|
|
||||||
|
**Precedence contract** (pinned by `tests/rigor/overlay-precedence.test.js`): persistent facts win over overlay facts for the same `(src, relation, dst)`; when the persistent fact is removed, the overlay surfaces. Overlay facts ride the *direct relations a policy consumes* — a TTU-derived relation does not consult the overlay directly.
|
||||||
|
|
||||||
|
**Proofs** (challenge rules): `{ name, subject, issuedAt, expiresAt }`. `issuedAt`/`expiresAt` are explicit (epoch timestamps are valid — `0` means already expired).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Snapshots
|
||||||
|
|
||||||
|
### Serialize
|
||||||
|
|
||||||
|
```js
|
||||||
|
arbiter.enableCondensedSnapshot(); // permanent read-only switch
|
||||||
|
const buf = arbiter.toSnapshotBinary(); // ArrayBuffer
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restore
|
||||||
|
|
||||||
|
```js
|
||||||
|
const restored = Arbiter.fromSnapshotBinary(buf);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Format** (v2): outer header `ARB1` magic + version 2 + graph length; embedded condensed graph section (`CGB1` magic); trailing JSON payload carrying relation metadata (validity, decay configs) and value TTLs. ~170 bytes/node for typical authorization graphs.
|
||||||
|
|
||||||
|
**Contracts**:
|
||||||
|
|
||||||
|
- **Lossless**: possibility, reliability, validity, decay configs, and TTLs round-trip. A restored engine answers checks identically (within 16-bit quantization, ≤ 1/65535).
|
||||||
|
- **Frozen**: the restored engine is read-only; mutations throw.
|
||||||
|
- **Trust boundary**: `fromSnapshotBinary` accepts untrusted bytes. Malformed buffers fail fast with clean, bounded errors — never hangs, crashes, or silently corrupted data. Every count field is cross-validated before use (pinned by `tests/rigor/snapshot-adversarial-fuzz.test.js`).
|
||||||
|
- **TTL caveat**: restore resets `changed_last_at` to access time, so post-restore TTL expiry is measured from the restore moment, not the original write.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Reachability
|
||||||
|
|
||||||
|
```js
|
||||||
|
await arbiter.initializeReachabilityChecker();
|
||||||
|
const r = arbiter.isReachable('u:1', 'doc:9'); // true | false | null
|
||||||
|
```
|
||||||
|
|
||||||
|
- `true` — a path exists (PLTC index is exact for reachability).
|
||||||
|
- `false` — no path exists; **sound fast-fail**: rule evaluation can skip.
|
||||||
|
- `null` — the PLTC index is unavailable (not initialized, or bypassed); **defer to rule evaluation** — this is the documented contract, not an error.
|
||||||
|
|
||||||
|
Chain rules use the PLTC index internally for fast-fail; `getReachabilityStats()` exposes hit/miss/query telemetry.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. The `OWAFusion` utility
|
||||||
|
|
||||||
|
Static interval-fusion functions used by OWA aggregators and exported for direct use:
|
||||||
|
|
||||||
|
| Method | Signature | Description |
|
||||||
|
|--------|-----------|-------------|
|
||||||
|
| `fuseIntervalsWithMeta` | `(intervals, metas, weights, operator) => { interval, possibility, meta? }` | Fusion with validity metadata |
|
||||||
|
| `maxInterval` | `(intervals, metas) => result` | Disjunctive (max) fusion |
|
||||||
|
| `isWithinTTL` | `(timestamp, ttlMs, now?) => boolean` | TTL check honoring the caller clock |
|
||||||
|
| `compareIntervals` | `(left, right, comparator, epsilon?) => number` | Interval comparison |
|
||||||
|
|
||||||
|
Operators: `max`, `min`, `owa`, `sum`, `average`, `product`, `top2`, `median`. Conjunctive operators surface conflict mass instead of silently averaging.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Audit hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
const arbiter = new Arbiter({
|
||||||
|
audit(record) {
|
||||||
|
// record: { timestamp, userKey, relation, objectKey, decision,
|
||||||
|
// possibility, binary, partialGraphUsed, validityLabel, sources }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The engine stores nothing — the caller owns persistence and retention. The hook fires once per `check` (not for internal recursive evaluations), at zero cost when absent.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Errors
|
||||||
|
|
||||||
|
`check` and `addRelation` throw on invalid input; all other failures surface as `reason` codes in the result:
|
||||||
|
|
||||||
|
| Error | Trigger |
|
||||||
|
|-------|---------|
|
||||||
|
| `Invalid relation config ...` | `setRelationConfig` with a non-object config |
|
||||||
|
| `Invalid possibility: expected a finite number in [0, 1]` | `addRelation` with out-of-range possibility |
|
||||||
|
| `Cannot add/remove relation while in snapshot read-only mode` | mutation after `enableCondensedSnapshot` |
|
||||||
|
| `Partial graph exceeds max relations/nodes ...` | overlay larger than `partialGraphPolicy` limits |
|
||||||
|
| `Invalid condensed graph binary ...` | malformed snapshot bytes (clean, bounded) |
|
||||||
|
| `Invalid arbiter snapshot payload` | snapshot JSON payload not an object |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Reason codes
|
||||||
|
|
||||||
|
| Code | Meaning |
|
||||||
|
|------|---------|
|
||||||
|
| `direct_match` | Direct edge granted |
|
||||||
|
| `no_relation` | No edge for the relation |
|
||||||
|
| `threshold_not_met` | Direct edge below the allow threshold |
|
||||||
|
| `missing_node` | User or object unknown |
|
||||||
|
| `no_config` | Relation has no configuration |
|
||||||
|
| `cycle` | Recursion cycle detected |
|
||||||
|
| `allow` / `deny` | Binary-mode decision |
|
||||||
|
| `insufficient_confidence` | Binary mode, thresholds not decisive |
|
||||||
|
| `chain_path_found` / `no_chain_path_found` | Chain traversal |
|
||||||
|
| `multi_hop_path_found` | Multi-hop path |
|
||||||
|
| `no_matching_rule` | No rule matched |
|
||||||
|
| `logical_operator_evaluation` | Union/intersection/exclusion result |
|
||||||
|
| `defeated_by_unless` / `never_rule_triggered` / `requirements_not_met` | Defeasible outcomes |
|
||||||
|
| `challenge_missing` / `challenge_missing_context` / `challenge_satisfied` | Challenge rules |
|
||||||
|
| `exists` / `relation_exists` / `no_direct_match` | Rule-internal outcomes |
|
||||||
|
| `qualitative_interval_comparison` | Qualitative comparator |
|
||||||
|
| `not_reachable` | PLTC fast-fail |
|
||||||
|
| `unknown_rule_type` / `invalid_rule` | Unrecognized config |
|
||||||
|
| `evaluation_error` / `error` | Evaluation failure (never crashes the process) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Validity taxonomy
|
||||||
|
|
||||||
|
Validity labels (weakest → strongest of *evidence*):
|
||||||
|
|
||||||
|
```
|
||||||
|
finite_sample < anytime < conformal < approximate < heuristic < unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
- `finite_sample` — evidence from an explicit sample
|
||||||
|
- `anytime` — anytime-algorithm guarantees
|
||||||
|
- `conformal` — conformal prediction coverage
|
||||||
|
- `approximate` — approximation with stated bounds
|
||||||
|
- `heuristic` — unvalidated ranking (default for unlabeled relations)
|
||||||
|
- `unknown` — unrecognized label
|
||||||
|
|
||||||
|
`validity.label` is the **weakest** source label, downgraded by the fusion operator's class: identity/max preserve the weakest label; min surfaces `conflictMass = 1 − fused_possibility` and a `validifiedPossibility`; product-style and interior-OWA operators are always `heuristic` (no linear validification under arbitrary dependence).
|
||||||
Generated
+22
-54
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@arbiter/core",
|
"name": "@arbiter/core",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@arbiter/core",
|
"name": "@arbiter/core",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tenere/pltc-core": "^0.6.3",
|
"@tenere/pltc-core": "^0.6.3",
|
||||||
@@ -14,21 +14,32 @@
|
|||||||
"uuidv7": "^1.0.2"
|
"uuidv7": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@arbiter/evidence-dsl": "^1.0.0",
|
||||||
"@rigor/core": "*",
|
"@rigor/core": "*",
|
||||||
"@tenere/benchmark-lib": "^2.0.1",
|
"@tenere/benchmark-lib": "^2.0.1",
|
||||||
"fast-check": "^4.5.3",
|
"fast-check": "^4.5.3"
|
||||||
"peggy": "^5.0.6"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@peggyjs/from-mem": {
|
"node_modules/@arbiter/core": {
|
||||||
"version": "3.1.3",
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://hub.kl1.tenere.ai/api/packages/Arbiter/npm/%40arbiter%2Fcore/-/1.0.1/core-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-BfIv6vRKsuJR39WBkBGxl2PQ/LDoW/J1u4UeFrog0AbeTSTvRqhxG1+its2pAcUvcQXq99ZOccFlK3XgNRKt4Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"semver": "7.7.4"
|
"@tenere/pltc-core": "^0.6.3",
|
||||||
},
|
"heapify": "^1.0.2",
|
||||||
"engines": {
|
"uuidv7": "^1.0.2"
|
||||||
"node": ">=20.8"
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@arbiter/evidence-dsl": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://hub.kl1.tenere.ai/api/packages/Arbiter/npm/%40arbiter%2Fevidence-dsl/-/1.0.0/evidence-dsl-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-v28SkNfcR60rJkDG6Btp5ieYaHQQZEiIJshsyGZYJiTt+V2bGbEj8341PUSzaSE5vRL2mVmRWKT9RoDHQXnzWg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@arbiter/core": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rigor/analysis": {
|
"node_modules/@rigor/analysis": {
|
||||||
@@ -332,14 +343,6 @@
|
|||||||
"@tenere/graph-core": "^1.0.1"
|
"@tenere/graph-core": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/commander": {
|
|
||||||
"version": "14.0.3",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fast-check": {
|
"node_modules/fast-check": {
|
||||||
"version": "4.9.0",
|
"version": "4.9.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -365,22 +368,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/peggy": {
|
|
||||||
"version": "5.1.0",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@peggyjs/from-mem": "3.1.3",
|
|
||||||
"commander": "^14.0.3",
|
|
||||||
"source-map-generator": "2.0.6"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"peggy": "bin/peggy.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pure-rand": {
|
"node_modules/pure-rand": {
|
||||||
"version": "8.4.2",
|
"version": "8.4.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -396,25 +383,6 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/semver": {
|
|
||||||
"version": "7.7.4",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"bin": {
|
|
||||||
"semver": "bin/semver.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/source-map-generator": {
|
|
||||||
"version": "2.0.6",
|
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/uuidv7": {
|
"node_modules/uuidv7": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
|||||||
+4
-5
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@arbiter/core",
|
"name": "@arbiter/core",
|
||||||
"version": "1.0.1",
|
"version": "1.0.8",
|
||||||
"description": "Arbiter core engine: graph indices, relation/reachability, authorization rule evaluator, DSL/AST, condensed & sharded snapshots, and evidence fusion.",
|
"description": "Arbiter core engine: graph indices, relation/reachability, authorization rule evaluator, DSL/AST, condensed & sharded snapshots, and evidence fusion.",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"author": "",
|
"author": "",
|
||||||
@@ -32,8 +32,6 @@
|
|||||||
"test:property": "node --test --test-force-exit \"tests/property-based/**/*.test.js\"",
|
"test:property": "node --test --test-force-exit \"tests/property-based/**/*.test.js\"",
|
||||||
"test:integration": "node --test --test-force-exit \"tests/integration/*.js\"",
|
"test:integration": "node --test --test-force-exit \"tests/integration/*.js\"",
|
||||||
"test:perf": "RUN_PERF_TESTS=1 node --test --test-force-exit tests/engine/core-performance-targets.test.js",
|
"test:perf": "RUN_PERF_TESTS=1 node --test --test-force-exit tests/engine/core-performance-targets.test.js",
|
||||||
"generate:parser": "node scripts/generate-parser.js",
|
|
||||||
"build:ast": "npm run generate:parser",
|
|
||||||
"benchmark": "node --expose-gc scripts/benchmark.js",
|
"benchmark": "node --expose-gc scripts/benchmark.js",
|
||||||
"benchmark:core": "node benchmarks/core-performance-benchmark.js",
|
"benchmark:core": "node benchmarks/core-performance-benchmark.js",
|
||||||
"benchmark:batch": "node benchmarks/batch-size-analysis.js",
|
"benchmark:batch": "node benchmarks/batch-size-analysis.js",
|
||||||
@@ -49,7 +47,8 @@
|
|||||||
"benchmark:memory": "node benchmarks/memory-breakdown.js",
|
"benchmark:memory": "node benchmarks/memory-breakdown.js",
|
||||||
"benchmark:multi-hop": "node benchmarks/multi-hop-rule-bench.js",
|
"benchmark:multi-hop": "node benchmarks/multi-hop-rule-bench.js",
|
||||||
"benchmark:save": "node --expose-gc scripts/benchmark.js --save",
|
"benchmark:save": "node --expose-gc scripts/benchmark.js --save",
|
||||||
"benchmark:complex": "node --expose-gc benchmarks/complex-query-bench.js"
|
"benchmark:complex": "node --expose-gc benchmarks/complex-query-bench.js",
|
||||||
|
"benchmark:complex-query": "node --expose-gc benchmarks/complex-query-bench.js"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"zanzibar",
|
"zanzibar",
|
||||||
@@ -68,6 +67,6 @@
|
|||||||
"@rigor/core": "*",
|
"@rigor/core": "*",
|
||||||
"@tenere/benchmark-lib": "^2.0.1",
|
"@tenere/benchmark-lib": "^2.0.1",
|
||||||
"fast-check": "^4.5.3",
|
"fast-check": "^4.5.3",
|
||||||
"peggy": "^5.0.6"
|
"@arbiter/evidence-dsl": "^1.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+100
-16
@@ -28,11 +28,24 @@ const BASELINE_PATH = (() => {
|
|||||||
|
|
||||||
benchmark.config({
|
benchmark.config({
|
||||||
measurements: ['timing'],
|
measurements: ['timing'],
|
||||||
uncertaintyThreshold: 0.99,
|
// Stop collecting once 90% confident (was 0.99 ≈ "confident" immediately,
|
||||||
minSamples: 0,
|
// so the loop stopped at the first check and the sub-ms checks were
|
||||||
maxSamples: 200,
|
// measured from ~5 noisy samples). A tighter threshold forces the loop to
|
||||||
|
// keep sampling until variance actually tightens.
|
||||||
|
uncertaintyThreshold: 0.1,
|
||||||
|
// Sample-count floor before any early stop (was 0). Sub-ms checks need a
|
||||||
|
// real sample population for a stable mostPlausible / p95.
|
||||||
|
minSamples: 200,
|
||||||
|
// Headroom for noisy actions to run to (was 200, which was the effective
|
||||||
|
// cap and doubled as the practical floor).
|
||||||
|
maxSamples: 2000,
|
||||||
overheadCompensation: true,
|
overheadCompensation: true,
|
||||||
gcBetweenSamples: true,
|
// Per-sample GC injected a global.gc() between every iteration, which
|
||||||
|
// dominated sub-ms timings and produced the wild run-to-run swings
|
||||||
|
// (check[overlay-on-top] flagged -23%..-83% "faster" across runs of
|
||||||
|
// unchanged code). GC is left to the runtime; the sample floor + tight
|
||||||
|
// confidence threshold now stabilize the distribution instead.
|
||||||
|
gcBetweenSamples: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
let CRASHED = 0
|
let CRASHED = 0
|
||||||
@@ -63,34 +76,64 @@ function buildEngine() {
|
|||||||
|
|
||||||
const engine = buildEngine()
|
const engine = buildEngine()
|
||||||
|
|
||||||
|
// ── Warmup ────────────────────────────────────────────────────────────
|
||||||
|
// Sub-ms checks measured without a warmup phase mix first-touch allocation,
|
||||||
|
// lazy index construction, and JIT compilation into the sample population —
|
||||||
|
// a bimodal distribution (check[direct-hit] ~4µs vs ~20µs) that flapped the
|
||||||
|
// regression gate across runs of unchanged code. Run each hot path to a
|
||||||
|
// steady state before any sampling.
|
||||||
|
function warmupChecks(a, iterations = 20000) {
|
||||||
|
const ctx = new PartialGraphContext(a, {
|
||||||
|
relations: [{ src: 'user:1', relation: 'owner', dst: 'doc:1', possibility: 0.5 }]
|
||||||
|
})
|
||||||
|
for (let i = 0; i < iterations; i++) {
|
||||||
|
a.check('user:1', 'owner', 'doc:1')
|
||||||
|
a.check('user:1', 'can_read', 'doc:1')
|
||||||
|
a.check('user:5', 'owner', 'doc:1')
|
||||||
|
a.check('user:1', 'can_read', 'doc:1', { includeMeta: true })
|
||||||
|
a.check('user:1', 'owner', 'doc:1', { partialGraphContext: ctx })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
warmupChecks(engine)
|
||||||
|
|
||||||
|
// Sub-ms checks are 4–20µs per call — below reliable single-call timing
|
||||||
|
// resolution, so one GC tick or context switch inflates a sample (bimodal
|
||||||
|
// distributions flapped the gate). Each sub-ms action measures a BATCH of
|
||||||
|
// calls per sample; jitter amortizes across the batch and the relative
|
||||||
|
// comparison against the baseline (which uses the same BATCH) stays exact.
|
||||||
|
const BATCH = 100
|
||||||
|
|
||||||
const directBench = benchmark('check[direct-hit]', () => {
|
const directBench = benchmark('check[direct-hit]', () => {
|
||||||
engine.check('user:1', 'owner', 'doc:1')
|
for (let i = 0; i < BATCH; i++) engine.check('user:1', 'owner', 'doc:1')
|
||||||
})
|
})
|
||||||
|
|
||||||
const unionBench = benchmark('check[union-ttu]', () => {
|
const unionBench = benchmark('check[union-ttu]', () => {
|
||||||
engine.check('user:1', 'can_read', 'doc:1')
|
for (let i = 0; i < BATCH; i++) engine.check('user:1', 'can_read', 'doc:1')
|
||||||
})
|
})
|
||||||
|
|
||||||
const deniedBench = benchmark('check[denied-miss]', () => {
|
const deniedBench = benchmark('check[denied-miss]', () => {
|
||||||
engine.check('user:5', 'owner', 'doc:1')
|
for (let i = 0; i < BATCH; i++) engine.check('user:5', 'owner', 'doc:1')
|
||||||
})
|
})
|
||||||
|
|
||||||
const metaBench = benchmark('check[include-meta]', () => {
|
const metaBench = benchmark('check[include-meta]', () => {
|
||||||
engine.check('user:1', 'can_read', 'doc:1', { includeMeta: true })
|
for (let i = 0; i < BATCH; i++) engine.check('user:1', 'can_read', 'doc:1', { includeMeta: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
const overlayBench = benchmark('check[overlay-on-top]', () => {
|
const overlayBench = benchmark('check[overlay-on-top]', () => {
|
||||||
const ctx = new PartialGraphContext(engine, {
|
for (let i = 0; i < BATCH; i++) {
|
||||||
relations: [{ src: 'user:1', relation: 'owner', dst: 'doc:1', possibility: 0.5 }]
|
const ctx = new PartialGraphContext(engine, {
|
||||||
})
|
relations: [{ src: 'user:1', relation: 'owner', dst: 'doc:1', possibility: 0.5 }]
|
||||||
engine.check('user:1', 'owner', 'doc:1', { partialGraphContext: ctx })
|
})
|
||||||
|
engine.check('user:1', 'owner', 'doc:1', { partialGraphContext: ctx })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const binaryBench = (() => {
|
const binaryBench = (() => {
|
||||||
const snap = buildEngine()
|
const snap = buildEngine()
|
||||||
snap.enableCondensedSnapshot()
|
snap.enableCondensedSnapshot()
|
||||||
|
for (let i = 0; i < 5000; i++) snap.check('user:1', 'owner', 'doc:1', { binary: true })
|
||||||
return benchmark('check[binary-direct]', () => {
|
return benchmark('check[binary-direct]', () => {
|
||||||
snap.check('user:1', 'owner', 'doc:1', { binary: true })
|
for (let i = 0; i < BATCH; i++) snap.check('user:1', 'owner', 'doc:1', { binary: true })
|
||||||
})
|
})
|
||||||
})()
|
})()
|
||||||
|
|
||||||
@@ -164,9 +207,50 @@ if (SAVE) {
|
|||||||
const baseline = JSON.parse(fs.readFileSync(BASELINE_PATH, 'utf8'))
|
const baseline = JSON.parse(fs.readFileSync(BASELINE_PATH, 'utf8'))
|
||||||
const regResult = detectRegressions(current, baseline)
|
const regResult = detectRegressions(current, baseline)
|
||||||
if (!AS_JSON) console.log(formatRegressions(regResult, 'pretty'))
|
if (!AS_JSON) console.log(formatRegressions(regResult, 'pretty'))
|
||||||
const critical = regResult.regressions.filter(r => r.severity === 'high')
|
|
||||||
if (critical.length > 0) {
|
// ── Ratio-based self-calibration ─────────────────────────────────────
|
||||||
console.error(`❌ ${critical.length} critical regression(s): ${critical.map(r => r.name).join(', ')}`)
|
// Absolute timings swing with machine load (a shared runner at load 19
|
||||||
|
// shifted every action +15..+50%). Comparing each action's RATIO to a cheap
|
||||||
|
// reference action cancels the load: load scales all actions proportionally,
|
||||||
|
// while a code regression shifts only the affected action's ratio. The
|
||||||
|
// reference action itself is still gate-checked absolutely with a loose
|
||||||
|
// bound (a regression of the reference would otherwise mask every ratio).
|
||||||
|
const RATIO_REFERENCE = process.env.BENCH_RATIO_REFERENCE || 'check[direct-hit]'
|
||||||
|
const RATIO_PERCENT = Number(process.env.RATIO_REGRESSION_PERCENT ?? 15)
|
||||||
|
const REFERENCE_PERCENT = Number(process.env.REFERENCE_REGRESSION_PERCENT ?? 30)
|
||||||
|
|
||||||
|
const currentRef = current.actions?.[RATIO_REFERENCE]?.mostPlausible
|
||||||
|
const baselineRef = baseline.actions?.[RATIO_REFERENCE]?.mostPlausible
|
||||||
|
const ratioViolations = []
|
||||||
|
let referenceViolation = false
|
||||||
|
|
||||||
|
if (currentRef > 0 && baselineRef > 0) {
|
||||||
|
if (currentRef > baselineRef * (1 + REFERENCE_PERCENT / 100)) {
|
||||||
|
referenceViolation = true
|
||||||
|
ratioViolations.push(
|
||||||
|
`${RATIO_REFERENCE} (reference) baseline=${baselineRef.toFixed(4)} → current=${currentRef.toFixed(4)} (+${(((currentRef / baselineRef) - 1) * 100).toFixed(1)}%)`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for (const [name, cur] of Object.entries(current.actions)) {
|
||||||
|
if (name === RATIO_REFERENCE) continue
|
||||||
|
const base = baseline.actions?.[name]
|
||||||
|
if (!base || !(base.mostPlausible > 0)) continue
|
||||||
|
const curRatio = cur.mostPlausible / currentRef
|
||||||
|
const baseRatio = base.mostPlausible / baselineRef
|
||||||
|
if (curRatio > baseRatio * (1 + RATIO_PERCENT / 100)) {
|
||||||
|
ratioViolations.push(
|
||||||
|
`${name} ratio ${curRatio.toFixed(3)} → baseline ${baseRatio.toFixed(3)} (+${(((curRatio / baseRatio) - 1) * 100).toFixed(1)}%)`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ratioViolations.length > 0) {
|
||||||
|
console.error(`❌ ${ratioViolations.length} ratio regression(s):`)
|
||||||
|
for (const v of ratioViolations) console.error(` - ${v}`)
|
||||||
|
if (referenceViolation) {
|
||||||
|
console.error(' (the reference action regressed absolutely — ratios may be unreliable)')
|
||||||
|
}
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
} else if (!SAVE) {
|
} else if (!SAVE) {
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Script to generate the DSL parser from Peggy grammar
|
|
||||||
*/
|
|
||||||
|
|
||||||
import peggy from 'peggy';
|
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
|
|
||||||
const grammarPath = path.join(__dirname, '../src/ast/grammar/dsl.peggy');
|
|
||||||
const outputPath = path.join(__dirname, '../src/ast/parser/GeneratedParser.js');
|
|
||||||
|
|
||||||
console.log('Generating DSL parser from Peggy grammar...');
|
|
||||||
console.log('Grammar:', grammarPath);
|
|
||||||
console.log('Output:', outputPath);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Read the grammar file
|
|
||||||
const grammar = fs.readFileSync(grammarPath, 'utf8');
|
|
||||||
|
|
||||||
// Generate the parser
|
|
||||||
const parserSource = peggy.generate(grammar, {
|
|
||||||
format: 'es',
|
|
||||||
output: 'source',
|
|
||||||
grammarSource: 'dsl.peggy'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Write the generated parser
|
|
||||||
fs.writeFileSync(outputPath, parserSource);
|
|
||||||
|
|
||||||
console.log('✅ Parser generated successfully!');
|
|
||||||
console.log('Generated file:', outputPath);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Error generating parser:');
|
|
||||||
console.error(error.message);
|
|
||||||
|
|
||||||
if (error.format) {
|
|
||||||
console.error('\nFormatted error:');
|
|
||||||
console.error(error.format([
|
|
||||||
{ source: 'dsl.peggy', text: fs.readFileSync(grammarPath, 'utf8') }
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
@@ -1,356 +0,0 @@
|
|||||||
import { parse } from './parser/DSLParser.js';
|
|
||||||
import { RuleGenerator } from './generator/RuleGenerator.js';
|
|
||||||
import { validateDslText } from './validation/DSLValidation.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DSL Compiler - Main integration layer
|
|
||||||
* Compiles DSL text into rule configurations for the zanzibar-graph system
|
|
||||||
*/
|
|
||||||
export class DSLCompiler {
|
|
||||||
constructor(arbiter) {
|
|
||||||
this.arbiter = arbiter;
|
|
||||||
this.parser = parse;
|
|
||||||
this.generator = new RuleGenerator(arbiter);
|
|
||||||
this.compiledPrograms = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compile DSL text into rule configurations
|
|
||||||
* @param {string} dslText - DSL text to compile
|
|
||||||
* @param {string} programName - Optional name for the program
|
|
||||||
* @returns {Object} Compilation result
|
|
||||||
*/
|
|
||||||
compile(dslText, programName = 'default') {
|
|
||||||
try {
|
|
||||||
const validation = validateDslText(dslText);
|
|
||||||
if (!validation.success) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
errors: validation.errors,
|
|
||||||
warnings: validation.warnings,
|
|
||||||
program: null,
|
|
||||||
generatedRules: new Map()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const program = validation.program;
|
|
||||||
|
|
||||||
// Convert plain AST to ProgramNode structure
|
|
||||||
const programNode = {
|
|
||||||
definitions: program.body.filter(s => s.type === 'Definition'),
|
|
||||||
facts: program.body.filter(s => s.type === 'Fact'),
|
|
||||||
evidence: program.body.filter(s => s.type === 'Evidence'),
|
|
||||||
measures: program.body.filter(s => s.type === 'Measure'),
|
|
||||||
validate: () => ({ isValid: true, errors: [], warnings: [] })
|
|
||||||
};
|
|
||||||
|
|
||||||
// Generate rules from AST
|
|
||||||
const generationResult = this.generator.generateRules(programNode);
|
|
||||||
|
|
||||||
if (!generationResult.success) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
errors: generationResult.errors,
|
|
||||||
warnings: [],
|
|
||||||
program: programNode,
|
|
||||||
generatedRules: new Map()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store compiled program
|
|
||||||
this.compiledPrograms.set(programName, {
|
|
||||||
program: programNode,
|
|
||||||
generatedRules: this.generator.getGeneratedRules(),
|
|
||||||
dependencyIndex: this.generator.getDependencyIndex(),
|
|
||||||
compiledAt: new Date()
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
errors: validation.errors,
|
|
||||||
warnings: validation.warnings,
|
|
||||||
program: programNode,
|
|
||||||
generatedRules: this.generator.getGeneratedRules(),
|
|
||||||
dependencyIndex: this.generator.getDependencyIndex(),
|
|
||||||
generatedCount: generationResult.generatedCount
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
errors: [`Compilation error: ${error.message}`],
|
|
||||||
warnings: [],
|
|
||||||
program: null,
|
|
||||||
generatedRules: new Map()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compile multiple DSL programs
|
|
||||||
* @param {Object} programs - Map of program names to DSL text
|
|
||||||
* @returns {Object} Compilation result for all programs
|
|
||||||
*/
|
|
||||||
compileMultiple(programs) {
|
|
||||||
const results = {};
|
|
||||||
let overallSuccess = true;
|
|
||||||
const allErrors = [];
|
|
||||||
const allWarnings = [];
|
|
||||||
|
|
||||||
for (const [name, dslText] of Object.entries(programs)) {
|
|
||||||
const result = this.compile(dslText, name);
|
|
||||||
results[name] = result;
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
overallSuccess = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
allErrors.push(...result.errors.map(err => `${name}: ${err}`));
|
|
||||||
allWarnings.push(...result.warnings.map(warn => `${name}: ${warn}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: overallSuccess,
|
|
||||||
errors: allErrors,
|
|
||||||
warnings: allWarnings,
|
|
||||||
results: results
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get compiled program by name
|
|
||||||
* @param {string} programName - Name of the program
|
|
||||||
* @returns {Object|null} Compiled program or null
|
|
||||||
*/
|
|
||||||
getCompiledProgram(programName) {
|
|
||||||
return this.compiledPrograms.get(programName) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all compiled programs
|
|
||||||
* @returns {Map} Map of all compiled programs
|
|
||||||
*/
|
|
||||||
getAllCompiledPrograms() {
|
|
||||||
return this.compiledPrograms;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove compiled program
|
|
||||||
* @param {string} programName - Name of the program to remove
|
|
||||||
* @returns {boolean} True if removed successfully
|
|
||||||
*/
|
|
||||||
removeCompiledProgram(programName) {
|
|
||||||
return this.compiledPrograms.delete(programName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all compiled programs
|
|
||||||
*/
|
|
||||||
clearCompiledPrograms() {
|
|
||||||
this.compiledPrograms.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get compilation statistics
|
|
||||||
* @returns {Object} Compilation statistics
|
|
||||||
*/
|
|
||||||
getCompilationStats() {
|
|
||||||
const stats = {
|
|
||||||
totalPrograms: this.compiledPrograms.size,
|
|
||||||
totalRules: 0,
|
|
||||||
programs: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.compiledPrograms.forEach((program, name) => {
|
|
||||||
const programStats = {
|
|
||||||
name: name,
|
|
||||||
compiledAt: program.compiledAt,
|
|
||||||
ruleCount: program.generatedRules.size,
|
|
||||||
definitions: program.program.definitions.length,
|
|
||||||
facts: program.program.facts.length,
|
|
||||||
evidence: program.program.evidence.length,
|
|
||||||
measures: program.program.measures.length
|
|
||||||
};
|
|
||||||
|
|
||||||
stats.programs[name] = programStats;
|
|
||||||
stats.totalRules += program.generatedRules.size;
|
|
||||||
});
|
|
||||||
|
|
||||||
return stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate DSL text without compiling
|
|
||||||
* @param {string} dslText - DSL text to validate
|
|
||||||
* @returns {Object} Validation result
|
|
||||||
*/
|
|
||||||
validate(dslText) {
|
|
||||||
const validation = validateDslText(dslText);
|
|
||||||
return {
|
|
||||||
success: validation.success,
|
|
||||||
errors: validation.errors,
|
|
||||||
warnings: validation.warnings,
|
|
||||||
program: validation.program
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get parser errors from last parse
|
|
||||||
* @returns {string[]} Array of parser errors
|
|
||||||
*/
|
|
||||||
getParserErrors() {
|
|
||||||
return this.parser.getErrors();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get generator errors from last generation
|
|
||||||
* @returns {string[]} Array of generator errors
|
|
||||||
*/
|
|
||||||
getGeneratorErrors() {
|
|
||||||
return this.generator.getErrors();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a relation is configured
|
|
||||||
* @param {string} relation - Relation name to check
|
|
||||||
* @returns {boolean} True if relation is configured
|
|
||||||
*/
|
|
||||||
isRelationConfigured(relation) {
|
|
||||||
return this.arbiter.relationConfigs.has(relation);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get relation configuration
|
|
||||||
* @param {string} relation - Relation name
|
|
||||||
* @returns {Object|null} Relation configuration or null
|
|
||||||
*/
|
|
||||||
getRelationConfig(relation) {
|
|
||||||
return this.arbiter.relationConfigs.get(relation) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all configured relations
|
|
||||||
* @returns {Map} Map of all relation configurations
|
|
||||||
*/
|
|
||||||
getAllRelationConfigs() {
|
|
||||||
return this.arbiter.relationConfigs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export compiled program to JSON
|
|
||||||
* @param {string} programName - Name of the program to export
|
|
||||||
* @returns {string|null} JSON string or null if program not found
|
|
||||||
*/
|
|
||||||
exportProgram(programName) {
|
|
||||||
const program = this.getCompiledProgram(programName);
|
|
||||||
if (!program) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSON.stringify({
|
|
||||||
name: programName,
|
|
||||||
compiledAt: program.compiledAt,
|
|
||||||
program: this.serializeProgram(program.program),
|
|
||||||
generatedRules: Array.from(program.generatedRules.entries())
|
|
||||||
}, null, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Import compiled program from JSON
|
|
||||||
* @param {string} jsonString - JSON string to import
|
|
||||||
* @returns {boolean} True if imported successfully
|
|
||||||
*/
|
|
||||||
importProgram(jsonString) {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(jsonString);
|
|
||||||
const program = this.deserializeProgram(data.program);
|
|
||||||
|
|
||||||
this.compiledPrograms.set(data.name, {
|
|
||||||
program: program,
|
|
||||||
generatedRules: new Map(data.generatedRules),
|
|
||||||
compiledAt: new Date(data.compiledAt)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Apply rules to arbiter
|
|
||||||
data.generatedRules.forEach(([relation, config]) => {
|
|
||||||
this.arbiter.setRelationConfig(relation, config);
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Serialize program to plain object
|
|
||||||
* @param {ProgramNode} program - Program to serialize
|
|
||||||
* @returns {Object} Serialized program
|
|
||||||
*/
|
|
||||||
serializeProgram(program) {
|
|
||||||
// This is a simplified serialization - in a real implementation,
|
|
||||||
// you'd want to properly serialize all node types
|
|
||||||
return {
|
|
||||||
type: 'Program',
|
|
||||||
definitions: program.definitions.map(def => ({
|
|
||||||
type: 'Definition',
|
|
||||||
name: def.name,
|
|
||||||
definitionType: def.definitionType,
|
|
||||||
fields: def.fields.map(field => ({
|
|
||||||
type: 'Field',
|
|
||||||
name: field.name,
|
|
||||||
type: field.type,
|
|
||||||
isArray: field.isArray,
|
|
||||||
isOptional: field.isOptional
|
|
||||||
}))
|
|
||||||
})),
|
|
||||||
facts: program.facts.map(fact => ({
|
|
||||||
type: 'Fact',
|
|
||||||
name: fact.name,
|
|
||||||
parameters: fact.parameters.map(param => ({
|
|
||||||
type: 'Parameter',
|
|
||||||
name: param.name,
|
|
||||||
type: param.type,
|
|
||||||
isArray: param.isArray
|
|
||||||
}))
|
|
||||||
})),
|
|
||||||
evidence: program.evidence.map(ev => ({
|
|
||||||
type: 'Evidence',
|
|
||||||
name: ev.name,
|
|
||||||
parameters: ev.parameters.map(param => ({
|
|
||||||
type: 'Parameter',
|
|
||||||
name: param.name,
|
|
||||||
type: param.type,
|
|
||||||
isArray: param.isArray
|
|
||||||
})),
|
|
||||||
returnType: ev.returnType
|
|
||||||
})),
|
|
||||||
measures: program.measures.map(measure => ({
|
|
||||||
type: 'Measure',
|
|
||||||
name: measure.name,
|
|
||||||
parameters: measure.parameters.map(param => ({
|
|
||||||
type: 'Parameter',
|
|
||||||
name: param.name,
|
|
||||||
type: param.type,
|
|
||||||
isArray: param.isArray
|
|
||||||
})),
|
|
||||||
returnType: measure.returnType
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deserialize program from plain object
|
|
||||||
* @param {Object} data - Serialized program data
|
|
||||||
* @returns {ProgramNode} Deserialized program
|
|
||||||
*/
|
|
||||||
deserializeProgram(data) {
|
|
||||||
// This is a simplified deserialization - in a real implementation,
|
|
||||||
// you'd want to properly deserialize all node types
|
|
||||||
const program = new ProgramNode();
|
|
||||||
|
|
||||||
// Note: This is a basic implementation. In practice, you'd need
|
|
||||||
// to properly reconstruct all the AST nodes from the serialized data
|
|
||||||
|
|
||||||
return program;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,352 +0,0 @@
|
|||||||
import { DSLCompiler } from '../DSLCompiler.js';
|
|
||||||
import { Arbiter } from '../../core/Arbiter.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Progressive Test Suite - Tests each language feature incrementally
|
|
||||||
*/
|
|
||||||
export function runProgressiveTestSuite() {
|
|
||||||
console.log('=== Progressive DSL Test Suite ===\n');
|
|
||||||
|
|
||||||
const arbiter = new Arbiter();
|
|
||||||
const compiler = new DSLCompiler(arbiter);
|
|
||||||
|
|
||||||
const tests = [
|
|
||||||
{
|
|
||||||
name: '1. Basic Definitions',
|
|
||||||
dsl: `
|
|
||||||
definition User {
|
|
||||||
name: string
|
|
||||||
age: number
|
|
||||||
}`,
|
|
||||||
expected: { definitions: 1, facts: 0, evidence: 0, measures: 0 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '2. Definitions with Arrays',
|
|
||||||
dsl: `
|
|
||||||
definition User {
|
|
||||||
name: string
|
|
||||||
roles: string[]
|
|
||||||
permissions: Permission[]
|
|
||||||
}`,
|
|
||||||
expected: { definitions: 1, facts: 0, evidence: 0, measures: 0 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '3. Definitions with Behaviors',
|
|
||||||
dsl: `
|
|
||||||
definition User {
|
|
||||||
name: string
|
|
||||||
lastActive: timestamp BEHAVES {
|
|
||||||
decaying down hourly
|
|
||||||
}
|
|
||||||
score: number BEHAVES {
|
|
||||||
blurring adaptive confidence_95
|
|
||||||
}
|
|
||||||
session: string BEHAVES {
|
|
||||||
ttl 24h
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
expected: { definitions: 1, facts: 0, evidence: 0, measures: 0 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '4. Definitions with Cache Directives',
|
|
||||||
dsl: `
|
|
||||||
definition User {
|
|
||||||
name: string
|
|
||||||
lastActive: timestamp BEHAVES {
|
|
||||||
decaying down hourly
|
|
||||||
} CACHE lazy
|
|
||||||
score: number BEHAVES {
|
|
||||||
blurring adaptive confidence_95
|
|
||||||
} CACHE eager
|
|
||||||
}`,
|
|
||||||
expected: { definitions: 1, facts: 0, evidence: 0, measures: 0 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '5. Basic Facts',
|
|
||||||
dsl: `
|
|
||||||
definition User {
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasRole(user: User, role: string)
|
|
||||||
fact isActive(user: User)`,
|
|
||||||
expected: { definitions: 1, facts: 2, evidence: 0, measures: 0 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '6. Facts with Properties',
|
|
||||||
dsl: `
|
|
||||||
definition User {
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasRole(user: User, role: string) CACHE eager
|
|
||||||
fact isMember(user: User, group: Group) transitive CACHE lazy
|
|
||||||
fact isFriend(user: User, friend: User) symmetrical`,
|
|
||||||
expected: { definitions: 1, facts: 3, evidence: 0, measures: 0 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '7. Facts with Limits',
|
|
||||||
dsl: `
|
|
||||||
definition User {
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
fact isMember(user: User, group: Group) transitive limit 10
|
|
||||||
fact isFriend(user: User, friend: User) symmetrical limit 100`,
|
|
||||||
expected: { definitions: 1, facts: 2, evidence: 0, measures: 0 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '8. Simple Evidence',
|
|
||||||
dsl: `
|
|
||||||
definition User {
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasRole(user: User, role: string)
|
|
||||||
|
|
||||||
evidence canRead(user: User, doc: Document) {
|
|
||||||
hasRole(user, 'admin')
|
|
||||||
}`,
|
|
||||||
expected: { definitions: 1, facts: 1, evidence: 1, measures: 0 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '9. Evidence with Multiple Statements',
|
|
||||||
dsl: `
|
|
||||||
definition User {
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasRole(user: User, role: string)
|
|
||||||
fact owns(user: User, doc: Document)
|
|
||||||
|
|
||||||
evidence canRead(user: User, doc: Document) {
|
|
||||||
owns(user, doc)
|
|
||||||
hasRole(user, 'admin')
|
|
||||||
}`,
|
|
||||||
expected: { definitions: 1, facts: 2, evidence: 1, measures: 0 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '10. Evidence with ALWAYS',
|
|
||||||
dsl: `
|
|
||||||
definition User {
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
fact isActive(user: User)
|
|
||||||
|
|
||||||
evidence canRead(user: User, doc: Document) {
|
|
||||||
ALWAYS isActive(user)
|
|
||||||
}`,
|
|
||||||
expected: { definitions: 1, facts: 1, evidence: 1, measures: 0 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '11. Evidence with REQUIRES',
|
|
||||||
dsl: `
|
|
||||||
definition User {
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasClearance(user: User, level: string)
|
|
||||||
|
|
||||||
evidence canRead(user: User, doc: Document) {
|
|
||||||
REQUIRES hasClearance(user, doc.level)
|
|
||||||
}`,
|
|
||||||
expected: { definitions: 1, facts: 1, evidence: 1, measures: 0 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '12. Evidence with WHEN',
|
|
||||||
dsl: `
|
|
||||||
definition User {
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasRole(user: User, role: string)
|
|
||||||
|
|
||||||
evidence canRead(user: User, doc: Document) {
|
|
||||||
WHEN hasRole(user, 'admin')
|
|
||||||
}`,
|
|
||||||
expected: { definitions: 1, facts: 1, evidence: 1, measures: 0 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '13. Evidence with WHEN UNLESS',
|
|
||||||
dsl: `
|
|
||||||
definition User {
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasRole(user: User, role: string)
|
|
||||||
fact isSuspended(user: User)
|
|
||||||
|
|
||||||
evidence canRead(user: User, doc: Document) {
|
|
||||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
|
||||||
}`,
|
|
||||||
expected: { definitions: 1, facts: 2, evidence: 1, measures: 0 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '14. Evidence with Pattern Matching',
|
|
||||||
dsl: `
|
|
||||||
definition User {
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
fact isMember(user: User, group: Group)
|
|
||||||
|
|
||||||
evidence canRead(user: User, doc: Document) {
|
|
||||||
isMember(user, *group) {
|
|
||||||
canRead(group, doc)
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
expected: { definitions: 1, facts: 1, evidence: 1, measures: 0 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '15. Evidence with Pattern Matching and Limits',
|
|
||||||
dsl: `
|
|
||||||
definition User {
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
fact isMember(user: User, group: Group)
|
|
||||||
|
|
||||||
evidence canRead(user: User, doc: Document) {
|
|
||||||
isMember(user, *group) {
|
|
||||||
canRead(group, doc)
|
|
||||||
} limit 5
|
|
||||||
}`,
|
|
||||||
expected: { definitions: 1, facts: 1, evidence: 1, measures: 0 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '16. Evidence with Fusion',
|
|
||||||
dsl: `
|
|
||||||
definition User {
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasRole(user: User, role: string)
|
|
||||||
fact isMember(user: User, group: Group)
|
|
||||||
|
|
||||||
evidence canRead(user: User, doc: Document) {
|
|
||||||
fusion max {
|
|
||||||
hasRole(user, 'admin')
|
|
||||||
isMember(user, *group) {
|
|
||||||
canRead(group, doc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
expected: { definitions: 1, facts: 2, evidence: 1, measures: 0 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '17. Basic Measures',
|
|
||||||
dsl: `
|
|
||||||
definition User {
|
|
||||||
name: string
|
|
||||||
role: string
|
|
||||||
}
|
|
||||||
|
|
||||||
measure userRole(user: User) {
|
|
||||||
user.role
|
|
||||||
} PROVIDES string`,
|
|
||||||
expected: { definitions: 1, facts: 0, evidence: 0, measures: 1 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '18. Measures with Fusion',
|
|
||||||
dsl: `
|
|
||||||
definition User {
|
|
||||||
name: string
|
|
||||||
role: string
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasRole(user: User, role: string)
|
|
||||||
|
|
||||||
measure userPermissions(user: User) {
|
|
||||||
fusion max {
|
|
||||||
user.role.permissions
|
|
||||||
hasRole(user, 'admin')
|
|
||||||
}
|
|
||||||
} PROVIDES Permission[]`,
|
|
||||||
expected: { definitions: 1, facts: 1, evidence: 0, measures: 1 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '19. Complete Example',
|
|
||||||
dsl: `
|
|
||||||
definition User {
|
|
||||||
role: string
|
|
||||||
isActive: boolean
|
|
||||||
lastActive: timestamp BEHAVES {
|
|
||||||
decaying down hourly
|
|
||||||
} CACHE lazy
|
|
||||||
}
|
|
||||||
|
|
||||||
definition Document {
|
|
||||||
level: string
|
|
||||||
owner: User
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasRole(user: User, role: string) CACHE eager
|
|
||||||
fact owns(user: User, doc: Document) CACHE eager
|
|
||||||
|
|
||||||
evidence canRead(user: User, doc: Document) {
|
|
||||||
owns(user, doc)
|
|
||||||
|
|
||||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
measure userRole(user: User) {
|
|
||||||
user.role
|
|
||||||
} PROVIDES string`,
|
|
||||||
expected: { definitions: 2, facts: 2, evidence: 1, measures: 1 }
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
let passed = 0;
|
|
||||||
let failed = 0;
|
|
||||||
|
|
||||||
for (const test of tests) {
|
|
||||||
console.log(`\n--- ${test.name} ---`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = compiler.compile(test.dsl, `test-${passed + failed + 1}`);
|
|
||||||
|
|
||||||
const actual = {
|
|
||||||
definitions: result.program.definitions.length,
|
|
||||||
facts: result.program.facts.length,
|
|
||||||
evidence: result.program.evidence.length,
|
|
||||||
measures: result.program.measures.length
|
|
||||||
};
|
|
||||||
|
|
||||||
const success = result.success &&
|
|
||||||
actual.definitions === test.expected.definitions &&
|
|
||||||
actual.facts === test.expected.facts &&
|
|
||||||
actual.evidence === test.expected.evidence &&
|
|
||||||
actual.measures === test.expected.measures;
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
console.log('✅ PASSED');
|
|
||||||
console.log(` Definitions: ${actual.definitions}, Facts: ${actual.facts}, Evidence: ${actual.evidence}, Measures: ${actual.measures}`);
|
|
||||||
passed++;
|
|
||||||
} else {
|
|
||||||
console.log('❌ FAILED');
|
|
||||||
console.log(` Expected: ${JSON.stringify(test.expected)}`);
|
|
||||||
console.log(` Actual: ${JSON.stringify(actual)}`);
|
|
||||||
if (result.errors.length > 0) {
|
|
||||||
console.log(` Errors: ${result.errors.join(', ')}`);
|
|
||||||
}
|
|
||||||
failed++;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('❌ FAILED');
|
|
||||||
console.log(` Error: ${error.message}`);
|
|
||||||
failed++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\n=== Test Suite Results ===`);
|
|
||||||
console.log(`Total Tests: ${passed + failed}`);
|
|
||||||
console.log(`Passed: ${passed}`);
|
|
||||||
console.log(`Failed: ${failed}`);
|
|
||||||
console.log(`Success Rate: ${((passed / (passed + failed)) * 100).toFixed(1)}%`);
|
|
||||||
|
|
||||||
return { passed, failed, total: passed + failed };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the test suite
|
|
||||||
runProgressiveTestSuite();
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import { DSLCompiler } from '../DSLCompiler.js';
|
|
||||||
import { Arbiter } from '../../core/Arbiter.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test with simple evidence containing predicate calls
|
|
||||||
*/
|
|
||||||
export function runSimpleEvidenceTest() {
|
|
||||||
console.log('=== Simple Evidence Test ===\n');
|
|
||||||
|
|
||||||
// Create arbiter instance
|
|
||||||
const arbiter = new Arbiter();
|
|
||||||
|
|
||||||
// Create compiler
|
|
||||||
const compiler = new DSLCompiler(arbiter);
|
|
||||||
|
|
||||||
// Simple evidence with predicate call
|
|
||||||
const dsl = `
|
|
||||||
definition User {
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasRole(user: User, role: string)
|
|
||||||
|
|
||||||
evidence canRead(user: User, doc: Document) {
|
|
||||||
hasRole(user, 'admin')
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
console.log('1. Compiling DSL with simple evidence...');
|
|
||||||
try {
|
|
||||||
const result = compiler.compile(dsl, 'simple-evidence');
|
|
||||||
|
|
||||||
console.log('Result:', result.success ? 'SUCCESS' : 'FAILED');
|
|
||||||
console.log('Generated rules:', result.generatedRules.size);
|
|
||||||
console.log('Program evidence:', result.program.evidence.length);
|
|
||||||
|
|
||||||
if (result.errors.length > 0) {
|
|
||||||
console.log('Errors:', result.errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.warnings.length > 0) {
|
|
||||||
console.log('Warnings:', result.warnings);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Compilation failed:', error.message);
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the test
|
|
||||||
runSimpleEvidenceTest();
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,420 +0,0 @@
|
|||||||
/*
|
|
||||||
* Peggy Parser for the Evidence DSL
|
|
||||||
*
|
|
||||||
* This grammar defines a declarative language for authorization policies.
|
|
||||||
* It parses definitions, facts, measures, and evidence rules into a structured
|
|
||||||
* Abstract Syntax Tree (AST) represented by plain JavaScript objects.
|
|
||||||
* (Version 4: Corrected infinite loop check in String literal parsing)
|
|
||||||
*/
|
|
||||||
{
|
|
||||||
// The location() function provides line/column info for error reporting.
|
|
||||||
// The text() function returns the matched text for a rule.
|
|
||||||
|
|
||||||
// Helper function to build a left-associative binary expression tree.
|
|
||||||
function buildLeftAssoc(head, tail) {
|
|
||||||
return tail.reduce((result, element) => {
|
|
||||||
return {
|
|
||||||
type: "BinaryExpression",
|
|
||||||
operator: element[1],
|
|
||||||
left: result,
|
|
||||||
right: element[3],
|
|
||||||
location: location()
|
|
||||||
};
|
|
||||||
}, head);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- Grammar Entry Point --
|
|
||||||
Program
|
|
||||||
= _ statements:(Statement _)* _ {
|
|
||||||
const allStatements = statements.map(s => s[0]);
|
|
||||||
return {
|
|
||||||
type: "Program",
|
|
||||||
body: allStatements,
|
|
||||||
definitions: allStatements.filter(s => s.type === "Definition"),
|
|
||||||
facts: allStatements.filter(s => s.type === "Fact"),
|
|
||||||
evidence: allStatements.filter(s => s.type === "Evidence"),
|
|
||||||
measures: allStatements.filter(s => s.type === "Measure"),
|
|
||||||
sources: allStatements.filter(s => s.type === "Source")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Statement
|
|
||||||
= Definition
|
|
||||||
/ Source
|
|
||||||
/ Fact
|
|
||||||
/ Evidence
|
|
||||||
/ Measure
|
|
||||||
|
|
||||||
// -- Top-Level Statements --
|
|
||||||
|
|
||||||
Definition "A type definition"
|
|
||||||
= ("definition" / "type") __ name:Identifier __ "{" _ fields:(Field _)* "}" {
|
|
||||||
return { type: "Definition", name, fields: fields.map(f => f[0]) };
|
|
||||||
}
|
|
||||||
|
|
||||||
Field
|
|
||||||
= name:Identifier _ ":" _ fieldType:Type _ isArray:("[]")? _ behavior:Behavior? _ cache:CacheDirective? {
|
|
||||||
return {
|
|
||||||
type: "Field",
|
|
||||||
name,
|
|
||||||
fieldType,
|
|
||||||
isArray: !!isArray,
|
|
||||||
behavior: behavior || null,
|
|
||||||
cache: cache || null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Fact "A statement of fact (or relation in ADR-000)"
|
|
||||||
= ("fact" / "relation") __ star:"*"? name:Identifier _ "(" _ params:ParameterList? _ ")" _ behavior:BehaviorAnnotation? _ properties:(FactProperty _)* cache:CacheDirective? _ limit:Limit? {
|
|
||||||
return {
|
|
||||||
type: "Fact",
|
|
||||||
name,
|
|
||||||
params: params || [],
|
|
||||||
behavior: behavior || null,
|
|
||||||
properties: properties.map(p => p[0]),
|
|
||||||
cache: cache || null,
|
|
||||||
limit: limit || null,
|
|
||||||
injectable: !!star
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Source "An injectable source (proof provider)"
|
|
||||||
= "source" __ star:"*"? name:Identifier _ "(" _ params:ParameterList? _ ")" _ provides:Provides? _ within:WithinClause? {
|
|
||||||
return {
|
|
||||||
type: "Source",
|
|
||||||
name,
|
|
||||||
params: params || [],
|
|
||||||
provides: provides || null,
|
|
||||||
injectable: !!star,
|
|
||||||
within: within || null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
WithinClause "A freshness constraint on a source"
|
|
||||||
= "within" __ duration:Duration { return duration; }
|
|
||||||
|
|
||||||
Evidence "An evidence rule"
|
|
||||||
= "evidence" __ star:"*"? name:Identifier _ "(" _ params:ParameterList? _ ")" _ limit:Limit? _ "{" _ body:EvidenceBody _ "}" _ provides:Provides? {
|
|
||||||
return {
|
|
||||||
type: "Evidence",
|
|
||||||
name,
|
|
||||||
params: params || [],
|
|
||||||
limit: limit || null,
|
|
||||||
body,
|
|
||||||
provides: provides || null,
|
|
||||||
challenge: !!star
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Measure "A derived measurement or value"
|
|
||||||
= "measure" __ name:Identifier _ "(" _ params:ParameterList? _ ")" _ "{" _ body:MeasureBody _ "}" _ provides:Provides? {
|
|
||||||
return {
|
|
||||||
type: "Measure",
|
|
||||||
name,
|
|
||||||
params: params || [],
|
|
||||||
body,
|
|
||||||
provides: provides || null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- Evidence & Measure Internals --
|
|
||||||
|
|
||||||
EvidenceBody
|
|
||||||
= statements:(EvidenceStatement _)* {
|
|
||||||
return { type: "EvidenceBody", statements: statements.map(s => s[0]) };
|
|
||||||
}
|
|
||||||
|
|
||||||
EvidenceStatement
|
|
||||||
= DefeasibleLogic
|
|
||||||
/ Fusion
|
|
||||||
/ CollectionProcessing
|
|
||||||
/ PatternMatch
|
|
||||||
/ Expression
|
|
||||||
|
|
||||||
MeasureBody
|
|
||||||
= statements:(MeasureStatement _)* returnStmt:ReturnStatement? {
|
|
||||||
return {
|
|
||||||
type: "MeasureBody",
|
|
||||||
statements: statements.map(s => s[0]),
|
|
||||||
returnStatement: returnStmt || null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
MeasureStatement
|
|
||||||
= Fusion
|
|
||||||
/ Aggregation
|
|
||||||
/ PatternMatch
|
|
||||||
/ Expression
|
|
||||||
|
|
||||||
ReturnStatement
|
|
||||||
= "return" __ expression:Expression {
|
|
||||||
return { type: "ReturnStatement", expression };
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- Complex Statement Types --
|
|
||||||
|
|
||||||
DefeasibleLogic
|
|
||||||
= type:("NEVER" / "ALWAYS" / "REQUIRES") __ condition:Expression {
|
|
||||||
return { type: "DefeasibleLogic", logicType: type, condition };
|
|
||||||
}
|
|
||||||
/ "WHEN" __ condition:Expression __ "UNLESS" __ defeater:Expression {
|
|
||||||
return { type: "DefeasibleLogic", logicType: "WHEN", condition, defeater };
|
|
||||||
}
|
|
||||||
/ "WHEN" __ condition:Expression {
|
|
||||||
return { type: "DefeasibleLogic", logicType: "WHEN", condition };
|
|
||||||
}
|
|
||||||
|
|
||||||
PatternMatch
|
|
||||||
= predicate:PatternPredicate _ binding:BindingClause? _ "{" _ body:EvidenceBody _ "}" _ limit:Limit? _ withClause:WithClause? {
|
|
||||||
return {
|
|
||||||
type: "PatternMatch",
|
|
||||||
predicate,
|
|
||||||
binding: binding || null,
|
|
||||||
limit: limit || null,
|
|
||||||
body,
|
|
||||||
withClause: withClause || null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
CollectionProcessing
|
|
||||||
= measure:Expression _ "|" _ variable:Identifier _ "|" _ fusionStrategy:("fusion" __ strategy:Identifier)? _ "{" _ body:EvidenceBody _ "}" _ limit:Limit? {
|
|
||||||
return {
|
|
||||||
type: "CollectionProcessing",
|
|
||||||
measure,
|
|
||||||
variable,
|
|
||||||
fusion: fusionStrategy ? { strategy: fusionStrategy[1] } : null,
|
|
||||||
body,
|
|
||||||
limit: limit || null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
PatternPredicate
|
|
||||||
= name:Identifier _ "(" _ args:PatternArgumentList? _ ")" {
|
|
||||||
return { type: "Predicate", name, args: args || [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
PatternArgumentList
|
|
||||||
= head:PatternArgument tail:(_ "," _ arg:PatternArgument)* {
|
|
||||||
return [head, ...tail.map(t => t[3])];
|
|
||||||
}
|
|
||||||
|
|
||||||
PatternArgument
|
|
||||||
= "*" _ name:Identifier { return { type: "Wildcard", name }; }
|
|
||||||
/ Expression
|
|
||||||
|
|
||||||
BindingClause
|
|
||||||
= "|" _ name:Identifier _ "|" { return name; }
|
|
||||||
|
|
||||||
WithClause
|
|
||||||
= "with" __ condition:Expression { return condition; }
|
|
||||||
|
|
||||||
Fusion
|
|
||||||
= "fusion" __ strategy:Identifier __ "{" _ expressions:ExpressionList _ "}" {
|
|
||||||
return { type: "Fusion", strategy, expressions };
|
|
||||||
}
|
|
||||||
|
|
||||||
Aggregation
|
|
||||||
= "aggregate" __ "{" _ expressions:ExpressionList _ "}" _ using:Using? {
|
|
||||||
return { type: "Aggregation", expressions, using: using || null };
|
|
||||||
}
|
|
||||||
|
|
||||||
Using
|
|
||||||
= "USING" __ method:Identifier { return method; }
|
|
||||||
|
|
||||||
// -- Type System & Parameters --
|
|
||||||
|
|
||||||
Type
|
|
||||||
= Identifier
|
|
||||||
|
|
||||||
TypeName
|
|
||||||
= name:Identifier { return { type: "TypeName", name }; }
|
|
||||||
/ literal:String { return { type: "TypeName", name: literal.value }; }
|
|
||||||
|
|
||||||
ParameterList
|
|
||||||
= head:Parameter tail:(_ "," _ param:Parameter)* {
|
|
||||||
return [head, ...tail.map(t => t[3])];
|
|
||||||
}
|
|
||||||
|
|
||||||
Parameter
|
|
||||||
= name:Identifier _ ":" _ paramType:Type _ isArray:("[]")? {
|
|
||||||
return { type: "Parameter", name, paramType, isArray: !!isArray };
|
|
||||||
}
|
|
||||||
|
|
||||||
Provides
|
|
||||||
= "PROVIDES" __ providesType:Type { return providesType; }
|
|
||||||
|
|
||||||
BehaviorAnnotation
|
|
||||||
= "BEHAVES" __ "AS" __ behavior:("edge" / "transitive" / "hierarchical" / "symmetrical_graph") {
|
|
||||||
return { type: "BehaviorAnnotation", behavior };
|
|
||||||
}
|
|
||||||
|
|
||||||
FactProperty
|
|
||||||
= "transitive" { return "transitive"; }
|
|
||||||
/ "symmetrical" { return "symmetrical"; }
|
|
||||||
|
|
||||||
Limit
|
|
||||||
= "limit" __ value:Integer { return value; }
|
|
||||||
|
|
||||||
// -- Behaviors and Caching --
|
|
||||||
|
|
||||||
Behavior
|
|
||||||
= "BEHAVES" __ "{" _ b:(DecayBehavior / BlurBehavior / TTLBehavior) _ "}" { return b; }
|
|
||||||
|
|
||||||
DecayBehavior
|
|
||||||
= "decaying" __ direction:("up" / "down" / "neutral" / "stable") __ period:("hourly" / "daily" / "weekly" / "monthly") {
|
|
||||||
return { type: "Behavior", behaviorType: "decay", direction, period };
|
|
||||||
}
|
|
||||||
|
|
||||||
BlurBehavior
|
|
||||||
= "blurring" __ mode:("fixed" / "adaptive" / "confidence") confidence:(__ ("confidence_90" / "confidence_95" / "confidence_99"))? {
|
|
||||||
return { type: "Behavior", behaviorType: "blur", mode, confidence: confidence ? confidence[1] : null };
|
|
||||||
}
|
|
||||||
|
|
||||||
TTLBehavior
|
|
||||||
= "ttl" __ duration:Duration {
|
|
||||||
return { type: "Behavior", behaviorType: "ttl", duration };
|
|
||||||
}
|
|
||||||
|
|
||||||
CacheDirective
|
|
||||||
= "CACHE" __ directive:("eager" / "lazy") { return directive; }
|
|
||||||
|
|
||||||
// -- Expressions (with operator precedence) --
|
|
||||||
|
|
||||||
Expression
|
|
||||||
= LogicalOr
|
|
||||||
|
|
||||||
LogicalOr
|
|
||||||
= head:LogicalAnd tail:(_ "||" _ right:LogicalAnd)* { return buildLeftAssoc(head, tail); }
|
|
||||||
|
|
||||||
LogicalAnd
|
|
||||||
= head:Comparison tail:(_ "&&" _ right:Comparison)* { return buildLeftAssoc(head, tail); }
|
|
||||||
|
|
||||||
Comparison
|
|
||||||
= head:TemporalComparison _ "is" __ typeName:TypeName {
|
|
||||||
return { type: "BinaryExpression", operator: "is", left: head, right: typeName };
|
|
||||||
}
|
|
||||||
/ head:TemporalComparison tail:(_ operator:("==" / "!=" / ">=" / "<=" / ">" / "<") _ right:TemporalComparison)* { return buildLeftAssoc(head, tail); }
|
|
||||||
|
|
||||||
TemporalComparison
|
|
||||||
= head:Addition _ "within" __ right:Duration {
|
|
||||||
return { type: "BinaryExpression", operator: "within", left: head, right };
|
|
||||||
}
|
|
||||||
/ Addition
|
|
||||||
|
|
||||||
Addition
|
|
||||||
= head:Multiplication tail:(_i operator:("+" / "-") _i right:Multiplication)* { return buildLeftAssoc(head, tail); }
|
|
||||||
|
|
||||||
Multiplication
|
|
||||||
= head:Unary tail:(_i operator:("*" / "/") _i right:Unary)* { return buildLeftAssoc(head, tail); }
|
|
||||||
|
|
||||||
Unary
|
|
||||||
= operator:("NOT" / "!") __ operand:Unary { return { type: "UnaryExpression", operator: "NOT", operand }; }
|
|
||||||
/ Postfix
|
|
||||||
|
|
||||||
Postfix
|
|
||||||
= primary:(AttributeAccess / PrimaryTerm) binding:BindingClause? {
|
|
||||||
if (binding) {
|
|
||||||
return { type: "BindingAccess", expression: primary, binding };
|
|
||||||
}
|
|
||||||
return primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
AttributeAccess
|
|
||||||
= head:PrimaryTerm tail:(_ "." _ attr:Identifier)+ {
|
|
||||||
return tail.reduce((obj, part) => {
|
|
||||||
return {
|
|
||||||
type: "AttributeAccess",
|
|
||||||
object: obj,
|
|
||||||
attribute: part[3], // The Identifier is the 4th element (index 3)
|
|
||||||
location: location()
|
|
||||||
};
|
|
||||||
}, head);
|
|
||||||
}
|
|
||||||
|
|
||||||
PrimaryTerm "The non-recursive base for an expression chain"
|
|
||||||
= ChallengePredicate
|
|
||||||
/ Literal
|
|
||||||
/ PredicateCall
|
|
||||||
/ Variable
|
|
||||||
/ "(" _ expr:Expression _ ")" { return expr; }
|
|
||||||
|
|
||||||
ChallengePredicate
|
|
||||||
= "*" name:Identifier _ "(" _ args:ArgumentList? _ ")" {
|
|
||||||
return { type: "PredicateCall", name, args: args || [], challenge: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
PredicateCall
|
|
||||||
= name:Identifier "(" _ args:ArgumentList? _ ")" {
|
|
||||||
return { type: "PredicateCall", name, args: args || [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
Variable
|
|
||||||
= name:Identifier { return { type: "Variable", name }; }
|
|
||||||
|
|
||||||
ArgumentList
|
|
||||||
= head:Expression tail:(_ "," _ expr:Expression)* {
|
|
||||||
return [head, ...tail.map(t => t[3])];
|
|
||||||
}
|
|
||||||
|
|
||||||
ExpressionList
|
|
||||||
= head:Expression tail:(_ "," _ expr:Expression)* {
|
|
||||||
return [head, ...tail.map(t => t[3])];
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- Literals --
|
|
||||||
|
|
||||||
Literal
|
|
||||||
= String / Float / Integer / Boolean / Duration
|
|
||||||
|
|
||||||
String "A string literal"
|
|
||||||
= '"' chars:((!("\"" / "\\")) . / "\\" .)* '"' {
|
|
||||||
return { type: "Literal", value: JSON.parse(text()) };
|
|
||||||
}
|
|
||||||
/ "'" chars:((!("'" / "\\")) . / "\\" .)* "'" {
|
|
||||||
return { type: "Literal", value: JSON.parse("\"" + chars.map(c => c[0] === '\\' ? c[1] : c[1]).join('') + "\"") };
|
|
||||||
}
|
|
||||||
|
|
||||||
Float "A floating-point number"
|
|
||||||
= value:([0-9]+ "." [0-9]+) { return { type: "Literal", value: parseFloat(text()) }; }
|
|
||||||
|
|
||||||
Integer "An integer"
|
|
||||||
= value:[0-9]+ { return { type: "Literal", value: parseInt(text(), 10) }; }
|
|
||||||
|
|
||||||
Boolean "A boolean literal"
|
|
||||||
= value:("true" / "false") { return { type: "Literal", value: value === "true" }; }
|
|
||||||
|
|
||||||
Duration "A time duration literal"
|
|
||||||
= value:([0-9]+ ("h" / "d" / "w" / "m")) { return { type: "Literal", value: text(), unit: text().slice(-1) }; }
|
|
||||||
|
|
||||||
|
|
||||||
// -- Core Tokens & Whitespace --
|
|
||||||
|
|
||||||
Identifier
|
|
||||||
= !Keyword name:$([a-zA-Z_][a-zA-Z0-9_]*) { return name; }
|
|
||||||
|
|
||||||
Keyword
|
|
||||||
= ("definition" / "type" / "fact" / "relation" / "evidence" / "measure" / "BEHAVES" / "AS" / "CACHE"
|
|
||||||
/ "decaying" / "blurring" / "ttl" / "transitive" / "symmetrical" / "hierarchical" / "symmetrical_graph" / "edge" / "limit"
|
|
||||||
/ "PROVIDES" / "fusion" / "aggregate" / "USING" / "NEVER" / "ALWAYS" / "WHEN" / "UNLESS"
|
|
||||||
/ "REQUIRES" / "with" / "true" / "false" / "NOT" / "within" / "return" / "is") !([a-zA-Z0-9_])
|
|
||||||
|
|
||||||
// _ = optional whitespace and comments
|
|
||||||
// __ = mandatory whitespace and comments
|
|
||||||
_
|
|
||||||
= (WhiteSpace / Comment)*
|
|
||||||
|
|
||||||
// Inline (single-line) optional whitespace — used around arithmetic
|
|
||||||
// operators so a `*` challenge-predicate on the next line is not
|
|
||||||
// absorbed as a multiplication tail.
|
|
||||||
_i
|
|
||||||
= [ \t]*
|
|
||||||
__
|
|
||||||
= (WhiteSpace / Comment)+
|
|
||||||
|
|
||||||
WhiteSpace
|
|
||||||
= [ \t\r\n]
|
|
||||||
|
|
||||||
Comment
|
|
||||||
= "//" [^\r\n]*
|
|
||||||
/ "/*" (!"*/" .)* "*/"
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
// Inline Expression Grammar for Permission Checking
|
|
||||||
//
|
|
||||||
// This grammar parses inline DSL expressions used for permission checks.
|
|
||||||
// It supports predicate calls, OWA Fusion blocks (exclusive composition),
|
|
||||||
// challenge predicates (* prefix for out-of-band), and defeasible logic (UNLESS).
|
|
||||||
// AND/OR operators removed — OWA Fusion is the only composition mechanism.
|
|
||||||
//
|
|
||||||
// Usage: npx peggy -o src/ast/parser/ExpressionParser.js src/ast/grammar/expression.peggy
|
|
||||||
|
|
||||||
{
|
|
||||||
// Helper functions
|
|
||||||
function makeVariable(name, path) {
|
|
||||||
return { type: 'Variable', name, path: path || [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
function makePredicate(name, args) {
|
|
||||||
return { type: 'Predicate', name, args: args || [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeChallengePredicate(name, args) {
|
|
||||||
return { type: 'Predicate', name, args: args || [], challenge: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeFusion(expressions, aggregator) {
|
|
||||||
return { type: 'Fusion', aggregator, expressions };
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeDefeasible(primary, exception) {
|
|
||||||
return { type: 'Defeasible', primary, exception };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Entry point
|
|
||||||
Expression
|
|
||||||
= _ expr:DefeasibleExpr _ { return expr; }
|
|
||||||
|
|
||||||
// Defeasible logic: primary UNLESS exception
|
|
||||||
DefeasibleExpr
|
|
||||||
= primary:PrimaryExpr _ "UNLESS" _ exception:PredicateCall {
|
|
||||||
return makeDefeasible(primary, exception);
|
|
||||||
}
|
|
||||||
/ PrimaryExpr
|
|
||||||
|
|
||||||
// Primary expressions: Fusion blocks or predicate calls
|
|
||||||
PrimaryExpr
|
|
||||||
= FusionBlock
|
|
||||||
/ ChallengePredicate
|
|
||||||
/ PredicateCall
|
|
||||||
|
|
||||||
// OWA Fusion block: FUSION <aggregator> { expr1 expr2 ... }
|
|
||||||
FusionBlock
|
|
||||||
= "FUSION" _ aggregator:AggregatorKeyword _ "{" _ expressions:ExpressionList _ "}" {
|
|
||||||
return makeFusion(expressions, aggregator);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aggregator keywords (subset of ADR-000 DSL v2 aggregators)
|
|
||||||
AggregatorKeyword
|
|
||||||
= "max" / "min" / "majority" / "average" / "sum" / "sum_unbounded"
|
|
||||||
/ "median" / "optimistic" / "pessimistic" / "top2" / "top3" / "priority"
|
|
||||||
|
|
||||||
// List of expressions (whitespace-separated)
|
|
||||||
ExpressionList
|
|
||||||
= head:Expression tail:(_ Expression)* {
|
|
||||||
const exprs = [head];
|
|
||||||
for (const t of tail) {
|
|
||||||
exprs.push(t[1]);
|
|
||||||
}
|
|
||||||
return exprs;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Challenge predicate (* prefixed): *name(arg1, arg2, ...)
|
|
||||||
ChallengePredicate
|
|
||||||
= "*" name:Identifier _ "(" _ args:ArgumentList? _ ")" {
|
|
||||||
return makeChallengePredicate(name, args || []);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Predicate call: name(arg1, arg2, ...)
|
|
||||||
PredicateCall
|
|
||||||
= name:Identifier _ "(" _ args:ArgumentList? _ ")" {
|
|
||||||
return makePredicate(name, args || []);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comma-separated arguments
|
|
||||||
ArgumentList
|
|
||||||
= head:Argument tail:(_ "," _ Argument)* {
|
|
||||||
const args = [head];
|
|
||||||
for (const t of tail) {
|
|
||||||
args.push(t[3]);
|
|
||||||
}
|
|
||||||
return args;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Argument types
|
|
||||||
// Order matters: try VariableBinding first (starts with :),
|
|
||||||
// then Literal (strings/numbers), then TypedReference (which looks like an identifier)
|
|
||||||
Argument
|
|
||||||
= VariableBinding
|
|
||||||
/ Literal
|
|
||||||
/ TypedReference
|
|
||||||
|
|
||||||
// Variable binding: :name or :name.path.subpath
|
|
||||||
VariableBinding
|
|
||||||
= ":" name:Identifier path:("." Identifier)* {
|
|
||||||
return makeVariable(name, path.map(p => p[1]));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Typed reference: Type::path.subpath (e.g., document::params.id)
|
|
||||||
TypedReference
|
|
||||||
= refType:Identifier "::" path:Path {
|
|
||||||
return { type: 'Reference', refType: refType, path: path };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Path for typed references
|
|
||||||
Path
|
|
||||||
= head:Identifier tail:("." Identifier)* {
|
|
||||||
const parts = [head];
|
|
||||||
for (const t of tail) {
|
|
||||||
parts.push(t[1]);
|
|
||||||
}
|
|
||||||
return parts;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Literals
|
|
||||||
Literal
|
|
||||||
= StringLiteral
|
|
||||||
/ NumberLiteral
|
|
||||||
|
|
||||||
// String literals (single or double quoted)
|
|
||||||
StringLiteral
|
|
||||||
= '"' chars:([^"\\] / EscapeSequence)* '"' {
|
|
||||||
return { type: 'Literal', value: chars.join(''), dataType: 'string' };
|
|
||||||
}
|
|
||||||
/ "'" chars:([^'\\] / EscapeSequence)* "'" {
|
|
||||||
return { type: 'Literal', value: chars.join(''), dataType: 'string' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Escape sequences
|
|
||||||
EscapeSequence
|
|
||||||
= "\\" char:["'\\nrt] {
|
|
||||||
const escapes = { '"': '"', "'": "'", '\\': '\\', 'n': '\n', 'r': '\r', 't': '\t' };
|
|
||||||
return escapes[char] || char;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Number literals
|
|
||||||
NumberLiteral
|
|
||||||
= digits:([0-9]+) {
|
|
||||||
return { type: 'Literal', value: parseInt(digits.join(''), 10), dataType: 'number' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Identifiers (support hyphens like doc-123, user-456)
|
|
||||||
Identifier
|
|
||||||
= first:[a-zA-Z_] rest:[a-zA-Z0-9_-]* {
|
|
||||||
return first + rest.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Whitespace and comments
|
|
||||||
_ "whitespace"
|
|
||||||
= (WS / LineComment / BlockComment)*
|
|
||||||
|
|
||||||
WS
|
|
||||||
= [ \t\n\r]+
|
|
||||||
|
|
||||||
LineComment
|
|
||||||
= "//" [^\n]*
|
|
||||||
|
|
||||||
BlockComment
|
|
||||||
= "/*" (!"*/" .)* "*/"
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
/**
|
|
||||||
* AST Module - Main export file
|
|
||||||
* Provides access to all AST functionality for DSL compilation
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Core AST components
|
|
||||||
export { DSLCompiler } from './DSLCompiler.js';
|
|
||||||
|
|
||||||
// Parser
|
|
||||||
export { PeggyDSLParser } from './parser/PeggyDSLParser.js';
|
|
||||||
|
|
||||||
// Generator
|
|
||||||
export { RuleGenerator } from './generator/RuleGenerator.js';
|
|
||||||
|
|
||||||
// Validation
|
|
||||||
export { validateDslText } from './validation/DSLValidation.js';
|
|
||||||
|
|
||||||
// All AST nodes
|
|
||||||
export * from './nodes/index.js';
|
|
||||||
|
|
||||||
// Re-export for convenience
|
|
||||||
export { DSLCompiler as default } from './DSLCompiler.js';
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
/**
|
|
||||||
* Built-in DSL Functions
|
|
||||||
*
|
|
||||||
* Native functions for use in DSL expressions
|
|
||||||
* Includes IP operations, time functions, string utilities
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Use optimized fast versions for hot paths
|
|
||||||
import {
|
|
||||||
isIpInCidrFast,
|
|
||||||
isPrivateIpFast,
|
|
||||||
ipToIntFast,
|
|
||||||
isIPv4Fast
|
|
||||||
} from '../../utils/ip-utils-fast.js';
|
|
||||||
import {
|
|
||||||
isIPv6,
|
|
||||||
isLoopbackIp,
|
|
||||||
ipEquals,
|
|
||||||
getIpVersion
|
|
||||||
} from '../../utils/ip-utils.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registry of built-in functions
|
|
||||||
*/
|
|
||||||
export const BUILT_IN_FUNCTIONS = {
|
|
||||||
// IP Address Functions - Using optimized fast versions
|
|
||||||
ip_in_cidr: {
|
|
||||||
params: ['ip', 'cidr'],
|
|
||||||
evaluate: (ip, cidr) => {
|
|
||||||
if (!ip || !cidr) return false;
|
|
||||||
return isIpInCidrFast(String(ip), String(cidr));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
ip_equals: {
|
|
||||||
params: ['ip1', 'ip2'],
|
|
||||||
evaluate: (ip1, ip2) => {
|
|
||||||
return ipEquals(String(ip1), String(ip2));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
ip_version: {
|
|
||||||
params: ['ip'],
|
|
||||||
evaluate: (ip) => {
|
|
||||||
return getIpVersion(String(ip));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
ip_is_private: {
|
|
||||||
params: ['ip'],
|
|
||||||
evaluate: (ip) => {
|
|
||||||
if (!ip) return false;
|
|
||||||
return isPrivateIpFast(String(ip));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
ip_is_loopback: {
|
|
||||||
params: ['ip'],
|
|
||||||
evaluate: (ip) => {
|
|
||||||
if (!ip) return false;
|
|
||||||
// Fast check: 127.x.x.x
|
|
||||||
return ipToIntFast(String(ip)) >>> 24 === 127;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
ip_is_v4: {
|
|
||||||
params: ['ip'],
|
|
||||||
evaluate: (ip) => {
|
|
||||||
if (!ip) return false;
|
|
||||||
return isIPv4Fast(String(ip));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
ip_is_v6: {
|
|
||||||
params: ['ip'],
|
|
||||||
evaluate: (ip) => {
|
|
||||||
if (!ip) return false;
|
|
||||||
return isIPv6(String(ip));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Time Functions
|
|
||||||
hour_of_day: {
|
|
||||||
params: ['timestamp'],
|
|
||||||
evaluate: (timestamp) => {
|
|
||||||
const ts = typeof timestamp === 'number' ? timestamp : Date.now();
|
|
||||||
return new Date(ts).getHours();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
day_of_week: {
|
|
||||||
params: ['timestamp'],
|
|
||||||
evaluate: (timestamp) => {
|
|
||||||
const ts = typeof timestamp === 'number' ? timestamp : Date.now();
|
|
||||||
return new Date(ts).getDay(); // 0 = Sunday
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// String Functions
|
|
||||||
contains: {
|
|
||||||
params: ['string', 'substring'],
|
|
||||||
evaluate: (str, substr) => {
|
|
||||||
if (!str || !substr) return false;
|
|
||||||
return String(str).includes(String(substr));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
starts_with: {
|
|
||||||
params: ['string', 'prefix'],
|
|
||||||
evaluate: (str, prefix) => {
|
|
||||||
if (!str || !prefix) return false;
|
|
||||||
return String(str).startsWith(String(prefix));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
ends_with: {
|
|
||||||
params: ['string', 'suffix'],
|
|
||||||
evaluate: (str, suffix) => {
|
|
||||||
if (!str || !suffix) return false;
|
|
||||||
return String(str).endsWith(String(suffix));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Comparison Functions
|
|
||||||
equals: {
|
|
||||||
params: ['a', 'b'],
|
|
||||||
evaluate: (a, b) => a === b
|
|
||||||
},
|
|
||||||
|
|
||||||
greater_than: {
|
|
||||||
params: ['a', 'b'],
|
|
||||||
evaluate: (a, b) => a > b
|
|
||||||
},
|
|
||||||
|
|
||||||
less_than: {
|
|
||||||
params: ['a', 'b'],
|
|
||||||
evaluate: (a, b) => a < b
|
|
||||||
},
|
|
||||||
|
|
||||||
in_range: {
|
|
||||||
params: ['value', 'min', 'max'],
|
|
||||||
evaluate: (value, min, max) => value >= min && value <= max
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a function name is a built-in
|
|
||||||
*/
|
|
||||||
export function isBuiltInFunction(name) {
|
|
||||||
return name in BUILT_IN_FUNCTIONS;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Evaluate a built-in function
|
|
||||||
*/
|
|
||||||
export function evaluateBuiltIn(name, args) {
|
|
||||||
const func = BUILT_IN_FUNCTIONS[name];
|
|
||||||
if (!func) {
|
|
||||||
throw new Error(`Unknown built-in function: ${name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.length !== func.params.length) {
|
|
||||||
throw new Error(
|
|
||||||
`Function ${name} expects ${func.params.length} arguments, got ${args.length}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return func.evaluate(...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get function signature
|
|
||||||
*/
|
|
||||||
export function getFunctionSignature(name) {
|
|
||||||
const func = BUILT_IN_FUNCTIONS[name];
|
|
||||||
if (!func) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
params: func.params
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,323 +0,0 @@
|
|||||||
/**
|
|
||||||
* Expression Interpreter
|
|
||||||
*
|
|
||||||
* Interprets inline DSL expressions by evaluating them against existing
|
|
||||||
* compiled DSL rules in the graph. No temporary rules are created.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { PredicateResolver } from './PredicateResolver.js';
|
|
||||||
|
|
||||||
export class ExpressionInterpreter {
|
|
||||||
constructor(context, options = {}) {
|
|
||||||
this.context = context;
|
|
||||||
this.graphStores = context.graphStores;
|
|
||||||
this.resolver = new PredicateResolver(context);
|
|
||||||
this.customBuiltIns = options.customBuiltIns || {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interpret an expression AST against the graph
|
|
||||||
*
|
|
||||||
* @param {Object} ast - Parsed expression AST
|
|
||||||
* @param {Object} bindings - Variable bindings
|
|
||||||
* @returns {Object} Interpretation result
|
|
||||||
*/
|
|
||||||
async interpret(ast, bindings) {
|
|
||||||
switch (ast.type) {
|
|
||||||
case 'Fusion':
|
|
||||||
return this.interpretFusion(ast, bindings);
|
|
||||||
case 'Or':
|
|
||||||
return this.interpretOr(ast, bindings);
|
|
||||||
case 'And':
|
|
||||||
return this.interpretAnd(ast, bindings);
|
|
||||||
case 'Not':
|
|
||||||
return this.interpretNot(ast, bindings);
|
|
||||||
case 'Defeasible':
|
|
||||||
return this.interpretDefeasible(ast, bindings);
|
|
||||||
case 'Predicate':
|
|
||||||
return this.interpretPredicate(ast, bindings);
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown AST node type: ${ast.type}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interpret FUSION block - ALL expressions must be true
|
|
||||||
* Uses OWA semantics: minimum possibility across all expressions
|
|
||||||
*/
|
|
||||||
async interpretFusion(ast, bindings) {
|
|
||||||
const results = await Promise.all(
|
|
||||||
ast.expressions.map(expr => this.interpret(expr, bindings))
|
|
||||||
);
|
|
||||||
|
|
||||||
const allAllowed = results.every(r => r.allowed);
|
|
||||||
const minPossibility = results.length > 0
|
|
||||||
? Math.min(...results.map(r => r.possibility || 0))
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
return {
|
|
||||||
allowed: allAllowed,
|
|
||||||
possibility: minPossibility,
|
|
||||||
type: 'Fusion',
|
|
||||||
details: results
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interpret OR - ANY expression can be true (short-circuited)
|
|
||||||
*/
|
|
||||||
async interpretOr(ast, bindings) {
|
|
||||||
for (const operand of ast.operands) {
|
|
||||||
const result = await this.interpret(operand, bindings);
|
|
||||||
if (result.allowed) {
|
|
||||||
return {
|
|
||||||
allowed: true,
|
|
||||||
possibility: result.possibility,
|
|
||||||
type: 'Or',
|
|
||||||
satisfiedBy: operand
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
allowed: false,
|
|
||||||
possibility: 0,
|
|
||||||
type: 'Or'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interpret AND - ALL expressions must be true
|
|
||||||
*/
|
|
||||||
async interpretAnd(ast, bindings) {
|
|
||||||
const results = [];
|
|
||||||
let minPossibility = 1;
|
|
||||||
|
|
||||||
for (const operand of ast.operands) {
|
|
||||||
const result = await this.interpret(operand, bindings);
|
|
||||||
results.push(result);
|
|
||||||
minPossibility = Math.min(minPossibility, result.possibility || 0);
|
|
||||||
|
|
||||||
if (!result.allowed) {
|
|
||||||
return {
|
|
||||||
allowed: false,
|
|
||||||
possibility: 0,
|
|
||||||
type: 'And',
|
|
||||||
failedAt: operand,
|
|
||||||
details: results
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
allowed: true,
|
|
||||||
possibility: minPossibility,
|
|
||||||
type: 'And',
|
|
||||||
details: results
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interpret NOT - negate the operand result
|
|
||||||
*/
|
|
||||||
async interpretNot(ast, bindings) {
|
|
||||||
const result = await this.interpret(ast.operand, bindings);
|
|
||||||
|
|
||||||
return {
|
|
||||||
allowed: !result.allowed,
|
|
||||||
possibility: result.allowed ? 0 : 1,
|
|
||||||
type: 'Not',
|
|
||||||
inner: result
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interpret Defeasible - primary UNLESS exception
|
|
||||||
* If exception is true, primary is defeated
|
|
||||||
*/
|
|
||||||
async interpretDefeasible(ast, bindings) {
|
|
||||||
// Check exception first (short-circuit if possible)
|
|
||||||
const exceptionResult = await this.interpret(ast.exception, bindings);
|
|
||||||
|
|
||||||
if (exceptionResult.allowed) {
|
|
||||||
return {
|
|
||||||
allowed: false,
|
|
||||||
possibility: 0,
|
|
||||||
type: 'Defeasible',
|
|
||||||
reason: 'Defeated by exception',
|
|
||||||
defeatedBy: exceptionResult
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exception is false, evaluate primary
|
|
||||||
const primaryResult = await this.interpret(ast.primary, bindings);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...primaryResult,
|
|
||||||
type: 'Defeasible',
|
|
||||||
primary: primaryResult,
|
|
||||||
exception: exceptionResult
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interpret Predicate - call existing DSL rule via graph.check()
|
|
||||||
* OR evaluate built-in function
|
|
||||||
*
|
|
||||||
* This is where we use the ALREADY COMPILED DSL rules.
|
|
||||||
* We do NOT create temporary rules.
|
|
||||||
*/
|
|
||||||
async interpretPredicate(ast, bindings) {
|
|
||||||
// Check for built-in functions first (ip_in_cidr, etc.)
|
|
||||||
const { isBuiltInFunction, evaluateBuiltIn } = await import('./BuiltInFunctions.js');
|
|
||||||
const resolvedArgs = ast.args.map(arg => this.resolveArgument(arg, bindings));
|
|
||||||
|
|
||||||
// Check custom built-ins first (e.g., PriceOps predicates)
|
|
||||||
if (this.customBuiltIns[ast.name]) {
|
|
||||||
const result = await this.customBuiltIns[ast.name](...resolvedArgs);
|
|
||||||
return {
|
|
||||||
allowed: result === true || result === 1,
|
|
||||||
possibility: result === true || result === 1 ? 1 : 0,
|
|
||||||
type: 'BuiltInFunction',
|
|
||||||
function: ast.name,
|
|
||||||
args: resolvedArgs,
|
|
||||||
result
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isBuiltInFunction(ast.name)) {
|
|
||||||
const result = evaluateBuiltIn(ast.name, resolvedArgs);
|
|
||||||
|
|
||||||
return {
|
|
||||||
allowed: result === true || result === 1,
|
|
||||||
possibility: result === true || result === 1 ? 1 : 0,
|
|
||||||
type: 'BuiltInFunction',
|
|
||||||
function: ast.name,
|
|
||||||
args: resolvedArgs,
|
|
||||||
result
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve predicate to existing rule
|
|
||||||
const rule = this.resolver.resolve(ast.name);
|
|
||||||
if (!rule) {
|
|
||||||
throw new Error(`Unknown predicate: ${ast.name} - must be defined in compiled DSL`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract subject (user) and optional object
|
|
||||||
const subject = resolvedArgs[0]; // First arg is always the subject
|
|
||||||
// For single-argument predicates, use subject as object to avoid "missing_node" errors
|
|
||||||
const object = resolvedArgs[1] || subject; // Second arg is optional object
|
|
||||||
|
|
||||||
// Get the appropriate graph store
|
|
||||||
const graphStore = this.context.getGraphStore
|
|
||||||
? this.context.getGraphStore(rule.scope, {
|
|
||||||
tenantId: bindings.tenant,
|
|
||||||
applicationId: bindings.applicationId
|
|
||||||
})
|
|
||||||
: this.graphStores[rule.scope];
|
|
||||||
if (!graphStore) {
|
|
||||||
throw new Error(`Graph store not found for scope: ${rule.scope}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute check using EXISTING compiled rule
|
|
||||||
// The graphStore.check() will use the pre-compiled DSL rule config
|
|
||||||
const checkOptions = {};
|
|
||||||
if (bindings.partialGraph) {
|
|
||||||
checkOptions.partialGraph = bindings.partialGraph;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = graphStore.check(subject, ast.name, object, checkOptions);
|
|
||||||
|
|
||||||
return {
|
|
||||||
allowed: result?.allowed || result?.possibility === 1,
|
|
||||||
possibility: result?.possibility || 0,
|
|
||||||
type: 'Predicate',
|
|
||||||
predicate: ast.name,
|
|
||||||
scope: rule.scope,
|
|
||||||
subject,
|
|
||||||
object,
|
|
||||||
rawResult: result
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve an argument to its actual value
|
|
||||||
*/
|
|
||||||
resolveArgument(arg, bindings) {
|
|
||||||
switch (arg.type) {
|
|
||||||
case 'Variable':
|
|
||||||
return this.resolveVariable(arg, bindings);
|
|
||||||
case 'Reference':
|
|
||||||
return this.resolveReference(arg, bindings);
|
|
||||||
case 'Literal':
|
|
||||||
return arg.value;
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown argument type: ${arg.type}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve a variable binding
|
|
||||||
* :user -> bindings.user
|
|
||||||
* :params.id -> bindings.params.id
|
|
||||||
*/
|
|
||||||
resolveVariable(variable, bindings) {
|
|
||||||
let value = bindings[variable.name];
|
|
||||||
|
|
||||||
// Handle nested paths: :params.id
|
|
||||||
if (variable.path && variable.path.length > 0) {
|
|
||||||
for (const key of variable.path) {
|
|
||||||
if (value === undefined || value === null) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
value = value[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve a typed reference
|
|
||||||
* document::params.id -> bindings.params.id with type info
|
|
||||||
*/
|
|
||||||
resolveReference(ref, bindings) {
|
|
||||||
// Typed references like document::params.id
|
|
||||||
// The type (document) is metadata, the value comes from the path
|
|
||||||
let value = bindings;
|
|
||||||
|
|
||||||
for (const key of ref.path) {
|
|
||||||
if (value === undefined || value === null) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
value = value[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility to collect all predicates from an AST
|
|
||||||
* Used for validation before interpretation
|
|
||||||
*/
|
|
||||||
export function collectPredicates(ast, predicates = []) {
|
|
||||||
if (ast.type === 'Predicate') {
|
|
||||||
predicates.push(ast);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recursively collect from child nodes
|
|
||||||
const childKeys = ['expressions', 'operands', 'operand', 'primary', 'exception', 'inner'];
|
|
||||||
for (const key of childKeys) {
|
|
||||||
if (ast[key]) {
|
|
||||||
if (Array.isArray(ast[key])) {
|
|
||||||
ast[key].forEach(child => collectPredicates(child, predicates));
|
|
||||||
} else {
|
|
||||||
collectPredicates(ast[key], predicates);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return predicates;
|
|
||||||
}
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
/**
|
|
||||||
* Predicate Resolver
|
|
||||||
*
|
|
||||||
* Maps predicate names to existing compiled DSL rules across all graph scopes.
|
|
||||||
* Does NOT create new rules - only looks up existing ones.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export class PredicateResolver {
|
|
||||||
constructor(context) {
|
|
||||||
this.context = context;
|
|
||||||
this.graphStores = context.graphStores || {};
|
|
||||||
this.cache = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve a predicate name to its DSL rule
|
|
||||||
*
|
|
||||||
* @param {string} predicateName - Name of the predicate
|
|
||||||
* @returns {Object|null} Rule info or null if not found
|
|
||||||
*/
|
|
||||||
resolve(predicateName) {
|
|
||||||
// Check cache first
|
|
||||||
if (this.cache.has(predicateName)) {
|
|
||||||
return this.cache.get(predicateName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look up in all graph scopes
|
|
||||||
const rule = this.findRule(predicateName);
|
|
||||||
|
|
||||||
if (rule) {
|
|
||||||
this.cache.set(predicateName, rule);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rule;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a rule across all graph scopes
|
|
||||||
* Prefers logical rules over direct rules for evidence predicates
|
|
||||||
*/
|
|
||||||
findRule(predicateName) {
|
|
||||||
const scopes = [
|
|
||||||
'tenantExternal',
|
|
||||||
'tenantInternal',
|
|
||||||
'rootExternal',
|
|
||||||
'rootInternal',
|
|
||||||
'masterExternal',
|
|
||||||
'masterInternal'
|
|
||||||
];
|
|
||||||
|
|
||||||
let directRule = null;
|
|
||||||
let directScope = null;
|
|
||||||
|
|
||||||
for (const scopeName of scopes) {
|
|
||||||
const graphStore = this.graphStores[scopeName];
|
|
||||||
if (!graphStore) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle both: Arbiter directly (has .check()) or wrapper with .arbiter
|
|
||||||
const arbiter = graphStore.arbiter || graphStore;
|
|
||||||
const config = arbiter.relationConfigs?.get(predicateName);
|
|
||||||
|
|
||||||
if (config) {
|
|
||||||
// Prefer logical rules (intersection/union) over direct rules
|
|
||||||
// This ensures evidence rules work correctly across all scopes
|
|
||||||
if (config.type === 'intersection' || config.type === 'union' || config.type === 'logical') {
|
|
||||||
return {
|
|
||||||
name: predicateName,
|
|
||||||
scope: scopeName,
|
|
||||||
config: config,
|
|
||||||
arity: this.inferArity(config)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remember the first direct rule as fallback
|
|
||||||
if (!directRule && config.type === 'direct') {
|
|
||||||
directRule = config;
|
|
||||||
directScope = scopeName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return direct rule if no logical rule found
|
|
||||||
if (directRule) {
|
|
||||||
return {
|
|
||||||
name: predicateName,
|
|
||||||
scope: directScope,
|
|
||||||
config: directRule,
|
|
||||||
arity: this.inferArity(directRule)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Infer the arity (parameter count) from rule config
|
|
||||||
*/
|
|
||||||
inferArity(config) {
|
|
||||||
// Most DSL evidence rules have 1 or 2 parameters:
|
|
||||||
// - 1 param: just the subject (user)
|
|
||||||
// - 2 params: subject (user) + object
|
|
||||||
|
|
||||||
if (config.arity) {
|
|
||||||
return config.arity;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default to checking if it's a relation that typically needs an object
|
|
||||||
// This is a heuristic - in practice, the DSL defines this explicitly
|
|
||||||
if (config.type === 'tuple_to_userset' || config.type === 'direct') {
|
|
||||||
return 2; // Likely needs subject + object
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1; // Default to 1 param
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a predicate exists without full resolution
|
|
||||||
*/
|
|
||||||
exists(predicateName) {
|
|
||||||
return this.resolve(predicateName) !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all available predicates across all scopes
|
|
||||||
*/
|
|
||||||
getAllPredicates() {
|
|
||||||
const predicates = [];
|
|
||||||
const scopes = [
|
|
||||||
'tenantExternal',
|
|
||||||
'tenantInternal',
|
|
||||||
'rootExternal',
|
|
||||||
'rootInternal',
|
|
||||||
'masterExternal',
|
|
||||||
'masterInternal'
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const scopeName of scopes) {
|
|
||||||
const graphStore = this.graphStores[scopeName];
|
|
||||||
if (!graphStore) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle both: Arbiter directly (has .check()) or wrapper with .arbiter
|
|
||||||
const arbiter = graphStore.arbiter || graphStore;
|
|
||||||
if (arbiter.relationConfigs) {
|
|
||||||
for (const [name, config] of arbiter.relationConfigs) {
|
|
||||||
predicates.push({
|
|
||||||
name,
|
|
||||||
scope: scopeName,
|
|
||||||
arity: this.inferArity(config)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return predicates;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear the cache (useful for testing or when rules change)
|
|
||||||
*/
|
|
||||||
clearCache() {
|
|
||||||
this.cache.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
import { BaseNode } from './BaseNode.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AST node for aggregation expressions
|
|
||||||
* Represents: aggregate { ... } USING majority
|
|
||||||
*/
|
|
||||||
export class AggregationNode extends BaseNode {
|
|
||||||
constructor(location = null) {
|
|
||||||
super('Aggregation', location);
|
|
||||||
this.expressions = []; // Array of expressions to aggregate
|
|
||||||
this.method = null; // Aggregation method ('majority', 'max', 'min', 'sum', 'avg')
|
|
||||||
this.weights = null; // Optional weights array
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add an expression to this aggregation
|
|
||||||
* @param {ExpressionNode} expression - Expression to add
|
|
||||||
*/
|
|
||||||
addExpression(expression) {
|
|
||||||
this.expressions.push(expression);
|
|
||||||
this.addChild(expression);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the aggregation method
|
|
||||||
* @param {string} method - Aggregation method
|
|
||||||
*/
|
|
||||||
setMethod(method) {
|
|
||||||
this.method = method;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set weights for this aggregation
|
|
||||||
* @param {number[]} weights - Weights array
|
|
||||||
*/
|
|
||||||
setWeights(weights) {
|
|
||||||
this.weights = weights;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all expressions
|
|
||||||
* @returns {ExpressionNode[]} Expressions to aggregate
|
|
||||||
*/
|
|
||||||
getExpressions() {
|
|
||||||
return this.expressions;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the aggregation method
|
|
||||||
* @returns {string|null} Aggregation method or null
|
|
||||||
*/
|
|
||||||
getMethod() {
|
|
||||||
return this.method;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the weights for this aggregation
|
|
||||||
* @returns {number[]|null} Weights or null
|
|
||||||
*/
|
|
||||||
getWeights() {
|
|
||||||
return this.weights;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this aggregation has weights
|
|
||||||
* @returns {boolean} True if has weights
|
|
||||||
*/
|
|
||||||
hasWeights() {
|
|
||||||
return this.weights !== null && this.weights.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is a majority aggregation
|
|
||||||
* @returns {boolean} True if majority
|
|
||||||
*/
|
|
||||||
isMajority() {
|
|
||||||
return this.method === 'majority';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is a max aggregation
|
|
||||||
* @returns {boolean} True if max
|
|
||||||
*/
|
|
||||||
isMax() {
|
|
||||||
return this.method === 'max';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is a min aggregation
|
|
||||||
* @returns {boolean} True if min
|
|
||||||
*/
|
|
||||||
isMin() {
|
|
||||||
return this.method === 'min';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is a sum aggregation
|
|
||||||
* @returns {boolean} True if sum
|
|
||||||
*/
|
|
||||||
isSum() {
|
|
||||||
return this.method === 'sum';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is an average aggregation
|
|
||||||
* @returns {boolean} True if average
|
|
||||||
*/
|
|
||||||
isAverage() {
|
|
||||||
return this.method === 'avg';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the number of expressions
|
|
||||||
* @returns {number} Number of expressions
|
|
||||||
*/
|
|
||||||
getExpressionCount() {
|
|
||||||
return this.expressions.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate the aggregation
|
|
||||||
* @returns {string[]} Array of error messages
|
|
||||||
*/
|
|
||||||
validate() {
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
// Validate method
|
|
||||||
const validMethods = ['majority', 'max', 'min', 'sum', 'avg', 'count'];
|
|
||||||
if (!this.method || !validMethods.includes(this.method)) {
|
|
||||||
errors.push(`Invalid aggregation method: ${this.method}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate expressions
|
|
||||||
if (this.expressions.length === 0) {
|
|
||||||
errors.push('Aggregation must have at least one expression');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate each expression
|
|
||||||
this.expressions.forEach((expr, index) => {
|
|
||||||
const exprErrors = expr.validate ? expr.validate() : [];
|
|
||||||
errors.push(...exprErrors.map(err => `Expression ${index + 1}: ${err}`));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Validate weights
|
|
||||||
if (this.weights !== null) {
|
|
||||||
if (!Array.isArray(this.weights)) {
|
|
||||||
errors.push('Weights must be an array');
|
|
||||||
} else if (this.weights.length !== this.expressions.length) {
|
|
||||||
errors.push('Weights array length must match expression count');
|
|
||||||
} else if (this.weights.some(w => typeof w !== 'number' || w < 0)) {
|
|
||||||
errors.push('All weights must be non-negative numbers');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
const weightsStr = this.hasWeights() ? ` weights[${this.weights.length}]` : '';
|
|
||||||
return `Aggregation(${this.method}, ${this.expressions.length} expressions${weightsStr})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
/**
|
|
||||||
* Base AST Node class for all DSL AST nodes
|
|
||||||
* Provides common functionality for all AST nodes
|
|
||||||
*/
|
|
||||||
export class BaseNode {
|
|
||||||
constructor(type, location = null) {
|
|
||||||
this.type = type;
|
|
||||||
this.location = location; // { start, end, line, column }
|
|
||||||
this.parent = null;
|
|
||||||
this.children = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a child node to this node
|
|
||||||
* @param {BaseNode} child - Child node to add
|
|
||||||
*/
|
|
||||||
addChild(child) {
|
|
||||||
if (child) {
|
|
||||||
child.parent = this;
|
|
||||||
this.children.push(child);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add multiple child nodes
|
|
||||||
* @param {BaseNode[]} children - Array of child nodes
|
|
||||||
*/
|
|
||||||
addChildren(children) {
|
|
||||||
children.forEach(child => this.addChild(child));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all children of a specific type
|
|
||||||
* @param {string} type - Node type to filter by
|
|
||||||
* @returns {BaseNode[]} Filtered children
|
|
||||||
*/
|
|
||||||
getChildrenOfType(type) {
|
|
||||||
return this.children.filter(child => child.type === type);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the first child of a specific type
|
|
||||||
* @param {string} type - Node type to find
|
|
||||||
* @returns {BaseNode|null} First matching child or null
|
|
||||||
*/
|
|
||||||
getChildOfType(type) {
|
|
||||||
return this.children.find(child => child.type === type) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all descendants of a specific type
|
|
||||||
* @param {string} type - Node type to find
|
|
||||||
* @returns {BaseNode[]} All matching descendants
|
|
||||||
*/
|
|
||||||
getDescendantsOfType(type) {
|
|
||||||
const results = [];
|
|
||||||
this.children.forEach(child => {
|
|
||||||
if (child.type === type) {
|
|
||||||
results.push(child);
|
|
||||||
}
|
|
||||||
results.push(...child.getDescendantsOfType(type));
|
|
||||||
});
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Accept a visitor (visitor pattern)
|
|
||||||
* @param {Object} visitor - Visitor object with visit methods
|
|
||||||
* @returns {*} Result of visitor.visit{NodeType}(this)
|
|
||||||
*/
|
|
||||||
accept(visitor) {
|
|
||||||
const methodName = `visit${this.type}`;
|
|
||||||
if (visitor[methodName]) {
|
|
||||||
return visitor[methodName](this);
|
|
||||||
}
|
|
||||||
if (visitor.visit) {
|
|
||||||
return visitor.visit(this);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a string representation of this node
|
|
||||||
* @returns {string} String representation
|
|
||||||
*/
|
|
||||||
toString() {
|
|
||||||
return `${this.type}(${this.children.length} children)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a detailed string representation for debugging
|
|
||||||
* @returns {string} Detailed string representation
|
|
||||||
*/
|
|
||||||
toDebugString() {
|
|
||||||
const childrenStr = this.children.map(child =>
|
|
||||||
child.toDebugString ? child.toDebugString() : child.toString()
|
|
||||||
).join(', ');
|
|
||||||
return `${this.type}(${childrenStr})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clone this node and all its children
|
|
||||||
* @returns {BaseNode} Cloned node
|
|
||||||
*/
|
|
||||||
clone() {
|
|
||||||
const cloned = new this.constructor();
|
|
||||||
cloned.type = this.type;
|
|
||||||
cloned.location = this.location ? { ...this.location } : null;
|
|
||||||
cloned.children = this.children.map(child => child.clone());
|
|
||||||
cloned.children.forEach(child => child.parent = cloned);
|
|
||||||
return cloned;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the root node of the AST
|
|
||||||
* @returns {BaseNode} Root node
|
|
||||||
*/
|
|
||||||
getRoot() {
|
|
||||||
let current = this;
|
|
||||||
while (current.parent) {
|
|
||||||
current = current.parent;
|
|
||||||
}
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the depth of this node in the AST
|
|
||||||
* @returns {number} Depth from root
|
|
||||||
*/
|
|
||||||
getDepth() {
|
|
||||||
let depth = 0;
|
|
||||||
let current = this.parent;
|
|
||||||
while (current) {
|
|
||||||
depth++;
|
|
||||||
current = current.parent;
|
|
||||||
}
|
|
||||||
return depth;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this node is a descendant of another node
|
|
||||||
* @param {BaseNode} ancestor - Potential ancestor node
|
|
||||||
* @returns {boolean} True if ancestor is an ancestor of this node
|
|
||||||
*/
|
|
||||||
isDescendantOf(ancestor) {
|
|
||||||
let current = this.parent;
|
|
||||||
while (current) {
|
|
||||||
if (current === ancestor) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
current = current.parent;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
import { BaseNode } from './BaseNode.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AST node for field behaviors (decay, blur, ttl)
|
|
||||||
* Represents: BEHAVES { decaying down hourly }
|
|
||||||
*/
|
|
||||||
export class BehaviorNode extends BaseNode {
|
|
||||||
constructor(type, location = null) {
|
|
||||||
super('Behavior', location);
|
|
||||||
this.type = type; // 'decay', 'blur', 'ttl'
|
|
||||||
this.parameters = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a parameter for this behavior
|
|
||||||
* @param {string} name - Parameter name
|
|
||||||
* @param {*} value - Parameter value
|
|
||||||
*/
|
|
||||||
setParameter(name, value) {
|
|
||||||
this.parameters.set(name, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a parameter value
|
|
||||||
* @param {string} name - Parameter name
|
|
||||||
* @returns {*} Parameter value or null
|
|
||||||
*/
|
|
||||||
getParameter(name) {
|
|
||||||
return this.parameters.get(name) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is a decay behavior
|
|
||||||
* @returns {boolean} True if decay behavior
|
|
||||||
*/
|
|
||||||
isDecay() {
|
|
||||||
return this.type === 'decay';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is a blur behavior
|
|
||||||
* @returns {boolean} True if blur behavior
|
|
||||||
*/
|
|
||||||
isBlur() {
|
|
||||||
return this.type === 'blur';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is a TTL behavior
|
|
||||||
* @returns {boolean} True if TTL behavior
|
|
||||||
*/
|
|
||||||
isTTL() {
|
|
||||||
return this.type === 'ttl';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For decay behaviors, get the direction
|
|
||||||
* @returns {string|null} Decay direction or null
|
|
||||||
*/
|
|
||||||
getDecayDirection() {
|
|
||||||
return this.getParameter('direction');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For decay behaviors, get the period
|
|
||||||
* @returns {string|null} Decay period or null
|
|
||||||
*/
|
|
||||||
getDecayPeriod() {
|
|
||||||
return this.getParameter('period');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For blur behaviors, get the mode
|
|
||||||
* @returns {string|null} Blur mode or null
|
|
||||||
*/
|
|
||||||
getBlurMode() {
|
|
||||||
return this.getParameter('mode');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For blur behaviors, get the confidence level
|
|
||||||
* @returns {string|null} Confidence level or null
|
|
||||||
*/
|
|
||||||
getBlurConfidence() {
|
|
||||||
return this.getParameter('confidence');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For TTL behaviors, get the duration
|
|
||||||
* @returns {string|null} TTL duration or null
|
|
||||||
*/
|
|
||||||
getTTLDuration() {
|
|
||||||
return this.getParameter('duration');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate the behavior
|
|
||||||
* @returns {string[]} Array of error messages
|
|
||||||
*/
|
|
||||||
validate() {
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
// Validate behavior type
|
|
||||||
if (!['decay', 'blur', 'ttl'].includes(this.type)) {
|
|
||||||
errors.push(`Invalid behavior type: ${this.type}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate decay behavior parameters
|
|
||||||
if (this.isDecay()) {
|
|
||||||
const direction = this.getDecayDirection();
|
|
||||||
if (!direction || !['up', 'down', 'neutral', 'stable'].includes(direction)) {
|
|
||||||
errors.push(`Invalid decay direction: ${direction}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const period = this.getDecayPeriod();
|
|
||||||
if (!period || !['hourly', 'daily', 'weekly', 'monthly'].includes(period)) {
|
|
||||||
errors.push(`Invalid decay period: ${period}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate blur behavior parameters
|
|
||||||
if (this.isBlur()) {
|
|
||||||
const mode = this.getBlurMode();
|
|
||||||
if (!mode || !['fixed', 'adaptive', 'confidence'].includes(mode)) {
|
|
||||||
errors.push(`Invalid blur mode: ${mode}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const confidence = this.getBlurConfidence();
|
|
||||||
if (confidence && !['confidence_90', 'confidence_95', 'confidence_99'].includes(confidence)) {
|
|
||||||
errors.push(`Invalid blur confidence: ${confidence}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate TTL behavior parameters
|
|
||||||
if (this.isTTL()) {
|
|
||||||
const duration = this.getTTLDuration();
|
|
||||||
if (!duration || !/^\d+[hd]$/.test(duration)) {
|
|
||||||
errors.push(`Invalid TTL duration: ${duration}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
const params = Array.from(this.parameters.entries())
|
|
||||||
.map(([key, value]) => `${key}: ${value}`)
|
|
||||||
.join(', ');
|
|
||||||
return `Behavior(${this.type}, ${params})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
import { BaseNode } from './BaseNode.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AST node for defeasible logic statements
|
|
||||||
* Represents: ALWAYS, WHEN, UNLESS, REQUIRES statements
|
|
||||||
*/
|
|
||||||
export class DefeasibleLogicNode extends BaseNode {
|
|
||||||
constructor(logicType, location = null) {
|
|
||||||
super('DefeasibleLogic', location);
|
|
||||||
this.logicType = logicType; // 'ALWAYS', 'WHEN', 'UNLESS', 'REQUIRES'
|
|
||||||
this.condition = null; // ExpressionNode or EvidenceBodyNode
|
|
||||||
this.defeater = null; // ExpressionNode or EvidenceBodyNode (for WHEN/UNLESS)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the condition for this defeasible logic
|
|
||||||
* @param {BaseNode} condition - Condition to set
|
|
||||||
*/
|
|
||||||
setCondition(condition) {
|
|
||||||
this.condition = condition;
|
|
||||||
this.addChild(condition);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the defeater for this defeasible logic (for WHEN/UNLESS)
|
|
||||||
* @param {BaseNode} defeater - Defeater to set
|
|
||||||
*/
|
|
||||||
setDefeater(defeater) {
|
|
||||||
this.defeater = defeater;
|
|
||||||
this.addChild(defeater);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is an ALWAYS statement
|
|
||||||
* @returns {boolean} True if ALWAYS
|
|
||||||
*/
|
|
||||||
isAlways() {
|
|
||||||
return this.logicType === 'ALWAYS';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is a WHEN statement
|
|
||||||
* @returns {boolean} True if WHEN
|
|
||||||
*/
|
|
||||||
isWhen() {
|
|
||||||
return this.logicType === 'WHEN';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is an UNLESS statement
|
|
||||||
* @returns {boolean} True if UNLESS
|
|
||||||
*/
|
|
||||||
isUnless() {
|
|
||||||
return this.logicType === 'UNLESS';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is a REQUIRES statement
|
|
||||||
* @returns {boolean} True if REQUIRES
|
|
||||||
*/
|
|
||||||
isRequires() {
|
|
||||||
return this.logicType === 'REQUIRES';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is a strict rule (ALWAYS)
|
|
||||||
* @returns {boolean} True if strict
|
|
||||||
*/
|
|
||||||
isStrict() {
|
|
||||||
return this.isAlways();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is a defeasible rule (WHEN)
|
|
||||||
* @returns {boolean} True if defeasible
|
|
||||||
*/
|
|
||||||
isDefeasible() {
|
|
||||||
return this.isWhen();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is a defeater (UNLESS)
|
|
||||||
* @returns {boolean} True if defeater
|
|
||||||
*/
|
|
||||||
isDefeater() {
|
|
||||||
return this.isUnless();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is a requirement (REQUIRES)
|
|
||||||
* @returns {boolean} True if requirement
|
|
||||||
*/
|
|
||||||
isRequirement() {
|
|
||||||
return this.isRequires();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the precedence level for this logic type
|
|
||||||
* @returns {number} Precedence level (higher = more important)
|
|
||||||
*/
|
|
||||||
getPrecedence() {
|
|
||||||
switch (this.logicType) {
|
|
||||||
case 'ALWAYS': return 3; // Highest precedence
|
|
||||||
case 'WHEN': return 2; // Medium precedence
|
|
||||||
case 'UNLESS': return 2; // Medium precedence
|
|
||||||
case 'REQUIRES': return 1; // Lowest precedence
|
|
||||||
default: return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate the defeasible logic
|
|
||||||
* @returns {string[]} Array of error messages
|
|
||||||
*/
|
|
||||||
validate() {
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
// Validate logic type
|
|
||||||
if (!['ALWAYS', 'WHEN', 'UNLESS', 'REQUIRES'].includes(this.logicType)) {
|
|
||||||
errors.push(`Invalid logic type: ${this.logicType}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate condition
|
|
||||||
if (!this.condition) {
|
|
||||||
errors.push(`${this.logicType} statement must have a condition`);
|
|
||||||
} else {
|
|
||||||
const condErrors = this.condition.validate ? this.condition.validate() : [];
|
|
||||||
errors.push(...condErrors);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate defeater for WHEN/UNLESS
|
|
||||||
if ((this.isWhen() || this.isUnless()) && !this.defeater) {
|
|
||||||
errors.push(`${this.logicType} statement must have a defeater`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate defeater if present
|
|
||||||
if (this.defeater) {
|
|
||||||
const defErrors = this.defeater.validate ? this.defeater.validate() : [];
|
|
||||||
errors.push(...defErrors);
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
const condStr = this.condition ? this.condition.toString() : 'null';
|
|
||||||
const defStr = this.defeater ? ` UNLESS ${this.defeater.toString()}` : '';
|
|
||||||
return `DefeasibleLogic(${this.logicType} ${condStr}${defStr})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
import { BaseNode } from './BaseNode.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AST node for type definitions
|
|
||||||
* Represents: definition User { ... }
|
|
||||||
*/
|
|
||||||
export class DefinitionNode extends BaseNode {
|
|
||||||
constructor(name, definitionType = 'type', location = null) {
|
|
||||||
super('Definition', location);
|
|
||||||
this.name = name;
|
|
||||||
this.definitionType = definitionType; // 'type', 'interface', etc.
|
|
||||||
this.fields = [];
|
|
||||||
this.behaviors = new Map(); // field name -> behavior
|
|
||||||
this.cacheDirectives = new Map(); // field name -> cache directive
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a field to the definition
|
|
||||||
* @param {FieldNode} field - Field to add
|
|
||||||
*/
|
|
||||||
addField(field) {
|
|
||||||
this.fields.push(field);
|
|
||||||
this.addChild(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set behavior for a field
|
|
||||||
* @param {string} fieldName - Name of the field
|
|
||||||
* @param {BehaviorNode} behavior - Behavior to set
|
|
||||||
*/
|
|
||||||
setBehavior(fieldName, behavior) {
|
|
||||||
this.behaviors.set(fieldName, behavior);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set cache directive for a field
|
|
||||||
* @param {string} fieldName - Name of the field
|
|
||||||
* @param {string} directive - Cache directive ('lazy')
|
|
||||||
*/
|
|
||||||
setCacheDirective(fieldName, directive) {
|
|
||||||
if (directive === 'eager') {
|
|
||||||
if (!DefinitionNode._warnedEagerCacheDirective) {
|
|
||||||
DefinitionNode._warnedEagerCacheDirective = true;
|
|
||||||
console.warn('[DefinitionNode] CACHE eager is deprecated; treating as CACHE lazy.');
|
|
||||||
}
|
|
||||||
this.cacheDirectives.set(fieldName, 'lazy');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.cacheDirectives.set(fieldName, directive);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get behavior for a field
|
|
||||||
* @param {string} fieldName - Name of the field
|
|
||||||
* @returns {BehaviorNode|null} Behavior or null
|
|
||||||
*/
|
|
||||||
getBehavior(fieldName) {
|
|
||||||
return this.behaviors.get(fieldName) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get cache directive for a field
|
|
||||||
* @param {string} fieldName - Name of the field
|
|
||||||
* @returns {string|null} Cache directive or null
|
|
||||||
*/
|
|
||||||
getCacheDirective(fieldName) {
|
|
||||||
return this.cacheDirectives.get(fieldName) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a field by name
|
|
||||||
* @param {string} fieldName - Name to search for
|
|
||||||
* @returns {FieldNode|null} Found field or null
|
|
||||||
*/
|
|
||||||
getField(fieldName) {
|
|
||||||
return this.fields.find(field => field.name === fieldName) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all fields with a specific type
|
|
||||||
* @param {string} type - Type to filter by
|
|
||||||
* @returns {FieldNode[]} Filtered fields
|
|
||||||
*/
|
|
||||||
getFieldsOfType(type) {
|
|
||||||
return this.fields.filter(field => field.type === type);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate the definition
|
|
||||||
* @returns {string[]} Array of error messages
|
|
||||||
*/
|
|
||||||
validate() {
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
// Check for duplicate field names
|
|
||||||
const fieldNames = new Set();
|
|
||||||
this.fields.forEach(field => {
|
|
||||||
if (fieldNames.has(field.name)) {
|
|
||||||
errors.push(`Duplicate field name '${field.name}' in definition '${this.name}'`);
|
|
||||||
} else {
|
|
||||||
fieldNames.add(field.name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Validate each field
|
|
||||||
this.fields.forEach(field => {
|
|
||||||
const fieldErrors = field.validate ? field.validate() : [];
|
|
||||||
errors.push(...fieldErrors);
|
|
||||||
});
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
return `Definition(${this.name}: ${this.fields.length} fields)`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import { BaseNode } from './BaseNode.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AST node for direct evidence statements
|
|
||||||
* Represents: owns(user, doc)
|
|
||||||
*/
|
|
||||||
export class DirectEvidenceNode extends BaseNode {
|
|
||||||
constructor(location = null) {
|
|
||||||
super('DirectEvidence', location);
|
|
||||||
this.predicate = null; // PredicateNode
|
|
||||||
this.negated = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the predicate for this direct evidence
|
|
||||||
* @param {PredicateNode} predicate - Predicate to set
|
|
||||||
*/
|
|
||||||
setPredicate(predicate) {
|
|
||||||
this.predicate = predicate;
|
|
||||||
this.addChild(predicate);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set whether this evidence is negated
|
|
||||||
* @param {boolean} negated - Whether evidence is negated
|
|
||||||
*/
|
|
||||||
setNegated(negated) {
|
|
||||||
this.negated = negated;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this evidence is negated
|
|
||||||
* @returns {boolean} True if negated
|
|
||||||
*/
|
|
||||||
isNegated() {
|
|
||||||
return this.negated;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the predicate name
|
|
||||||
* @returns {string|null} Predicate name or null
|
|
||||||
*/
|
|
||||||
getPredicateName() {
|
|
||||||
return this.predicate ? this.predicate.name : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the predicate arguments
|
|
||||||
* @returns {ExpressionNode[]} Predicate arguments
|
|
||||||
*/
|
|
||||||
getArguments() {
|
|
||||||
return this.predicate ? this.predicate.arguments : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate the direct evidence
|
|
||||||
* @returns {string[]} Array of error messages
|
|
||||||
*/
|
|
||||||
validate() {
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
// Validate predicate
|
|
||||||
if (!this.predicate) {
|
|
||||||
errors.push('Direct evidence must have a predicate');
|
|
||||||
} else {
|
|
||||||
const predErrors = this.predicate.validate ? this.predicate.validate() : [];
|
|
||||||
errors.push(...predErrors);
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
const negStr = this.negated ? 'NOT ' : '';
|
|
||||||
const predStr = this.predicate ? this.predicate.toString() : 'null';
|
|
||||||
return `DirectEvidence(${negStr}${predStr})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import { BaseNode } from './BaseNode.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AST node for evidence body containing statements
|
|
||||||
* Represents: { statement1; statement2; ... }
|
|
||||||
*/
|
|
||||||
export class EvidenceBodyNode extends BaseNode {
|
|
||||||
constructor(location = null) {
|
|
||||||
super('EvidenceBody', location);
|
|
||||||
this.statements = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a statement to the evidence body
|
|
||||||
* @param {BaseNode} statement - Statement to add
|
|
||||||
*/
|
|
||||||
addStatement(statement) {
|
|
||||||
this.statements.push(statement);
|
|
||||||
this.addChild(statement);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all statements of a specific type
|
|
||||||
* @param {string} type - Statement type to filter by
|
|
||||||
* @returns {BaseNode[]} Filtered statements
|
|
||||||
*/
|
|
||||||
getStatementsOfType(type) {
|
|
||||||
return this.statements.filter(stmt => stmt.type === type);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all direct evidence statements
|
|
||||||
* @returns {DirectEvidenceNode[]} Direct evidence statements
|
|
||||||
*/
|
|
||||||
getDirectEvidence() {
|
|
||||||
return this.getStatementsOfType('DirectEvidence');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all pattern matching statements
|
|
||||||
* @returns {PatternMatchNode[]} Pattern matching statements
|
|
||||||
*/
|
|
||||||
getPatternMatches() {
|
|
||||||
return this.getStatementsOfType('PatternMatch');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all defeasible logic statements
|
|
||||||
* @returns {DefeasibleLogicNode[]} Defeasible logic statements
|
|
||||||
*/
|
|
||||||
getDefeasibleLogic() {
|
|
||||||
return this.getStatementsOfType('DefeasibleLogic');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all fusion statements
|
|
||||||
* @returns {FusionNode[]} Fusion statements
|
|
||||||
*/
|
|
||||||
getFusions() {
|
|
||||||
return this.getStatementsOfType('Fusion');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate the evidence body
|
|
||||||
* @returns {string[]} Array of error messages
|
|
||||||
*/
|
|
||||||
validate() {
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
// Validate each statement
|
|
||||||
this.statements.forEach((stmt, index) => {
|
|
||||||
const stmtErrors = stmt.validate ? stmt.validate() : [];
|
|
||||||
errors.push(...stmtErrors.map(err => `Statement ${index + 1}: ${err}`));
|
|
||||||
});
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
return `EvidenceBody(${this.statements.length} statements)`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
import { BaseNode } from './BaseNode.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AST node for evidence definitions
|
|
||||||
* Represents: evidence canRead(user: User, doc: Document) { ... } PROVIDES string
|
|
||||||
*/
|
|
||||||
export class EvidenceNode extends BaseNode {
|
|
||||||
constructor(name, location = null) {
|
|
||||||
super('Evidence', location);
|
|
||||||
this.name = name;
|
|
||||||
this.parameters = [];
|
|
||||||
this.returnType = null;
|
|
||||||
this.body = null; // EvidenceBodyNode
|
|
||||||
this.provides = null; // Return type specification
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a parameter to the evidence
|
|
||||||
* @param {ParameterNode} parameter - Parameter to add
|
|
||||||
*/
|
|
||||||
addParameter(parameter) {
|
|
||||||
this.parameters.push(parameter);
|
|
||||||
this.addChild(parameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the body of the evidence
|
|
||||||
* @param {EvidenceBodyNode} body - Evidence body
|
|
||||||
*/
|
|
||||||
setBody(body) {
|
|
||||||
this.body = body;
|
|
||||||
this.addChild(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the return type for this evidence
|
|
||||||
* @param {string} returnType - Return type
|
|
||||||
*/
|
|
||||||
setReturnType(returnType) {
|
|
||||||
this.returnType = returnType;
|
|
||||||
this.provides = returnType;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the parameter names as an array
|
|
||||||
* @returns {string[]} Array of parameter names
|
|
||||||
*/
|
|
||||||
getParameterNames() {
|
|
||||||
return this.parameters.map(param => param.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the parameter types as an array
|
|
||||||
* @returns {string[]} Array of parameter types
|
|
||||||
*/
|
|
||||||
getParameterTypes() {
|
|
||||||
return this.parameters.map(param => param.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a parameter by name
|
|
||||||
* @param {string} name - Parameter name to find
|
|
||||||
* @returns {ParameterNode|null} Found parameter or null
|
|
||||||
*/
|
|
||||||
getParameter(name) {
|
|
||||||
return this.parameters.find(param => param.name === name) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the signature string for this evidence
|
|
||||||
* @returns {string} Evidence signature
|
|
||||||
*/
|
|
||||||
getSignature() {
|
|
||||||
const paramStr = this.parameters.map(param => `${param.name}: ${param.type}`).join(', ');
|
|
||||||
return `${this.name}(${paramStr})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this evidence has a return type
|
|
||||||
* @returns {boolean} True if has return type
|
|
||||||
*/
|
|
||||||
hasReturnType() {
|
|
||||||
return this.returnType !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate the evidence
|
|
||||||
* @returns {string[]} Array of error messages
|
|
||||||
*/
|
|
||||||
validate() {
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
// Validate evidence name
|
|
||||||
if (!this.name || typeof this.name !== 'string') {
|
|
||||||
errors.push(`Invalid evidence name: ${this.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate parameters
|
|
||||||
this.parameters.forEach((param, index) => {
|
|
||||||
const paramErrors = param.validate ? param.validate() : [];
|
|
||||||
errors.push(...paramErrors.map(err => `Parameter ${index + 1}: ${err}`));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Validate body
|
|
||||||
if (this.body) {
|
|
||||||
const bodyErrors = this.body.validate ? this.body.validate() : [];
|
|
||||||
errors.push(...bodyErrors);
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
const providesStr = this.returnType ? ` PROVIDES ${this.returnType}` : '';
|
|
||||||
return `Evidence(${this.getSignature()}${providesStr})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,260 +0,0 @@
|
|||||||
import { BaseNode } from './BaseNode.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AST node for expressions (variables, literals, attribute access, etc.)
|
|
||||||
*/
|
|
||||||
export class ExpressionNode extends BaseNode {
|
|
||||||
constructor(expressionType, location = null) {
|
|
||||||
super('Expression', location);
|
|
||||||
this.expressionType = expressionType; // 'variable', 'literal', 'attribute', 'function', etc.
|
|
||||||
this.value = null;
|
|
||||||
this.name = null;
|
|
||||||
this.attribute = null;
|
|
||||||
this.object = null;
|
|
||||||
this.arguments = [];
|
|
||||||
this.operator = null;
|
|
||||||
this.left = null;
|
|
||||||
this.right = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the value for this expression
|
|
||||||
* @param {*} value - Value to set
|
|
||||||
*/
|
|
||||||
setValue(value) {
|
|
||||||
this.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the name for this expression
|
|
||||||
* @param {string} name - Name to set
|
|
||||||
*/
|
|
||||||
setName(name) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the attribute for this expression
|
|
||||||
* @param {string} attribute - Attribute to set
|
|
||||||
*/
|
|
||||||
setAttribute(attribute) {
|
|
||||||
this.attribute = attribute;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the object for this expression
|
|
||||||
* @param {ExpressionNode} object - Object to set
|
|
||||||
*/
|
|
||||||
setObject(object) {
|
|
||||||
this.object = object;
|
|
||||||
this.addChild(object);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add an argument to this expression
|
|
||||||
* @param {ExpressionNode} argument - Argument to add
|
|
||||||
*/
|
|
||||||
addArgument(argument) {
|
|
||||||
this.arguments.push(argument);
|
|
||||||
this.addChild(argument);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the operator for this expression
|
|
||||||
* @param {string} operator - Operator to set
|
|
||||||
*/
|
|
||||||
setOperator(operator) {
|
|
||||||
this.operator = operator;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the left operand for this expression
|
|
||||||
* @param {ExpressionNode} left - Left operand to set
|
|
||||||
*/
|
|
||||||
setLeft(left) {
|
|
||||||
this.left = left;
|
|
||||||
this.addChild(left);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the right operand for this expression
|
|
||||||
* @param {ExpressionNode} right - Right operand to set
|
|
||||||
*/
|
|
||||||
setRight(right) {
|
|
||||||
this.right = right;
|
|
||||||
this.addChild(right);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is a variable expression
|
|
||||||
* @returns {boolean} True if variable
|
|
||||||
*/
|
|
||||||
isVariable() {
|
|
||||||
return this.expressionType === 'variable';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is a literal expression
|
|
||||||
* @returns {boolean} True if literal
|
|
||||||
*/
|
|
||||||
isLiteral() {
|
|
||||||
return this.expressionType === 'literal';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is an attribute access expression
|
|
||||||
* @returns {boolean} True if attribute access
|
|
||||||
*/
|
|
||||||
isAttributeAccess() {
|
|
||||||
return this.expressionType === 'attribute';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is a function call expression
|
|
||||||
* @returns {boolean} True if function call
|
|
||||||
*/
|
|
||||||
isFunctionCall() {
|
|
||||||
return this.expressionType === 'function';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is a binary operation expression
|
|
||||||
* @returns {boolean} True if binary operation
|
|
||||||
*/
|
|
||||||
isBinaryOperation() {
|
|
||||||
return this.expressionType === 'binary';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is a wildcard variable
|
|
||||||
* @returns {boolean} True if wildcard
|
|
||||||
*/
|
|
||||||
isWildcard() {
|
|
||||||
return this.isVariable() && this.name && this.name.startsWith('*');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the variable name (without wildcard prefix)
|
|
||||||
* @returns {string|null} Variable name or null
|
|
||||||
*/
|
|
||||||
getVariableName() {
|
|
||||||
if (this.isVariable() && this.name) {
|
|
||||||
return this.name.startsWith('*') ? this.name.substring(1) : this.name;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the full attribute path
|
|
||||||
* @returns {string|null} Full attribute path or null
|
|
||||||
*/
|
|
||||||
getAttributePath() {
|
|
||||||
if (this.isAttributeAccess()) {
|
|
||||||
const objStr = this.object ? this.object.toString() : '';
|
|
||||||
return `${objStr}.${this.attribute}`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the function signature
|
|
||||||
* @returns {string|null} Function signature or null
|
|
||||||
*/
|
|
||||||
getFunctionSignature() {
|
|
||||||
if (this.isFunctionCall()) {
|
|
||||||
const argStr = this.arguments.map(arg => arg.toString()).join(', ');
|
|
||||||
return `${this.name}(${argStr})`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate the expression
|
|
||||||
* @returns {string[]} Array of error messages
|
|
||||||
*/
|
|
||||||
validate() {
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
// Validate expression type
|
|
||||||
const validTypes = ['variable', 'literal', 'attribute', 'function', 'binary'];
|
|
||||||
if (!validTypes.includes(this.expressionType)) {
|
|
||||||
errors.push(`Invalid expression type: ${this.expressionType}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate variable expressions
|
|
||||||
if (this.isVariable() && !this.name) {
|
|
||||||
errors.push('Variable expression must have a name');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate literal expressions
|
|
||||||
if (this.isLiteral() && this.value === null) {
|
|
||||||
errors.push('Literal expression must have a value');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate attribute access expressions
|
|
||||||
if (this.isAttributeAccess()) {
|
|
||||||
if (!this.attribute) {
|
|
||||||
errors.push('Attribute access expression must have an attribute');
|
|
||||||
}
|
|
||||||
if (this.object) {
|
|
||||||
const objErrors = this.object.validate ? this.object.validate() : [];
|
|
||||||
errors.push(...objErrors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate function call expressions
|
|
||||||
if (this.isFunctionCall()) {
|
|
||||||
if (!this.name) {
|
|
||||||
errors.push('Function call expression must have a name');
|
|
||||||
}
|
|
||||||
this.arguments.forEach((arg, index) => {
|
|
||||||
const argErrors = arg.validate ? arg.validate() : [];
|
|
||||||
errors.push(...argErrors.map(err => `Argument ${index + 1}: ${err}`));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate binary operation expressions
|
|
||||||
if (this.isBinaryOperation()) {
|
|
||||||
if (!this.operator) {
|
|
||||||
errors.push('Binary operation expression must have an operator');
|
|
||||||
}
|
|
||||||
if (!this.left) {
|
|
||||||
errors.push('Binary operation expression must have a left operand');
|
|
||||||
}
|
|
||||||
if (!this.right) {
|
|
||||||
errors.push('Binary operation expression must have a right operand');
|
|
||||||
}
|
|
||||||
if (this.left) {
|
|
||||||
const leftErrors = this.left.validate ? this.left.validate() : [];
|
|
||||||
errors.push(...leftErrors);
|
|
||||||
}
|
|
||||||
if (this.right) {
|
|
||||||
const rightErrors = this.right.validate ? this.right.validate() : [];
|
|
||||||
errors.push(...rightErrors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
switch (this.expressionType) {
|
|
||||||
case 'variable':
|
|
||||||
return `Variable(${this.name})`;
|
|
||||||
case 'literal':
|
|
||||||
return `Literal(${this.value})`;
|
|
||||||
case 'attribute':
|
|
||||||
const objStr = this.object ? this.object.toString() : '';
|
|
||||||
return `Attribute(${objStr}.${this.attribute})`;
|
|
||||||
case 'function':
|
|
||||||
const argStr = this.arguments.map(arg => arg.toString()).join(', ');
|
|
||||||
return `Function(${this.name}(${argStr}))`;
|
|
||||||
case 'binary':
|
|
||||||
const leftStr = this.left ? this.left.toString() : 'null';
|
|
||||||
const rightStr = this.right ? this.right.toString() : 'null';
|
|
||||||
return `Binary(${leftStr} ${this.operator} ${rightStr})`;
|
|
||||||
default:
|
|
||||||
return `Expression(${this.expressionType})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
import { BaseNode } from './BaseNode.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AST node for fact definitions
|
|
||||||
* Represents: fact hasRole(user: User, role: string) CACHE lazy
|
|
||||||
*/
|
|
||||||
export class FactNode extends BaseNode {
|
|
||||||
constructor(name, location = null) {
|
|
||||||
super('Fact', location);
|
|
||||||
this.name = name;
|
|
||||||
this.parameters = [];
|
|
||||||
this.returnType = null;
|
|
||||||
this.properties = new Map(); // transitive, symmetrical, etc.
|
|
||||||
this.cacheDirective = null;
|
|
||||||
this.limit = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a parameter to the fact
|
|
||||||
* @param {ParameterNode} parameter - Parameter to add
|
|
||||||
*/
|
|
||||||
addParameter(parameter) {
|
|
||||||
this.parameters.push(parameter);
|
|
||||||
this.addChild(parameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the return type for this fact
|
|
||||||
* @param {string} returnType - Return type
|
|
||||||
*/
|
|
||||||
setReturnType(returnType) {
|
|
||||||
this.returnType = returnType;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a property for this fact
|
|
||||||
* @param {string} name - Property name
|
|
||||||
* @param {*} value - Property value
|
|
||||||
*/
|
|
||||||
setProperty(name, value) {
|
|
||||||
this.properties.set(name, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a property value
|
|
||||||
* @param {string} name - Property name
|
|
||||||
* @returns {*} Property value or null
|
|
||||||
*/
|
|
||||||
getProperty(name) {
|
|
||||||
return this.properties.get(name) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the cache directive for this fact
|
|
||||||
* @param {string} directive - Cache directive ('lazy')
|
|
||||||
*/
|
|
||||||
setCacheDirective(directive) {
|
|
||||||
if (directive === 'eager') {
|
|
||||||
if (!FactNode._warnedEagerCacheDirective) {
|
|
||||||
FactNode._warnedEagerCacheDirective = true;
|
|
||||||
console.warn('[FactNode] CACHE eager is deprecated; treating as CACHE lazy.');
|
|
||||||
}
|
|
||||||
this.cacheDirective = 'lazy';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.cacheDirective = directive;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the limit for this fact
|
|
||||||
* @param {number} limit - Limit value
|
|
||||||
*/
|
|
||||||
setLimit(limit) {
|
|
||||||
this.limit = limit;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this fact is transitive
|
|
||||||
* @returns {boolean} True if transitive
|
|
||||||
*/
|
|
||||||
isTransitive() {
|
|
||||||
return this.getProperty('transitive') === true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this fact is symmetrical
|
|
||||||
* @returns {boolean} True if symmetrical
|
|
||||||
*/
|
|
||||||
isSymmetrical() {
|
|
||||||
return this.getProperty('symmetrical') === true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the parameter names as an array
|
|
||||||
* @returns {string[]} Array of parameter names
|
|
||||||
*/
|
|
||||||
getParameterNames() {
|
|
||||||
return this.parameters.map(param => param.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the parameter types as an array
|
|
||||||
* @returns {string[]} Array of parameter types
|
|
||||||
*/
|
|
||||||
getParameterTypes() {
|
|
||||||
return this.parameters.map(param => param.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a parameter by name
|
|
||||||
* @param {string} name - Parameter name to find
|
|
||||||
* @returns {ParameterNode|null} Found parameter or null
|
|
||||||
*/
|
|
||||||
getParameter(name) {
|
|
||||||
return this.parameters.find(param => param.name === name) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the signature string for this fact
|
|
||||||
* @returns {string} Fact signature
|
|
||||||
*/
|
|
||||||
getSignature() {
|
|
||||||
const paramStr = this.parameters.map(param => `${param.name}: ${param.type}`).join(', ');
|
|
||||||
return `${this.name}(${paramStr})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate the fact
|
|
||||||
* @returns {string[]} Array of error messages
|
|
||||||
*/
|
|
||||||
validate() {
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
// Validate fact name
|
|
||||||
if (!this.name || typeof this.name !== 'string') {
|
|
||||||
errors.push(`Invalid fact name: ${this.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate parameters
|
|
||||||
this.parameters.forEach((param, index) => {
|
|
||||||
const paramErrors = param.validate ? param.validate() : [];
|
|
||||||
errors.push(...paramErrors.map(err => `Parameter ${index + 1}: ${err}`));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Validate cache directive
|
|
||||||
if (this.cacheDirective && !['lazy'].includes(this.cacheDirective)) {
|
|
||||||
errors.push(`Invalid cache directive: ${this.cacheDirective}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate limit
|
|
||||||
if (this.limit !== null && (typeof this.limit !== 'number' || this.limit < 0)) {
|
|
||||||
errors.push(`Invalid limit: ${this.limit}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
const props = Array.from(this.properties.entries())
|
|
||||||
.map(([key, value]) => `${key}: ${value}`)
|
|
||||||
.join(', ');
|
|
||||||
const cacheStr = this.cacheDirective ? ` CACHE ${this.cacheDirective}` : '';
|
|
||||||
const limitStr = this.limit ? ` LIMIT ${this.limit}` : '';
|
|
||||||
return `Fact(${this.getSignature()}${props ? `, ${props}` : ''}${cacheStr}${limitStr})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
import { BaseNode } from './BaseNode.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AST node for field definitions within type definitions
|
|
||||||
* Represents: fieldName: type BEHAVES { ... } CACHE lazy
|
|
||||||
*/
|
|
||||||
export class FieldNode extends BaseNode {
|
|
||||||
constructor(name, type, location = null) {
|
|
||||||
super('Field', location);
|
|
||||||
this.name = name;
|
|
||||||
this.type = type;
|
|
||||||
this.isArray = false;
|
|
||||||
this.behavior = null;
|
|
||||||
this.cacheDirective = null;
|
|
||||||
this.isOptional = false;
|
|
||||||
this.defaultValue = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the behavior for this field
|
|
||||||
* @param {BehaviorNode} behavior - Behavior to set
|
|
||||||
*/
|
|
||||||
setBehavior(behavior) {
|
|
||||||
this.behavior = behavior;
|
|
||||||
this.addChild(behavior);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the cache directive for this field
|
|
||||||
* @param {string} directive - Cache directive ('lazy')
|
|
||||||
*/
|
|
||||||
setCacheDirective(directive) {
|
|
||||||
if (directive === 'eager') {
|
|
||||||
if (!FieldNode._warnedEagerCacheDirective) {
|
|
||||||
FieldNode._warnedEagerCacheDirective = true;
|
|
||||||
console.warn('[FieldNode] CACHE eager is deprecated; treating as CACHE lazy.');
|
|
||||||
}
|
|
||||||
this.cacheDirective = 'lazy';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.cacheDirective = directive;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark this field as an array type
|
|
||||||
* @param {boolean} isArray - Whether this is an array
|
|
||||||
*/
|
|
||||||
setArray(isArray) {
|
|
||||||
this.isArray = isArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set whether this field is optional
|
|
||||||
* @param {boolean} optional - Whether field is optional
|
|
||||||
*/
|
|
||||||
setOptional(optional) {
|
|
||||||
this.isOptional = optional;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set default value for this field
|
|
||||||
* @param {*} value - Default value
|
|
||||||
*/
|
|
||||||
setDefaultValue(value) {
|
|
||||||
this.defaultValue = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the full type string including array notation
|
|
||||||
* @returns {string} Full type string
|
|
||||||
*/
|
|
||||||
getFullType() {
|
|
||||||
let typeStr = this.type;
|
|
||||||
if (this.isArray) {
|
|
||||||
typeStr += '[]';
|
|
||||||
}
|
|
||||||
if (this.isOptional) {
|
|
||||||
typeStr += '?';
|
|
||||||
}
|
|
||||||
return typeStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this field has decay behavior
|
|
||||||
* @returns {boolean} True if field has decay behavior
|
|
||||||
*/
|
|
||||||
hasDecayBehavior() {
|
|
||||||
return this.behavior && this.behavior.type === 'decay';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this field has blur behavior
|
|
||||||
* @returns {boolean} True if field has blur behavior
|
|
||||||
*/
|
|
||||||
hasBlurBehavior() {
|
|
||||||
return this.behavior && this.behavior.type === 'blur';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this field has TTL behavior
|
|
||||||
* @returns {boolean} True if field has TTL behavior
|
|
||||||
*/
|
|
||||||
hasTTLBehavior() {
|
|
||||||
return this.behavior && this.behavior.type === 'ttl';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate the field
|
|
||||||
* @returns {string[]} Array of error messages
|
|
||||||
*/
|
|
||||||
validate() {
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
// Validate field name
|
|
||||||
if (!this.name || typeof this.name !== 'string') {
|
|
||||||
errors.push(`Invalid field name: ${this.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate type
|
|
||||||
if (!this.type || typeof this.type !== 'string') {
|
|
||||||
errors.push(`Invalid field type: ${this.type}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate behavior if present
|
|
||||||
if (this.behavior) {
|
|
||||||
const behaviorErrors = this.behavior.validate ? this.behavior.validate() : [];
|
|
||||||
errors.push(...behaviorErrors);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate cache directive
|
|
||||||
if (this.cacheDirective && !['lazy'].includes(this.cacheDirective)) {
|
|
||||||
errors.push(`Invalid cache directive: ${this.cacheDirective}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
return `Field(${this.name}: ${this.getFullType()})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
import { BaseNode } from './BaseNode.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AST node for fusion statements
|
|
||||||
* Represents: fusion max { ... }
|
|
||||||
*/
|
|
||||||
export class FusionNode extends BaseNode {
|
|
||||||
constructor(strategy, location = null) {
|
|
||||||
super('Fusion', location);
|
|
||||||
this.strategy = strategy; // 'max', 'min', 'majority', 'average', etc.
|
|
||||||
this.evidence = []; // Array of evidence statements
|
|
||||||
this.weights = null; // Optional weights array
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add evidence to this fusion
|
|
||||||
* @param {BaseNode} evidence - Evidence to add
|
|
||||||
*/
|
|
||||||
addEvidence(evidence) {
|
|
||||||
this.evidence.push(evidence);
|
|
||||||
this.addChild(evidence);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set weights for this fusion
|
|
||||||
* @param {number[]} weights - Weights array
|
|
||||||
*/
|
|
||||||
setWeights(weights) {
|
|
||||||
this.weights = weights;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the fusion strategy
|
|
||||||
* @returns {string} Fusion strategy
|
|
||||||
*/
|
|
||||||
getStrategy() {
|
|
||||||
return this.strategy;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all evidence statements
|
|
||||||
* @returns {BaseNode[]} Evidence statements
|
|
||||||
*/
|
|
||||||
getEvidence() {
|
|
||||||
return this.evidence;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the weights for this fusion
|
|
||||||
* @returns {number[]|null} Weights or null
|
|
||||||
*/
|
|
||||||
getWeights() {
|
|
||||||
return this.weights;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this fusion has weights
|
|
||||||
* @returns {boolean} True if has weights
|
|
||||||
*/
|
|
||||||
hasWeights() {
|
|
||||||
return this.weights !== null && this.weights.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is a max fusion
|
|
||||||
* @returns {boolean} True if max fusion
|
|
||||||
*/
|
|
||||||
isMax() {
|
|
||||||
return this.strategy === 'max';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is a min fusion
|
|
||||||
* @returns {boolean} True if min fusion
|
|
||||||
*/
|
|
||||||
isMin() {
|
|
||||||
return this.strategy === 'min';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is a majority fusion
|
|
||||||
* @returns {boolean} True if majority fusion
|
|
||||||
*/
|
|
||||||
isMajority() {
|
|
||||||
return this.strategy === 'majority';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is an average fusion
|
|
||||||
* @returns {boolean} True if average fusion
|
|
||||||
*/
|
|
||||||
isAverage() {
|
|
||||||
return this.strategy === 'average';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the number of evidence statements
|
|
||||||
* @returns {number} Number of evidence statements
|
|
||||||
*/
|
|
||||||
getEvidenceCount() {
|
|
||||||
return this.evidence.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate the fusion
|
|
||||||
* @returns {string[]} Array of error messages
|
|
||||||
*/
|
|
||||||
validate() {
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
// Validate strategy
|
|
||||||
const validStrategies = [
|
|
||||||
'max',
|
|
||||||
'min',
|
|
||||||
'majority',
|
|
||||||
'average',
|
|
||||||
'sum',
|
|
||||||
'sum_unbounded',
|
|
||||||
'median',
|
|
||||||
'optimistic',
|
|
||||||
'pessimistic',
|
|
||||||
'top2',
|
|
||||||
'top3',
|
|
||||||
'priority',
|
|
||||||
'custom',
|
|
||||||
'count'
|
|
||||||
];
|
|
||||||
if (!validStrategies.includes(this.strategy)) {
|
|
||||||
errors.push(`Invalid fusion strategy: ${this.strategy}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate evidence
|
|
||||||
if (this.evidence.length === 0) {
|
|
||||||
errors.push('Fusion must have at least one evidence statement');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate each evidence statement
|
|
||||||
this.evidence.forEach((ev, index) => {
|
|
||||||
const evErrors = ev.validate ? ev.validate() : [];
|
|
||||||
errors.push(...evErrors.map(err => `Evidence ${index + 1}: ${err}`));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Validate weights
|
|
||||||
if (this.weights !== null) {
|
|
||||||
if (!Array.isArray(this.weights)) {
|
|
||||||
errors.push('Weights must be an array');
|
|
||||||
} else if (this.weights.length !== this.evidence.length) {
|
|
||||||
errors.push('Weights array length must match evidence count');
|
|
||||||
} else if (this.weights.some(w => typeof w !== 'number' || w < 0)) {
|
|
||||||
errors.push('All weights must be non-negative numbers');
|
|
||||||
} else if (this.strategy === 'custom') {
|
|
||||||
const total = this.weights.reduce((sum, w) => sum + w, 0);
|
|
||||||
if (Math.abs(total - 1.0) > 1e-6) {
|
|
||||||
errors.push('Custom weights must sum to 1.0');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.strategy === 'custom' && (!this.weights || this.weights.length === 0)) {
|
|
||||||
errors.push('Custom fusion requires weights');
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
const weightsStr = this.hasWeights() ? ` weights[${this.weights.length}]` : '';
|
|
||||||
return `Fusion(${this.strategy}, ${this.evidence.length} evidence${weightsStr})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
import { BaseNode } from './BaseNode.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AST node for measure body containing expressions
|
|
||||||
* Represents: { user.role }
|
|
||||||
*/
|
|
||||||
export class MeasureBodyNode extends BaseNode {
|
|
||||||
constructor(location = null) {
|
|
||||||
super('MeasureBody', location);
|
|
||||||
this.expression = null; // ExpressionNode
|
|
||||||
this.fusion = null; // FusionNode (optional)
|
|
||||||
this.aggregation = null; // AggregationNode (optional)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the expression for this measure body
|
|
||||||
* @param {ExpressionNode} expression - Expression to set
|
|
||||||
*/
|
|
||||||
setExpression(expression) {
|
|
||||||
this.expression = expression;
|
|
||||||
this.addChild(expression);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the fusion for this measure body
|
|
||||||
* @param {FusionNode} fusion - Fusion to set
|
|
||||||
*/
|
|
||||||
setFusion(fusion) {
|
|
||||||
this.fusion = fusion;
|
|
||||||
this.addChild(fusion);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the aggregation for this measure body
|
|
||||||
* @param {AggregationNode} aggregation - Aggregation to set
|
|
||||||
*/
|
|
||||||
setAggregation(aggregation) {
|
|
||||||
this.aggregation = aggregation;
|
|
||||||
this.addChild(aggregation);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the expression
|
|
||||||
* @returns {ExpressionNode|null} Expression or null
|
|
||||||
*/
|
|
||||||
getExpression() {
|
|
||||||
return this.expression;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the fusion
|
|
||||||
* @returns {FusionNode|null} Fusion or null
|
|
||||||
*/
|
|
||||||
getFusion() {
|
|
||||||
return this.fusion;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the aggregation
|
|
||||||
* @returns {AggregationNode|null} Aggregation or null
|
|
||||||
*/
|
|
||||||
getAggregation() {
|
|
||||||
return this.aggregation;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this measure body has an expression
|
|
||||||
* @returns {boolean} True if has expression
|
|
||||||
*/
|
|
||||||
hasExpression() {
|
|
||||||
return this.expression !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this measure body has fusion
|
|
||||||
* @returns {boolean} True if has fusion
|
|
||||||
*/
|
|
||||||
hasFusion() {
|
|
||||||
return this.fusion !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this measure body has aggregation
|
|
||||||
* @returns {boolean} True if has aggregation
|
|
||||||
*/
|
|
||||||
hasAggregation() {
|
|
||||||
return this.aggregation !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate the measure body
|
|
||||||
* @returns {string[]} Array of error messages
|
|
||||||
*/
|
|
||||||
validate() {
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
// Must have at least one of expression, fusion, or aggregation
|
|
||||||
if (!this.expression && !this.fusion && !this.aggregation) {
|
|
||||||
errors.push('Measure body must have an expression, fusion, or aggregation');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate expression if present
|
|
||||||
if (this.expression) {
|
|
||||||
const exprErrors = this.expression.validate ? this.expression.validate() : [];
|
|
||||||
errors.push(...exprErrors);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate fusion if present
|
|
||||||
if (this.fusion) {
|
|
||||||
const fusionErrors = this.fusion.validate ? this.fusion.validate() : [];
|
|
||||||
errors.push(...fusionErrors);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate aggregation if present
|
|
||||||
if (this.aggregation) {
|
|
||||||
const aggErrors = this.aggregation.validate ? this.aggregation.validate() : [];
|
|
||||||
errors.push(...aggErrors);
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
const parts = [];
|
|
||||||
if (this.expression) parts.push(this.expression.toString());
|
|
||||||
if (this.fusion) parts.push(this.fusion.toString());
|
|
||||||
if (this.aggregation) parts.push(this.aggregation.toString());
|
|
||||||
return `MeasureBody(${parts.join(', ')})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
import { BaseNode } from './BaseNode.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AST node for measure definitions
|
|
||||||
* Represents: measure userRole(user: User) { ... } PROVIDES string
|
|
||||||
*/
|
|
||||||
export class MeasureNode extends BaseNode {
|
|
||||||
constructor(name, location = null) {
|
|
||||||
super('Measure', location);
|
|
||||||
this.name = name;
|
|
||||||
this.parameters = [];
|
|
||||||
this.returnType = null;
|
|
||||||
this.body = null; // MeasureBodyNode
|
|
||||||
this.provides = null; // Return type specification
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a parameter to the measure
|
|
||||||
* @param {ParameterNode} parameter - Parameter to add
|
|
||||||
*/
|
|
||||||
addParameter(parameter) {
|
|
||||||
this.parameters.push(parameter);
|
|
||||||
this.addChild(parameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the body of the measure
|
|
||||||
* @param {MeasureBodyNode} body - Measure body
|
|
||||||
*/
|
|
||||||
setBody(body) {
|
|
||||||
this.body = body;
|
|
||||||
this.addChild(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the return type for this measure
|
|
||||||
* @param {string} returnType - Return type
|
|
||||||
*/
|
|
||||||
setReturnType(returnType) {
|
|
||||||
this.returnType = returnType;
|
|
||||||
this.provides = returnType;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the parameter names as an array
|
|
||||||
* @returns {string[]} Array of parameter names
|
|
||||||
*/
|
|
||||||
getParameterNames() {
|
|
||||||
return this.parameters.map(param => param.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the parameter types as an array
|
|
||||||
* @returns {string[]} Array of parameter types
|
|
||||||
*/
|
|
||||||
getParameterTypes() {
|
|
||||||
return this.parameters.map(param => param.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a parameter by name
|
|
||||||
* @param {string} name - Parameter name to find
|
|
||||||
* @returns {ParameterNode|null} Found parameter or null
|
|
||||||
*/
|
|
||||||
getParameter(name) {
|
|
||||||
return this.parameters.find(param => param.name === name) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the signature string for this measure
|
|
||||||
* @returns {string} Measure signature
|
|
||||||
*/
|
|
||||||
getSignature() {
|
|
||||||
const paramStr = this.parameters.map(param => `${param.name}: ${param.type}`).join(', ');
|
|
||||||
return `${this.name}(${paramStr})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this measure has a return type
|
|
||||||
* @returns {boolean} True if has return type
|
|
||||||
*/
|
|
||||||
hasReturnType() {
|
|
||||||
return this.returnType !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate the measure
|
|
||||||
* @returns {string[]} Array of error messages
|
|
||||||
*/
|
|
||||||
validate() {
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
// Validate measure name
|
|
||||||
if (!this.name || typeof this.name !== 'string') {
|
|
||||||
errors.push(`Invalid measure name: ${this.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate parameters
|
|
||||||
this.parameters.forEach((param, index) => {
|
|
||||||
const paramErrors = param.validate ? param.validate() : [];
|
|
||||||
errors.push(...paramErrors.map(err => `Parameter ${index + 1}: ${err}`));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Validate body
|
|
||||||
if (this.body) {
|
|
||||||
const bodyErrors = this.body.validate ? this.body.validate() : [];
|
|
||||||
errors.push(...bodyErrors);
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
const providesStr = this.returnType ? ` PROVIDES ${this.returnType}` : '';
|
|
||||||
return `Measure(${this.getSignature()}${providesStr})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import { BaseNode } from './BaseNode.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AST node for function/evidence parameters
|
|
||||||
* Represents: user: User, role: string
|
|
||||||
*/
|
|
||||||
export class ParameterNode extends BaseNode {
|
|
||||||
constructor(name, type, location = null) {
|
|
||||||
super('Parameter', location);
|
|
||||||
this.name = name;
|
|
||||||
this.type = type;
|
|
||||||
this.isOptional = false;
|
|
||||||
this.defaultValue = null;
|
|
||||||
this.isArray = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set whether this parameter is optional
|
|
||||||
* @param {boolean} optional - Whether parameter is optional
|
|
||||||
*/
|
|
||||||
setOptional(optional) {
|
|
||||||
this.isOptional = optional;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set default value for this parameter
|
|
||||||
* @param {*} value - Default value
|
|
||||||
*/
|
|
||||||
setDefaultValue(value) {
|
|
||||||
this.defaultValue = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set whether this parameter is an array
|
|
||||||
* @param {boolean} isArray - Whether parameter is an array
|
|
||||||
*/
|
|
||||||
setArray(isArray) {
|
|
||||||
this.isArray = isArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the full type string including array notation
|
|
||||||
* @returns {string} Full type string
|
|
||||||
*/
|
|
||||||
getFullType() {
|
|
||||||
let typeStr = this.type;
|
|
||||||
if (this.isArray) {
|
|
||||||
typeStr += '[]';
|
|
||||||
}
|
|
||||||
if (this.isOptional) {
|
|
||||||
typeStr += '?';
|
|
||||||
}
|
|
||||||
return typeStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate the parameter
|
|
||||||
* @returns {string[]} Array of error messages
|
|
||||||
*/
|
|
||||||
validate() {
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
// Validate parameter name
|
|
||||||
if (!this.name || typeof this.name !== 'string') {
|
|
||||||
errors.push(`Invalid parameter name: ${this.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate type
|
|
||||||
if (!this.type || typeof this.type !== 'string') {
|
|
||||||
errors.push(`Invalid parameter type: ${this.type}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
return `Parameter(${this.name}: ${this.getFullType()})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
import { BaseNode } from './BaseNode.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AST node for pattern matching statements
|
|
||||||
* Represents: isMember(user, *group) { ... } limit 5
|
|
||||||
*/
|
|
||||||
export class PatternMatchNode extends BaseNode {
|
|
||||||
constructor(location = null) {
|
|
||||||
super('PatternMatch', location);
|
|
||||||
this.predicate = null; // PredicateNode
|
|
||||||
this.body = null; // EvidenceBodyNode
|
|
||||||
this.limit = null;
|
|
||||||
this.withClause = null; // WithClauseNode
|
|
||||||
this.negated = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the predicate for this pattern match
|
|
||||||
* @param {PredicateNode} predicate - Predicate to set
|
|
||||||
*/
|
|
||||||
setPredicate(predicate) {
|
|
||||||
this.predicate = predicate;
|
|
||||||
this.addChild(predicate);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the body of the pattern match
|
|
||||||
* @param {EvidenceBodyNode} body - Evidence body
|
|
||||||
*/
|
|
||||||
setBody(body) {
|
|
||||||
this.body = body;
|
|
||||||
this.addChild(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the limit for this pattern match
|
|
||||||
* @param {number} limit - Limit value
|
|
||||||
*/
|
|
||||||
setLimit(limit) {
|
|
||||||
this.limit = limit;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the with clause for this pattern match
|
|
||||||
* @param {WithClauseNode} withClause - With clause
|
|
||||||
*/
|
|
||||||
setWithClause(withClause) {
|
|
||||||
this.withClause = withClause;
|
|
||||||
this.addChild(withClause);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set whether this pattern match is negated
|
|
||||||
* @param {boolean} negated - Whether pattern match is negated
|
|
||||||
*/
|
|
||||||
setNegated(negated) {
|
|
||||||
this.negated = negated;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this pattern match is negated
|
|
||||||
* @returns {boolean} True if negated
|
|
||||||
*/
|
|
||||||
isNegated() {
|
|
||||||
return this.negated;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the predicate name
|
|
||||||
* @returns {string|null} Predicate name or null
|
|
||||||
*/
|
|
||||||
getPredicateName() {
|
|
||||||
return this.predicate ? this.predicate.name : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the predicate arguments
|
|
||||||
* @returns {ExpressionNode[]} Predicate arguments
|
|
||||||
*/
|
|
||||||
getArguments() {
|
|
||||||
return this.predicate ? this.predicate.arguments : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this pattern match has a limit
|
|
||||||
* @returns {boolean} True if has limit
|
|
||||||
*/
|
|
||||||
hasLimit() {
|
|
||||||
return this.limit !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this pattern match has a with clause
|
|
||||||
* @returns {boolean} True if has with clause
|
|
||||||
*/
|
|
||||||
hasWithClause() {
|
|
||||||
return this.withClause !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate the pattern match
|
|
||||||
* @returns {string[]} Array of error messages
|
|
||||||
*/
|
|
||||||
validate() {
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
// Validate predicate
|
|
||||||
if (!this.predicate) {
|
|
||||||
errors.push('Pattern match must have a predicate');
|
|
||||||
} else {
|
|
||||||
const predErrors = this.predicate.validate ? this.predicate.validate() : [];
|
|
||||||
errors.push(...predErrors);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate body
|
|
||||||
if (this.body) {
|
|
||||||
const bodyErrors = this.body.validate ? this.body.validate() : [];
|
|
||||||
errors.push(...bodyErrors);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate limit
|
|
||||||
if (this.limit !== null && (typeof this.limit !== 'number' || this.limit < 0)) {
|
|
||||||
errors.push(`Invalid limit: ${this.limit}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate with clause
|
|
||||||
if (this.withClause) {
|
|
||||||
const withErrors = this.withClause.validate ? this.withClause.validate() : [];
|
|
||||||
errors.push(...withErrors);
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
const negStr = this.negated ? 'NOT ' : '';
|
|
||||||
const predStr = this.predicate ? this.predicate.toString() : 'null';
|
|
||||||
const limitStr = this.limit ? ` limit ${this.limit}` : '';
|
|
||||||
const withStr = this.withClause ? ` ${this.withClause.toString()}` : '';
|
|
||||||
return `PatternMatch(${negStr}${predStr}${limitStr}${withStr})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import { BaseNode } from './BaseNode.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AST node for predicate calls
|
|
||||||
* Represents: hasRole(user, role), owns(user, doc)
|
|
||||||
*/
|
|
||||||
export class PredicateNode extends BaseNode {
|
|
||||||
constructor(name, location = null) {
|
|
||||||
super('Predicate', location);
|
|
||||||
this.name = name;
|
|
||||||
this.arguments = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add an argument to this predicate
|
|
||||||
* @param {ExpressionNode} argument - Argument to add
|
|
||||||
*/
|
|
||||||
addArgument(argument) {
|
|
||||||
this.arguments.push(argument);
|
|
||||||
this.addChild(argument);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the predicate name
|
|
||||||
* @returns {string} Predicate name
|
|
||||||
*/
|
|
||||||
getName() {
|
|
||||||
return this.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all arguments
|
|
||||||
* @returns {ExpressionNode[]} Predicate arguments
|
|
||||||
*/
|
|
||||||
getArguments() {
|
|
||||||
return this.arguments;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the number of arguments
|
|
||||||
* @returns {number} Number of arguments
|
|
||||||
*/
|
|
||||||
getArgumentCount() {
|
|
||||||
return this.arguments.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an argument by index
|
|
||||||
* @param {number} index - Argument index
|
|
||||||
* @returns {ExpressionNode|null} Argument or null
|
|
||||||
*/
|
|
||||||
getArgument(index) {
|
|
||||||
return this.arguments[index] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this predicate has a specific number of arguments
|
|
||||||
* @param {number} count - Expected argument count
|
|
||||||
* @returns {boolean} True if has expected count
|
|
||||||
*/
|
|
||||||
hasArgumentCount(count) {
|
|
||||||
return this.arguments.length === count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this predicate has any arguments
|
|
||||||
* @returns {boolean} True if has arguments
|
|
||||||
*/
|
|
||||||
hasArguments() {
|
|
||||||
return this.arguments.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the signature string for this predicate
|
|
||||||
* @returns {string} Predicate signature
|
|
||||||
*/
|
|
||||||
getSignature() {
|
|
||||||
const argStr = this.arguments.map(arg => arg.toString()).join(', ');
|
|
||||||
return `${this.name}(${argStr})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate the predicate
|
|
||||||
* @returns {string[]} Array of error messages
|
|
||||||
*/
|
|
||||||
validate() {
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
// Validate predicate name
|
|
||||||
if (!this.name || typeof this.name !== 'string') {
|
|
||||||
errors.push(`Invalid predicate name: ${this.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate arguments
|
|
||||||
this.arguments.forEach((arg, index) => {
|
|
||||||
const argErrors = arg.validate ? arg.validate() : [];
|
|
||||||
errors.push(...argErrors.map(err => `Argument ${index + 1}: ${err}`));
|
|
||||||
});
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
return `Predicate(${this.getSignature()})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
import { BaseNode } from './BaseNode.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Root node of the AST representing the entire DSL program
|
|
||||||
*/
|
|
||||||
export class ProgramNode extends BaseNode {
|
|
||||||
constructor(location = null) {
|
|
||||||
super('Program', location);
|
|
||||||
this.definitions = [];
|
|
||||||
this.facts = [];
|
|
||||||
this.evidence = [];
|
|
||||||
this.measures = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a definition to the program
|
|
||||||
* @param {DefinitionNode} definition - Definition to add
|
|
||||||
*/
|
|
||||||
addDefinition(definition) {
|
|
||||||
this.definitions.push(definition);
|
|
||||||
this.addChild(definition);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a fact to the program
|
|
||||||
* @param {FactNode} fact - Fact to add
|
|
||||||
*/
|
|
||||||
addFact(fact) {
|
|
||||||
this.facts.push(fact);
|
|
||||||
this.addChild(fact);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add evidence to the program
|
|
||||||
* @param {EvidenceNode} evidence - Evidence to add
|
|
||||||
*/
|
|
||||||
addEvidence(evidence) {
|
|
||||||
this.evidence.push(evidence);
|
|
||||||
this.addChild(evidence);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a measure to the program
|
|
||||||
* @param {MeasureNode} measure - Measure to add
|
|
||||||
*/
|
|
||||||
addMeasure(measure) {
|
|
||||||
this.measures.push(measure);
|
|
||||||
this.addChild(measure);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all definitions of a specific type
|
|
||||||
* @param {string} type - Definition type to filter by
|
|
||||||
* @returns {DefinitionNode[]} Filtered definitions
|
|
||||||
*/
|
|
||||||
getDefinitionsOfType(type) {
|
|
||||||
return this.definitions.filter(def => def.definitionType === type);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a definition by name
|
|
||||||
* @param {string} name - Name to search for
|
|
||||||
* @returns {DefinitionNode|null} Found definition or null
|
|
||||||
*/
|
|
||||||
getDefinitionByName(name) {
|
|
||||||
return this.definitions.find(def => def.name === name) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find evidence by name
|
|
||||||
* @param {string} name - Name to search for
|
|
||||||
* @returns {EvidenceNode|null} Found evidence or null
|
|
||||||
*/
|
|
||||||
getEvidenceByName(name) {
|
|
||||||
return this.evidence.find(ev => ev.name === name) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a fact by name
|
|
||||||
* @param {string} name - Name to search for
|
|
||||||
* @returns {FactNode|null} Found fact or null
|
|
||||||
*/
|
|
||||||
getFactByName(name) {
|
|
||||||
return this.facts.find(fact => fact.name === name) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a measure by name
|
|
||||||
* @param {string} name - Name to search for
|
|
||||||
* @returns {MeasureNode|null} Found measure or null
|
|
||||||
*/
|
|
||||||
getMeasureByName(name) {
|
|
||||||
return this.measures.find(measure => measure.name === name) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all symbols (definitions, facts, evidence, measures) by name
|
|
||||||
* @param {string} name - Name to search for
|
|
||||||
* @returns {BaseNode[]} All matching symbols
|
|
||||||
*/
|
|
||||||
getSymbolsByName(name) {
|
|
||||||
return [
|
|
||||||
...this.definitions.filter(def => def.name === name),
|
|
||||||
...this.facts.filter(fact => fact.name === name),
|
|
||||||
...this.evidence.filter(ev => ev.name === name),
|
|
||||||
...this.measures.filter(measure => measure.name === name)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate the program structure
|
|
||||||
* @returns {Object} Validation result with errors and warnings
|
|
||||||
*/
|
|
||||||
validate() {
|
|
||||||
const errors = [];
|
|
||||||
const warnings = [];
|
|
||||||
|
|
||||||
// Check for duplicate names
|
|
||||||
const allNames = new Map();
|
|
||||||
[...this.definitions, ...this.facts, ...this.evidence, ...this.measures].forEach(symbol => {
|
|
||||||
if (allNames.has(symbol.name)) {
|
|
||||||
errors.push(`Duplicate symbol name: ${symbol.name}`);
|
|
||||||
} else {
|
|
||||||
allNames.set(symbol.name, symbol);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Validate each definition
|
|
||||||
this.definitions.forEach(def => {
|
|
||||||
const defErrors = def.validate ? def.validate() : [];
|
|
||||||
errors.push(...defErrors);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Validate each fact
|
|
||||||
this.facts.forEach(fact => {
|
|
||||||
const factErrors = fact.validate ? fact.validate() : [];
|
|
||||||
errors.push(...factErrors);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Validate each evidence
|
|
||||||
this.evidence.forEach(ev => {
|
|
||||||
const evErrors = ev.validate ? ev.validate() : [];
|
|
||||||
errors.push(...evErrors);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Validate each measure
|
|
||||||
this.measures.forEach(measure => {
|
|
||||||
const measureErrors = measure.validate ? measure.validate() : [];
|
|
||||||
errors.push(...measureErrors);
|
|
||||||
});
|
|
||||||
|
|
||||||
return { errors, warnings, isValid: errors.length === 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
return `Program(${this.definitions.length} definitions, ${this.facts.length} facts, ${this.evidence.length} evidence, ${this.measures.length} measures)`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
import { BaseNode } from './BaseNode.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AST node for source definitions
|
|
||||||
* Represents: source *mfa(user: User) PROVIDES Proof within 10m
|
|
||||||
*
|
|
||||||
* Sources are injectable object/proof references that must be
|
|
||||||
* provided in the partial graph before authorization evaluation.
|
|
||||||
* They always carry a PROVIDES type and an optional freshness window.
|
|
||||||
*/
|
|
||||||
export class SourceNode extends BaseNode {
|
|
||||||
constructor(name, location = null) {
|
|
||||||
super('Source', location);
|
|
||||||
this.name = name;
|
|
||||||
this.injectable = false;
|
|
||||||
this.parameters = [];
|
|
||||||
this.returnType = null;
|
|
||||||
this.provides = null;
|
|
||||||
this.within = null;
|
|
||||||
this.cacheDirective = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
addParameter(parameter) {
|
|
||||||
this.parameters.push(parameter);
|
|
||||||
this.addChild(parameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
setReturnType(returnType) {
|
|
||||||
this.returnType = returnType;
|
|
||||||
this.provides = returnType;
|
|
||||||
}
|
|
||||||
|
|
||||||
setWithin(within) {
|
|
||||||
this.within = within;
|
|
||||||
}
|
|
||||||
|
|
||||||
setCacheDirective(directive) {
|
|
||||||
this.cacheDirective = directive;
|
|
||||||
}
|
|
||||||
|
|
||||||
setInjectable(value) {
|
|
||||||
this.injectable = !!value;
|
|
||||||
}
|
|
||||||
|
|
||||||
getParameterNames() {
|
|
||||||
return this.parameters.map(param => param.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
getParameterTypes() {
|
|
||||||
return this.parameters.map(param => param.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
getParameter(name) {
|
|
||||||
return this.parameters.find(param => param.name === name) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getSignature() {
|
|
||||||
const paramStr = this.parameters.map(param => `${param.name}: ${param.type}`).join(', ');
|
|
||||||
return `${this.name}(${paramStr})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasReturnType() {
|
|
||||||
return this.returnType !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
validate() {
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
if (!this.name || typeof this.name !== 'string') {
|
|
||||||
errors.push(`Invalid source name: ${this.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.parameters.forEach((param, index) => {
|
|
||||||
const paramErrors = param.validate ? param.validate() : [];
|
|
||||||
errors.push(...paramErrors.map(err => `Parameter ${index + 1}: ${err}`));
|
|
||||||
});
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
const injectableStr = this.injectable ? '*' : '';
|
|
||||||
const providesStr = this.returnType ? ` PROVIDES ${this.returnType}` : '';
|
|
||||||
const withinStr = this.within ? ` within ${this.within.value}` : '';
|
|
||||||
return `Source(${injectableStr}${this.getSignature()}${providesStr}${withinStr})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
import { BaseNode } from './BaseNode.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AST node for with clauses in pattern matching
|
|
||||||
* Represents: with similarity > 0.7
|
|
||||||
*/
|
|
||||||
export class WithClauseNode extends BaseNode {
|
|
||||||
constructor(location = null) {
|
|
||||||
super('WithClause', location);
|
|
||||||
this.condition = null; // ExpressionNode
|
|
||||||
this.operator = null; // '>', '>=', '<', '<=', '==', '!='
|
|
||||||
this.value = null; // Literal value
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the condition for this with clause
|
|
||||||
* @param {ExpressionNode} condition - Condition to set
|
|
||||||
*/
|
|
||||||
setCondition(condition) {
|
|
||||||
this.condition = condition;
|
|
||||||
this.addChild(condition);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the operator for this with clause
|
|
||||||
* @param {string} operator - Operator to set
|
|
||||||
*/
|
|
||||||
setOperator(operator) {
|
|
||||||
this.operator = operator;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the value for this with clause
|
|
||||||
* @param {*} value - Value to set
|
|
||||||
*/
|
|
||||||
setValue(value) {
|
|
||||||
this.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the condition expression
|
|
||||||
* @returns {ExpressionNode|null} Condition expression or null
|
|
||||||
*/
|
|
||||||
getCondition() {
|
|
||||||
return this.condition;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the operator
|
|
||||||
* @returns {string|null} Operator or null
|
|
||||||
*/
|
|
||||||
getOperator() {
|
|
||||||
return this.operator;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the value
|
|
||||||
* @returns {*} Value or null
|
|
||||||
*/
|
|
||||||
getValue() {
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is a greater than comparison
|
|
||||||
* @returns {boolean} True if greater than
|
|
||||||
*/
|
|
||||||
isGreaterThan() {
|
|
||||||
return this.operator === '>';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is a greater than or equal comparison
|
|
||||||
* @returns {boolean} True if greater than or equal
|
|
||||||
*/
|
|
||||||
isGreaterThanOrEqual() {
|
|
||||||
return this.operator === '>=';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is a less than comparison
|
|
||||||
* @returns {boolean} True if less than
|
|
||||||
*/
|
|
||||||
isLessThan() {
|
|
||||||
return this.operator === '<';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is a less than or equal comparison
|
|
||||||
* @returns {boolean} True if less than or equal
|
|
||||||
*/
|
|
||||||
isLessThanOrEqual() {
|
|
||||||
return this.operator === '<=';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is an equality comparison
|
|
||||||
* @returns {boolean} True if equality
|
|
||||||
*/
|
|
||||||
isEqual() {
|
|
||||||
return this.operator === '==';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this is a not equal comparison
|
|
||||||
* @returns {boolean} True if not equal
|
|
||||||
*/
|
|
||||||
isNotEqual() {
|
|
||||||
return this.operator === '!=';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the comparison string
|
|
||||||
* @returns {string} Comparison string
|
|
||||||
*/
|
|
||||||
getComparisonString() {
|
|
||||||
const condStr = this.condition ? this.condition.toString() : 'null';
|
|
||||||
const valStr = this.value !== null ? this.value.toString() : 'null';
|
|
||||||
return `${condStr} ${this.operator} ${valStr}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate the with clause
|
|
||||||
* @returns {string[]} Array of error messages
|
|
||||||
*/
|
|
||||||
validate() {
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
// Validate condition
|
|
||||||
if (!this.condition) {
|
|
||||||
errors.push('With clause must have a condition');
|
|
||||||
} else {
|
|
||||||
const condErrors = this.condition.validate ? this.condition.validate() : [];
|
|
||||||
errors.push(...condErrors);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate operator
|
|
||||||
const validOperators = ['>', '>=', '<', '<=', '==', '!='];
|
|
||||||
if (!this.operator || !validOperators.includes(this.operator)) {
|
|
||||||
errors.push(`Invalid operator: ${this.operator}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate value
|
|
||||||
if (this.value === null) {
|
|
||||||
errors.push('With clause must have a value');
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
return `WithClause(${this.getComparisonString()})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
/**
|
|
||||||
* AST Node exports
|
|
||||||
* Central export file for all AST node classes
|
|
||||||
*/
|
|
||||||
|
|
||||||
export { BaseNode } from './BaseNode.js';
|
|
||||||
export { ProgramNode } from './ProgramNode.js';
|
|
||||||
export { DefinitionNode } from './DefinitionNode.js';
|
|
||||||
export { FieldNode } from './FieldNode.js';
|
|
||||||
export { BehaviorNode } from './BehaviorNode.js';
|
|
||||||
export { FactNode } from './FactNode.js';
|
|
||||||
export { ParameterNode } from './ParameterNode.js';
|
|
||||||
export { EvidenceNode } from './EvidenceNode.js';
|
|
||||||
export { EvidenceBodyNode } from './EvidenceBodyNode.js';
|
|
||||||
export { DirectEvidenceNode } from './DirectEvidenceNode.js';
|
|
||||||
export { PatternMatchNode } from './PatternMatchNode.js';
|
|
||||||
export { DefeasibleLogicNode } from './DefeasibleLogicNode.js';
|
|
||||||
export { FusionNode } from './FusionNode.js';
|
|
||||||
export { PredicateNode } from './PredicateNode.js';
|
|
||||||
export { ExpressionNode } from './ExpressionNode.js';
|
|
||||||
export { WithClauseNode } from './WithClauseNode.js';
|
|
||||||
export { MeasureNode } from './MeasureNode.js';
|
|
||||||
export { MeasureBodyNode } from './MeasureBodyNode.js';
|
|
||||||
export { AggregationNode } from './AggregationNode.js';
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,114 +0,0 @@
|
|||||||
import * as GeneratedParser from './GeneratedParser.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Peggy-based DSL Parser
|
|
||||||
* Uses the generated parser from Peggy grammar
|
|
||||||
*/
|
|
||||||
export class PeggyDSLParser {
|
|
||||||
constructor() {
|
|
||||||
this.parser = GeneratedParser;
|
|
||||||
this.errors = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse DSL text into AST
|
|
||||||
* @param {string} dslText - DSL text to parse
|
|
||||||
* @returns {ProgramNode} Parsed AST
|
|
||||||
*/
|
|
||||||
parse(dslText) {
|
|
||||||
this.errors = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const program = this.parser.parse(dslText);
|
|
||||||
return program;
|
|
||||||
} catch (error) {
|
|
||||||
this.errors.push(`Parse error: ${error.message}`);
|
|
||||||
|
|
||||||
// If the error has location information, add it to the error
|
|
||||||
if (error.location) {
|
|
||||||
const location = error.location;
|
|
||||||
this.errors.push(`Location: line ${location.start.line}, column ${location.start.column}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the error has expected/found information, add it
|
|
||||||
if (error.expected && error.found) {
|
|
||||||
this.errors.push(`Expected: ${error.expected.join(', ')}`);
|
|
||||||
this.errors.push(`Found: ${error.found}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Parsing failed: ${this.errors.join('; ')}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get parser errors from last parse
|
|
||||||
* @returns {string[]} Array of parser errors
|
|
||||||
*/
|
|
||||||
getErrors() {
|
|
||||||
return this.errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate DSL text without throwing errors
|
|
||||||
* @param {string} dslText - DSL text to validate
|
|
||||||
* @returns {Object} Validation result with success status and errors
|
|
||||||
*/
|
|
||||||
validate(dslText) {
|
|
||||||
try {
|
|
||||||
const program = this.parse(dslText);
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
errors: [],
|
|
||||||
program: program
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
errors: this.errors,
|
|
||||||
program: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse with options
|
|
||||||
* @param {string} dslText - DSL text to parse
|
|
||||||
* @param {Object} options - Parser options
|
|
||||||
* @returns {ProgramNode} Parsed AST
|
|
||||||
*/
|
|
||||||
parseWithOptions(dslText, options = {}) {
|
|
||||||
this.errors = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const program = this.parser.parse(dslText, options);
|
|
||||||
return program;
|
|
||||||
} catch (error) {
|
|
||||||
this.errors.push(`Parse error: ${error.message}`);
|
|
||||||
|
|
||||||
if (error.location) {
|
|
||||||
const location = error.location;
|
|
||||||
this.errors.push(`Location: line ${location.start.line}, column ${location.start.column}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.expected && error.found) {
|
|
||||||
this.errors.push(`Expected: ${error.expected.join(', ')}`);
|
|
||||||
this.errors.push(`Found: ${error.found}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Parsing failed: ${this.errors.join('; ')}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get parser information
|
|
||||||
* @returns {Object} Parser information
|
|
||||||
*/
|
|
||||||
getParserInfo() {
|
|
||||||
return {
|
|
||||||
name: 'PeggyDSLParser',
|
|
||||||
version: '1.0.0',
|
|
||||||
generated: true,
|
|
||||||
grammar: 'dsl.peggy'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
// Wrapper for Peggy-generated parser that handles ESM imports correctly
|
|
||||||
import {
|
|
||||||
ProgramNode, DefinitionNode, FieldNode, BehaviorNode, FactNode, ParameterNode,
|
|
||||||
EvidenceNode, EvidenceBodyNode, DirectEvidenceNode, PatternMatchNode,
|
|
||||||
DefeasibleLogicNode, FusionNode, PredicateNode, ExpressionNode, WithClauseNode,
|
|
||||||
MeasureNode, MeasureBodyNode, AggregationNode
|
|
||||||
} from '../nodes/index.js';
|
|
||||||
|
|
||||||
// Make AST nodes globally available to the generated parser
|
|
||||||
global.ProgramNode = ProgramNode;
|
|
||||||
global.DefinitionNode = DefinitionNode;
|
|
||||||
global.FieldNode = FieldNode;
|
|
||||||
global.BehaviorNode = BehaviorNode;
|
|
||||||
global.FactNode = FactNode;
|
|
||||||
global.ParameterNode = ParameterNode;
|
|
||||||
global.EvidenceNode = EvidenceNode;
|
|
||||||
global.EvidenceBodyNode = EvidenceBodyNode;
|
|
||||||
global.DirectEvidenceNode = DirectEvidenceNode;
|
|
||||||
global.PatternMatchNode = PatternMatchNode;
|
|
||||||
global.DefeasibleLogicNode = DefeasibleLogicNode;
|
|
||||||
global.FusionNode = FusionNode;
|
|
||||||
global.PredicateNode = PredicateNode;
|
|
||||||
global.ExpressionNode = ExpressionNode;
|
|
||||||
global.WithClauseNode = WithClauseNode;
|
|
||||||
global.MeasureNode = MeasureNode;
|
|
||||||
global.MeasureBodyNode = MeasureBodyNode;
|
|
||||||
global.AggregationNode = AggregationNode;
|
|
||||||
|
|
||||||
// Import the generated parser
|
|
||||||
import { parse } from './GeneratedParser.js';
|
|
||||||
|
|
||||||
// Create a wrapper class that matches the expected interface
|
|
||||||
export class PeggyDSLParser {
|
|
||||||
static parse(text) {
|
|
||||||
console.log('PeggyDSLParser: Parsing text:', text.substring(0, 100) + '...');
|
|
||||||
try {
|
|
||||||
const result = parse(text);
|
|
||||||
console.log('PeggyDSLParser: Parse result:', result);
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('PeggyDSLParser: Parse error:', error.message);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,381 +0,0 @@
|
|||||||
/**
|
|
||||||
* Type Definition Tests
|
|
||||||
*
|
|
||||||
* Tests the type definition system of the Evidence DSL,
|
|
||||||
* including fields, behaviors, caching, and complex type structures.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { DSLCompiler } from '../DSLCompiler.js';
|
|
||||||
|
|
||||||
export class DefinitionTests {
|
|
||||||
constructor() {
|
|
||||||
this.arbiter = null;
|
|
||||||
this.compiler = null;
|
|
||||||
this.testResults = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
setup(arbiter) {
|
|
||||||
this.arbiter = arbiter;
|
|
||||||
this.compiler = new DSLCompiler(arbiter);
|
|
||||||
}
|
|
||||||
|
|
||||||
runAllTests() {
|
|
||||||
console.log('=== Type Definition Tests ===\n');
|
|
||||||
|
|
||||||
this.testBasicDefinitions();
|
|
||||||
this.testFieldTypes();
|
|
||||||
this.testArrayTypes();
|
|
||||||
this.testBehaviors();
|
|
||||||
this.testCaching();
|
|
||||||
this.testComplexDefinitions();
|
|
||||||
this.testDefinitionErrors();
|
|
||||||
|
|
||||||
return this.getTestResults();
|
|
||||||
}
|
|
||||||
|
|
||||||
testBasicDefinitions() {
|
|
||||||
console.log('Testing Basic Definitions...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `definition User { role: string }`,
|
|
||||||
description: 'Simple definition with one field'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
role: string
|
|
||||||
isActive: boolean
|
|
||||||
}`,
|
|
||||||
description: 'Definition with multiple fields'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition Group {
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
created: timestamp
|
|
||||||
}`,
|
|
||||||
description: 'Definition with different field types'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const result = this.compiler.compile(input, `test-basic-def-${Date.now()}`);
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
this.assert(result.program.definitions.length > 0, 'Should have definitions');
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.fail(`Basic definition test: ${description}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testFieldTypes() {
|
|
||||||
console.log('Testing Field Types...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{ type: 'string', description: 'String field type' },
|
|
||||||
{ type: 'number', description: 'Number field type' },
|
|
||||||
{ type: 'boolean', description: 'Boolean field type' },
|
|
||||||
{ type: 'timestamp', description: 'Timestamp field type' },
|
|
||||||
{ type: 'User', description: 'Custom type field' },
|
|
||||||
{ type: 'Permission', description: 'Another custom type field' }
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ type, description }) => {
|
|
||||||
try {
|
|
||||||
const dsl = `definition Test { field: ${type} }`;
|
|
||||||
const result = this.compiler.compile(dsl, `test-field-type-${Date.now()}`);
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.fail(`Field type test: ${description}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testArrayTypes() {
|
|
||||||
console.log('Testing Array Types...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{ type: 'string[]', description: 'String array' },
|
|
||||||
{ type: 'number[]', description: 'Number array' },
|
|
||||||
{ type: 'boolean[]', description: 'Boolean array' },
|
|
||||||
{ type: 'Permission[]', description: 'Custom type array' },
|
|
||||||
{ type: 'User[]', description: 'User array' }
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ type, description }) => {
|
|
||||||
try {
|
|
||||||
const dsl = `definition Test { items: ${type} }`;
|
|
||||||
const result = this.compiler.compile(dsl, `test-array-${Date.now()}`);
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.fail(`Array type test: ${description}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testBehaviors() {
|
|
||||||
console.log('Testing Behaviors...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
balance: number BEHAVES { decaying down hourly }
|
|
||||||
}`,
|
|
||||||
description: 'Decay behavior - down hourly'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
reputation: number BEHAVES { decaying up daily }
|
|
||||||
}`,
|
|
||||||
description: 'Decay behavior - up daily'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
score: number BEHAVES { decaying neutral weekly }
|
|
||||||
}`,
|
|
||||||
description: 'Decay behavior - neutral weekly'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
stability: number BEHAVES { decaying stable monthly }
|
|
||||||
}`,
|
|
||||||
description: 'Decay behavior - stable monthly'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
confidence: number BEHAVES { blurring fixed }
|
|
||||||
}`,
|
|
||||||
description: 'Blur behavior - fixed'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
accuracy: number BEHAVES { blurring adaptive }
|
|
||||||
}`,
|
|
||||||
description: 'Blur behavior - adaptive'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
precision: number BEHAVES { blurring confidence confidence_90 }
|
|
||||||
}`,
|
|
||||||
description: 'Blur behavior - confidence with level'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
session: string BEHAVES { ttl 1h }
|
|
||||||
}`,
|
|
||||||
description: 'TTL behavior - hours'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
token: string BEHAVES { ttl 24h }
|
|
||||||
}`,
|
|
||||||
description: 'TTL behavior - 24 hours'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
cache: string BEHAVES { ttl 7d }
|
|
||||||
}`,
|
|
||||||
description: 'TTL behavior - days'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const result = this.compiler.compile(input, `test-behavior-${Date.now()}`);
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.fail(`Behavior test: ${description}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testCaching() {
|
|
||||||
console.log('Testing Caching...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
role: string CACHE eager
|
|
||||||
}`,
|
|
||||||
description: 'Eager caching'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
score: number CACHE lazy
|
|
||||||
}`,
|
|
||||||
description: 'Lazy caching'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
balance: number BEHAVES { decaying down hourly } CACHE eager
|
|
||||||
}`,
|
|
||||||
description: 'Behavior with eager caching'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
reputation: number BEHAVES { blurring adaptive } CACHE lazy
|
|
||||||
}`,
|
|
||||||
description: 'Behavior with lazy caching'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const result = this.compiler.compile(input, `test-cache-${Date.now()}`);
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.fail(`Cache test: ${description}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testComplexDefinitions() {
|
|
||||||
console.log('Testing Complex Definitions...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
role: string
|
|
||||||
isActive: boolean
|
|
||||||
lastActive: timestamp BEHAVES {
|
|
||||||
decaying down hourly
|
|
||||||
} CACHE lazy
|
|
||||||
isSuspended: boolean
|
|
||||||
balance: number BEHAVES {
|
|
||||||
decaying down hourly
|
|
||||||
} CACHE eager
|
|
||||||
score: number BEHAVES {
|
|
||||||
blurring adaptive confidence_95
|
|
||||||
} CACHE lazy
|
|
||||||
session: string BEHAVES {
|
|
||||||
ttl 24h
|
|
||||||
} CACHE eager
|
|
||||||
}`,
|
|
||||||
description: 'Complex definition with multiple behaviors and caching'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition Group {
|
|
||||||
name: string
|
|
||||||
permissions: Permission[]
|
|
||||||
members: User[]
|
|
||||||
created: timestamp BEHAVES {
|
|
||||||
decaying stable monthly
|
|
||||||
} CACHE lazy
|
|
||||||
isPublic: boolean CACHE eager
|
|
||||||
}`,
|
|
||||||
description: 'Definition with arrays and mixed behaviors'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition Document {
|
|
||||||
level: string
|
|
||||||
owner: User
|
|
||||||
tags: string[]
|
|
||||||
content: string BEHAVES {
|
|
||||||
blurring fixed
|
|
||||||
} CACHE lazy
|
|
||||||
accessCount: number BEHAVES {
|
|
||||||
decaying up daily
|
|
||||||
} CACHE eager
|
|
||||||
expiresAt: timestamp BEHAVES {
|
|
||||||
ttl 30d
|
|
||||||
} CACHE eager
|
|
||||||
}`,
|
|
||||||
description: 'Definition with all behavior types'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const result = this.compiler.compile(input, `test-complex-def-${Date.now()}`);
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
this.assert(result.program.definitions.length > 0, 'Should have definitions');
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.fail(`Complex definition test: ${description}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testDefinitionErrors() {
|
|
||||||
console.log('Testing Definition Error Handling...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `definition User { role: string`,
|
|
||||||
description: 'Missing closing brace should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User { role: }`,
|
|
||||||
description: 'Missing field type should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User { : string }`,
|
|
||||||
description: 'Missing field name should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User { role: string BEHAVES { }`,
|
|
||||||
description: 'Incomplete behavior should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User { role: string CACHE }`,
|
|
||||||
description: 'Incomplete cache directive should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User { role: string BEHAVES { invalid } }`,
|
|
||||||
description: 'Invalid behavior should fail'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const result = this.compiler.compile(input, `test-def-error-${Date.now()}`);
|
|
||||||
this.assert(!result.success, `${description} should fail to parse`);
|
|
||||||
console.log(` ✓ ${description} (correctly failed)`);
|
|
||||||
} catch (error) {
|
|
||||||
// Expected to fail
|
|
||||||
console.log(` ✓ ${description} (correctly failed)`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(condition, message) {
|
|
||||||
if (!condition) {
|
|
||||||
throw new Error(`Assertion failed: ${message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fail(testName, error) {
|
|
||||||
console.log(` ✗ ${testName} failed: ${error.message}`);
|
|
||||||
this.testResults.push({
|
|
||||||
test: testName,
|
|
||||||
status: 'FAILED',
|
|
||||||
error: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getTestResults() {
|
|
||||||
const passed = this.testResults.filter(r => r.status === 'PASSED').length;
|
|
||||||
const failed = this.testResults.filter(r => r.status === 'FAILED').length;
|
|
||||||
const total = this.testResults.length;
|
|
||||||
|
|
||||||
return {
|
|
||||||
total: total,
|
|
||||||
passed: passed,
|
|
||||||
failed: failed,
|
|
||||||
success: failed === 0,
|
|
||||||
results: this.testResults
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function runDefinitionTests(arbiter) {
|
|
||||||
const test = new DefinitionTests();
|
|
||||||
test.setup(arbiter);
|
|
||||||
return test.runAllTests();
|
|
||||||
}
|
|
||||||
@@ -1,464 +0,0 @@
|
|||||||
/**
|
|
||||||
* Evidence Rule Tests
|
|
||||||
*
|
|
||||||
* Tests the evidence rule system of the Evidence DSL,
|
|
||||||
* including defeasible logic, pattern matching, fusion, and complex evidence composition.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { DSLCompiler } from '../DSLCompiler.js';
|
|
||||||
|
|
||||||
export class EvidenceTests {
|
|
||||||
constructor() {
|
|
||||||
this.arbiter = null;
|
|
||||||
this.compiler = null;
|
|
||||||
this.testResults = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
setup(arbiter) {
|
|
||||||
this.arbiter = arbiter;
|
|
||||||
this.compiler = new DSLCompiler(arbiter);
|
|
||||||
}
|
|
||||||
|
|
||||||
runAllTests() {
|
|
||||||
console.log('=== Evidence Rule Tests ===\n');
|
|
||||||
|
|
||||||
this.testBasicEvidence();
|
|
||||||
this.testDefeasibleLogic();
|
|
||||||
this.testPatternMatching();
|
|
||||||
this.testFusion();
|
|
||||||
this.testComplexEvidence();
|
|
||||||
this.testEvidenceErrors();
|
|
||||||
|
|
||||||
return this.getTestResults();
|
|
||||||
}
|
|
||||||
|
|
||||||
testBasicEvidence() {
|
|
||||||
console.log('Testing Basic Evidence...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `evidence canRead(user: User, doc: Document) {
|
|
||||||
hasRole(user, 'admin')
|
|
||||||
}`,
|
|
||||||
description: 'Simple evidence with function call'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canAccess(user: User, resource: Resource) {
|
|
||||||
user.isActive
|
|
||||||
}`,
|
|
||||||
description: 'Evidence with attribute access'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canModify(user: User, doc: Document) {
|
|
||||||
user.isActive
|
|
||||||
hasRole(user, 'admin')
|
|
||||||
}`,
|
|
||||||
description: 'Evidence with multiple conditions'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canDelete(user: User, doc: Document) {
|
|
||||||
owns(user, doc)
|
|
||||||
user.isActive
|
|
||||||
}`,
|
|
||||||
description: 'Evidence with ownership and status'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const result = this.compiler.compile(input, `test-basic-evidence-${Date.now()}`);
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
this.assert(result.program.evidence.length > 0, 'Should have evidence');
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.fail(`Basic evidence test: ${description}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testDefeasibleLogic() {
|
|
||||||
console.log('Testing Defeasible Logic...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `evidence canAccess(user: User, resource: Resource) {
|
|
||||||
ALWAYS user.isActive
|
|
||||||
}`,
|
|
||||||
description: 'ALWAYS rule - strict requirement'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canAccess(user: User, resource: Resource) {
|
|
||||||
WHEN hasRole(user, 'admin')
|
|
||||||
}`,
|
|
||||||
description: 'WHEN rule - defeasible condition'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canAccess(user: User, resource: Resource) {
|
|
||||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
|
||||||
}`,
|
|
||||||
description: 'WHEN/UNLESS rule - defeasible with defeater'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canAccess(user: User, resource: Resource) {
|
|
||||||
REQUIRES hasClearance(user, resource.level)
|
|
||||||
}`,
|
|
||||||
description: 'REQUIRES rule - inverse defeater'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canAccessCritical(user: User, resource: Resource) {
|
|
||||||
ALWAYS user.isActive
|
|
||||||
|
|
||||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
|
||||||
|
|
||||||
REQUIRES hasClearance(user, resource.level)
|
|
||||||
}`,
|
|
||||||
description: 'Complex defeasible logic with all rule types'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canAccessSensitive(user: User, doc: Document) {
|
|
||||||
ALWAYS user.isActive
|
|
||||||
|
|
||||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
|
||||||
|
|
||||||
REQUIRES hasClearance(user, doc.level)
|
|
||||||
|
|
||||||
fusion majority {
|
|
||||||
user.isTrusted
|
|
||||||
user.hasRecentActivity
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
description: 'Defeasible logic with fusion'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const result = this.compiler.compile(input, `test-defeasible-${Date.now()}`);
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.fail(`Defeasible logic test: ${description}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testPatternMatching() {
|
|
||||||
console.log('Testing Pattern Matching...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `evidence canRead(user: User, doc: Document) {
|
|
||||||
isMember(user, *group) {
|
|
||||||
canRead(group, doc)
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
description: 'Basic pattern matching with wildcard'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canRead(user: User, doc: Document) {
|
|
||||||
isMember(user, *group) {
|
|
||||||
canRead(group, doc)
|
|
||||||
} limit 5
|
|
||||||
}`,
|
|
||||||
description: 'Pattern matching with limit'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canRead(user: User, doc: Document) {
|
|
||||||
similar(doc, *similar) |similarity| {
|
|
||||||
canRead(user, similar)
|
|
||||||
} with similarity > 0.7
|
|
||||||
}`,
|
|
||||||
description: 'Pattern matching with binding and condition'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canRead(user: User, doc: Document) {
|
|
||||||
similar(doc, *similar) |similarity| {
|
|
||||||
canRead(user, similar)
|
|
||||||
} with similarity > 0.7 limit 5
|
|
||||||
}`,
|
|
||||||
description: 'Pattern matching with binding, condition, and limit'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canRead(user: User, doc: Document) {
|
|
||||||
isMember(user, *group) {
|
|
||||||
isMember(group, *parentGroup) {
|
|
||||||
canRead(parentGroup, doc)
|
|
||||||
} limit 2
|
|
||||||
} limit 3
|
|
||||||
}`,
|
|
||||||
description: 'Nested pattern matching'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canRead(user: User, doc: Document) {
|
|
||||||
isFriend(user, *friend) {
|
|
||||||
isMember(friend, *group) {
|
|
||||||
canRead(group, doc)
|
|
||||||
} limit 1
|
|
||||||
} limit 5
|
|
||||||
}`,
|
|
||||||
description: 'Multi-hop pattern matching'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const result = this.compiler.compile(input, `test-pattern-${Date.now()}`);
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.fail(`Pattern matching test: ${description}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testFusion() {
|
|
||||||
console.log('Testing Fusion...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `evidence canAccess(user: User, resource: Resource) {
|
|
||||||
fusion min {
|
|
||||||
hasClearance(user, resource.level)
|
|
||||||
user.isActive
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
description: 'Min fusion - all conditions must be true'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canAccess(user: User, resource: Resource) {
|
|
||||||
fusion max {
|
|
||||||
hasRole(user, 'admin')
|
|
||||||
hasRole(user, 'superuser')
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
description: 'Max fusion - any condition can be true'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canAccess(user: User, resource: Resource) {
|
|
||||||
fusion majority {
|
|
||||||
hasClearance(user, 'secret')
|
|
||||||
user.isTrusted
|
|
||||||
user.hasRecentActivity
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
description: 'Majority fusion - most conditions must be true'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canAccessCritical(user: User, resource: Resource) {
|
|
||||||
fusion min {
|
|
||||||
hasClearance(user, resource.level)
|
|
||||||
user.isActive
|
|
||||||
NOT user.isBlacklisted
|
|
||||||
}
|
|
||||||
|
|
||||||
fusion max {
|
|
||||||
hasRole(user, 'admin')
|
|
||||||
fusion majority {
|
|
||||||
hasClearance(user, 'secret')
|
|
||||||
user.isTrusted
|
|
||||||
user.lastActive within 1hr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
description: 'Nested fusion with different strategies'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canAccess(user: User, resource: Resource) {
|
|
||||||
fusion average {
|
|
||||||
user.reputation
|
|
||||||
user.activityScore
|
|
||||||
user.verificationLevel
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
description: 'Average fusion for numeric values'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const result = this.compiler.compile(input, `test-fusion-${Date.now()}`);
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.fail(`Fusion test: ${description}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testComplexEvidence() {
|
|
||||||
console.log('Testing Complex Evidence...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `evidence canRead(user: User, doc: Document) {
|
|
||||||
owns(user, doc)
|
|
||||||
|
|
||||||
isMember(user, *group) {
|
|
||||||
canRead(group, doc)
|
|
||||||
} limit 5
|
|
||||||
|
|
||||||
parentOf(user, *parent) {
|
|
||||||
canRead(parent, doc)
|
|
||||||
} limit 3
|
|
||||||
|
|
||||||
similar(doc, *similar) |similarity| {
|
|
||||||
canRead(user, similar)
|
|
||||||
} with similarity > 0.7 limit 5
|
|
||||||
|
|
||||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
|
||||||
}`,
|
|
||||||
description: 'Complex evidence with all features'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canAccessCritical(user: User, resource: Resource) {
|
|
||||||
ALWAYS user.isActive
|
|
||||||
|
|
||||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
|
||||||
|
|
||||||
REQUIRES hasClearance(user, resource.level)
|
|
||||||
|
|
||||||
fusion min {
|
|
||||||
hasClearance(user, resource.level)
|
|
||||||
user.isActive
|
|
||||||
NOT user.isBlacklisted
|
|
||||||
}
|
|
||||||
|
|
||||||
fusion max {
|
|
||||||
hasRole(user, 'admin')
|
|
||||||
fusion majority {
|
|
||||||
hasClearance(user, 'secret')
|
|
||||||
user.isTrusted
|
|
||||||
user.lastActive within 1hr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
description: 'Critical access with all rule types and fusion'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canModify(user: User, doc: Document) {
|
|
||||||
owns(user, doc)
|
|
||||||
|
|
||||||
isMember(user, *group) {
|
|
||||||
canModify(group, doc)
|
|
||||||
} limit 3
|
|
||||||
|
|
||||||
similar(doc, *similar) |similarity| {
|
|
||||||
canModify(user, similar)
|
|
||||||
similar.isEditable
|
|
||||||
} with similarity > 0.8 limit 2
|
|
||||||
|
|
||||||
fusion majority {
|
|
||||||
user.isTrusted
|
|
||||||
user.hasRecentActivity
|
|
||||||
doc.isPublic
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
description: 'Modification access with similarity and fusion'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const result = this.compiler.compile(input, `test-complex-evidence-${Date.now()}`);
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.fail(`Complex evidence test: ${description}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testEvidenceErrors() {
|
|
||||||
console.log('Testing Evidence Error Handling...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `evidence canRead(user: User, doc: Document) {
|
|
||||||
hasRole(user, 'admin'
|
|
||||||
}`,
|
|
||||||
description: 'Missing closing parenthesis should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canRead(user: User, doc: Document) {
|
|
||||||
WHEN hasRole(user, 'admin') UNLESS
|
|
||||||
}`,
|
|
||||||
description: 'Incomplete UNLESS condition should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canRead(user: User, doc: Document) {
|
|
||||||
fusion min {
|
|
||||||
hasRole(user, 'admin')
|
|
||||||
}`,
|
|
||||||
description: 'Incomplete fusion should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canRead(user: User, doc: Document) {
|
|
||||||
isMember(user, *group) {
|
|
||||||
canRead(group, doc)
|
|
||||||
} with
|
|
||||||
}`,
|
|
||||||
description: 'Incomplete with clause should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canRead(user: User, doc: Document) {
|
|
||||||
isMember(user, *group) {
|
|
||||||
canRead(group, doc)
|
|
||||||
} limit
|
|
||||||
}`,
|
|
||||||
description: 'Incomplete limit should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canRead(user: User, doc: Document) {
|
|
||||||
invalid syntax here
|
|
||||||
}`,
|
|
||||||
description: 'Invalid syntax should fail'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const result = this.compiler.compile(input, `test-evidence-error-${Date.now()}`);
|
|
||||||
this.assert(!result.success, `${description} should fail to parse`);
|
|
||||||
console.log(` ✓ ${description} (correctly failed)`);
|
|
||||||
} catch (error) {
|
|
||||||
// Expected to fail
|
|
||||||
console.log(` ✓ ${description} (correctly failed)`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(condition, message) {
|
|
||||||
if (!condition) {
|
|
||||||
throw new Error(`Assertion failed: ${message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fail(testName, error) {
|
|
||||||
console.log(` ✗ ${testName} failed: ${error.message}`);
|
|
||||||
this.testResults.push({
|
|
||||||
test: testName,
|
|
||||||
status: 'FAILED',
|
|
||||||
error: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getTestResults() {
|
|
||||||
const passed = this.testResults.filter(r => r.status === 'PASSED').length;
|
|
||||||
const failed = this.testResults.filter(r => r.status === 'FAILED').length;
|
|
||||||
const total = this.testResults.length;
|
|
||||||
|
|
||||||
return {
|
|
||||||
total: total,
|
|
||||||
passed: passed,
|
|
||||||
failed: failed,
|
|
||||||
success: failed === 0,
|
|
||||||
results: this.testResults
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function runEvidenceTests(arbiter) {
|
|
||||||
const test = new EvidenceTests();
|
|
||||||
test.setup(arbiter);
|
|
||||||
return test.runAllTests();
|
|
||||||
}
|
|
||||||
@@ -1,347 +0,0 @@
|
|||||||
/**
|
|
||||||
* Expression Parsing Tests
|
|
||||||
*
|
|
||||||
* Tests the expression parsing capabilities of the Evidence DSL,
|
|
||||||
* focusing on operator precedence, associativity, and complex expressions.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { DSLCompiler } from '../DSLCompiler.js';
|
|
||||||
|
|
||||||
export class ExpressionTests {
|
|
||||||
constructor() {
|
|
||||||
this.arbiter = null;
|
|
||||||
this.compiler = null;
|
|
||||||
this.testResults = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
setup(arbiter) {
|
|
||||||
this.arbiter = arbiter;
|
|
||||||
this.compiler = new DSLCompiler(arbiter);
|
|
||||||
}
|
|
||||||
|
|
||||||
runAllTests() {
|
|
||||||
console.log('=== Expression Parsing Tests ===\n');
|
|
||||||
|
|
||||||
this.testArithmeticPrecedence();
|
|
||||||
this.testLogicalPrecedence();
|
|
||||||
this.testComparisonOperators();
|
|
||||||
this.testTemporalExpressions();
|
|
||||||
this.testUnaryOperators();
|
|
||||||
this.testAttributeAccess();
|
|
||||||
this.testFunctionCalls();
|
|
||||||
this.testComplexExpressions();
|
|
||||||
this.testExpressionErrors();
|
|
||||||
|
|
||||||
return this.getTestResults();
|
|
||||||
}
|
|
||||||
|
|
||||||
testArithmeticPrecedence() {
|
|
||||||
console.log('Testing Arithmetic Operator Precedence...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: '1 + 2 * 3',
|
|
||||||
expected: 'Should evaluate as 1 + (2 * 3) = 7',
|
|
||||||
description: 'Multiplication before addition'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: '10 - 3 * 2',
|
|
||||||
expected: 'Should evaluate as 10 - (3 * 2) = 4',
|
|
||||||
description: 'Multiplication before subtraction'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: '8 / 2 * 4',
|
|
||||||
expected: 'Should evaluate as (8 / 2) * 4 = 16',
|
|
||||||
description: 'Left-associative division and multiplication'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: '2 + 3 * 4 - 5',
|
|
||||||
expected: 'Should evaluate as 2 + (3 * 4) - 5 = 9',
|
|
||||||
description: 'Mixed arithmetic with correct precedence'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: '(1 + 2) * 3',
|
|
||||||
expected: 'Should evaluate as (1 + 2) * 3 = 9',
|
|
||||||
description: 'Parentheses override precedence'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, expected, description }) => {
|
|
||||||
try {
|
|
||||||
const dsl = `evidence test() { ${input} }`;
|
|
||||||
const result = this.compiler.compile(dsl, `test-arithmetic-${Date.now()}`);
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.fail(`Arithmetic precedence test: ${description}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testLogicalPrecedence() {
|
|
||||||
console.log('Testing Logical Operator Precedence...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: 'true && false || true',
|
|
||||||
expected: 'Should evaluate as (true && false) || true = true',
|
|
||||||
description: 'AND before OR'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'false || true && false',
|
|
||||||
expected: 'Should evaluate as false || (true && false) = false',
|
|
||||||
description: 'AND before OR (alternative)'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'NOT true && false',
|
|
||||||
expected: 'Should evaluate as (NOT true) && false = false',
|
|
||||||
description: 'NOT before AND'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'true && NOT false',
|
|
||||||
expected: 'Should evaluate as true && (NOT false) = true',
|
|
||||||
description: 'NOT before AND (alternative)'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: '(true || false) && true',
|
|
||||||
expected: 'Should evaluate as (true || false) && true = true',
|
|
||||||
description: 'Parentheses override logical precedence'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, expected, description }) => {
|
|
||||||
try {
|
|
||||||
const dsl = `evidence test() { ${input} }`;
|
|
||||||
const result = this.compiler.compile(dsl, `test-logical-${Date.now()}`);
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.fail(`Logical precedence test: ${description}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testComparisonOperators() {
|
|
||||||
console.log('Testing Comparison Operators...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{ input: '1 == 1', description: 'Equality comparison' },
|
|
||||||
{ input: '1 != 2', description: 'Inequality comparison' },
|
|
||||||
{ input: '5 > 3', description: 'Greater than' },
|
|
||||||
{ input: '3 < 5', description: 'Less than' },
|
|
||||||
{ input: '4 >= 4', description: 'Greater than or equal' },
|
|
||||||
{ input: '4 <= 4', description: 'Less than or equal' },
|
|
||||||
{ input: '1 == 1 && 2 > 1', description: 'Comparison with logical operators' },
|
|
||||||
{ input: '1 + 2 == 3', description: 'Arithmetic in comparison' }
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const dsl = `evidence test() { ${input} }`;
|
|
||||||
const result = this.compiler.compile(dsl, `test-comparison-${Date.now()}`);
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.fail(`Comparison test: ${description}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testTemporalExpressions() {
|
|
||||||
console.log('Testing Temporal Expressions...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{ input: 'user.lastActive within 1h', description: 'Temporal within expression' },
|
|
||||||
{ input: 'user.lastLogin within 24h', description: 'Temporal within with hours' },
|
|
||||||
{ input: 'user.createdAt within 7d', description: 'Temporal within with days' },
|
|
||||||
{ input: 'user.lastActivity within 1h && user.isActive', description: 'Temporal with logical operators' }
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const dsl = `evidence test() { ${input} }`;
|
|
||||||
const result = this.compiler.compile(dsl, `test-temporal-${Date.now()}`);
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.fail(`Temporal test: ${description}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testUnaryOperators() {
|
|
||||||
console.log('Testing Unary Operators...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{ input: 'NOT true', description: 'NOT operator' },
|
|
||||||
{ input: '!false', description: 'Alternative NOT operator' },
|
|
||||||
{ input: 'NOT (true && false)', description: 'NOT with parenthesized expression' },
|
|
||||||
{ input: 'NOT user.isSuspended', description: 'NOT with attribute access' }
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const dsl = `evidence test() { ${input} }`;
|
|
||||||
const result = this.compiler.compile(dsl, `test-unary-${Date.now()}`);
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.fail(`Unary test: ${description}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testAttributeAccess() {
|
|
||||||
console.log('Testing Attribute Access...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{ input: 'user.role', description: 'Simple attribute access' },
|
|
||||||
{ input: 'user.profile.name', description: 'Nested attribute access' },
|
|
||||||
{ input: 'user.permissions[0]', description: 'Array access' },
|
|
||||||
{ input: 'user.role.permissions[0]', description: 'Nested attribute with array access' },
|
|
||||||
{ input: 'user.isActive && user.role == "admin"', description: 'Attribute access in logical expression' }
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const dsl = `evidence test() { ${input} }`;
|
|
||||||
const result = this.compiler.compile(dsl, `test-attribute-${Date.now()}`);
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.fail(`Attribute access test: ${description}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testFunctionCalls() {
|
|
||||||
console.log('Testing Function Calls...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{ input: 'hasRole(user, "admin")', description: 'Simple function call' },
|
|
||||||
{ input: 'isMember(user, group)', description: 'Function call with variables' },
|
|
||||||
{ input: 'hasPermission(user, resource, "read")', description: 'Function call with multiple arguments' },
|
|
||||||
{ input: 'hasRole(user, "admin") && isActive(user)', description: 'Multiple function calls' },
|
|
||||||
{ input: 'hasRole(user, user.role)', description: 'Function call with attribute access' }
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const dsl = `evidence test() { ${input} }`;
|
|
||||||
const result = this.compiler.compile(dsl, `test-function-${Date.now()}`);
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.fail(`Function call test: ${description}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testComplexExpressions() {
|
|
||||||
console.log('Testing Complex Expressions...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: 'user.isActive && (hasRole(user, "admin") || hasPermission(user, resource, "read"))',
|
|
||||||
description: 'Complex logical expression with function calls'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'user.balance > 100 && user.isActive && NOT user.isSuspended',
|
|
||||||
description: 'Multiple conditions with NOT'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'user.lastActive within 1h && (user.role == "admin" || user.hasEmergencyAccess)',
|
|
||||||
description: 'Temporal with logical conditions'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'hasRole(user, "admin") && user.isActive && NOT (user.isSuspended || user.isBlacklisted)',
|
|
||||||
description: 'Complex negation with multiple conditions'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'user.score > 0.8 && user.isTrusted && user.lastActivity within 24h',
|
|
||||||
description: 'Multiple attribute conditions with temporal'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const dsl = `evidence test() { ${input} }`;
|
|
||||||
const result = this.compiler.compile(dsl, `test-complex-${Date.now()}`);
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.fail(`Complex expression test: ${description}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testExpressionErrors() {
|
|
||||||
console.log('Testing Expression Error Handling...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: 'user.role ==',
|
|
||||||
description: 'Incomplete comparison should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'user.role &&',
|
|
||||||
description: 'Incomplete logical expression should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'hasRole(user,)',
|
|
||||||
description: 'Function call with missing argument should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'user.role == "admin" &&',
|
|
||||||
description: 'Incomplete logical expression should fail'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const dsl = `evidence test() { ${input} }`;
|
|
||||||
const result = this.compiler.compile(dsl, `test-error-${Date.now()}`);
|
|
||||||
this.assert(!result.success, `${description} should fail to parse`);
|
|
||||||
console.log(` ✓ ${description} (correctly failed)`);
|
|
||||||
} catch (error) {
|
|
||||||
// Expected to fail
|
|
||||||
console.log(` ✓ ${description} (correctly failed)`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(condition, message) {
|
|
||||||
if (!condition) {
|
|
||||||
throw new Error(`Assertion failed: ${message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fail(testName, error) {
|
|
||||||
console.log(` ✗ ${testName} failed: ${error.message}`);
|
|
||||||
this.testResults.push({
|
|
||||||
test: testName,
|
|
||||||
status: 'FAILED',
|
|
||||||
error: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getTestResults() {
|
|
||||||
const passed = this.testResults.filter(r => r.status === 'PASSED').length;
|
|
||||||
const failed = this.testResults.filter(r => r.status === 'FAILED').length;
|
|
||||||
const total = this.testResults.length;
|
|
||||||
|
|
||||||
return {
|
|
||||||
total: total,
|
|
||||||
passed: passed,
|
|
||||||
failed: failed,
|
|
||||||
success: failed === 0,
|
|
||||||
results: this.testResults
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function runExpressionTests(arbiter) {
|
|
||||||
const test = new ExpressionTests();
|
|
||||||
test.setup(arbiter);
|
|
||||||
return test.runAllTests();
|
|
||||||
}
|
|
||||||
@@ -1,330 +0,0 @@
|
|||||||
/**
|
|
||||||
* Fact Declaration Tests
|
|
||||||
*
|
|
||||||
* Tests the fact declaration system of the Evidence DSL,
|
|
||||||
* including properties, caching, limits, and parameter types.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { DSLCompiler } from '../DSLCompiler.js';
|
|
||||||
|
|
||||||
export class FactTests {
|
|
||||||
constructor() {
|
|
||||||
this.arbiter = null;
|
|
||||||
this.compiler = null;
|
|
||||||
this.testResults = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
setup(arbiter) {
|
|
||||||
this.arbiter = arbiter;
|
|
||||||
this.compiler = new DSLCompiler(arbiter);
|
|
||||||
}
|
|
||||||
|
|
||||||
runAllTests() {
|
|
||||||
console.log('=== Fact Declaration Tests ===\n');
|
|
||||||
|
|
||||||
this.testBasicFacts();
|
|
||||||
this.testFactProperties();
|
|
||||||
this.testFactCaching();
|
|
||||||
this.testFactLimits();
|
|
||||||
this.testParameterTypes();
|
|
||||||
this.testComplexFacts();
|
|
||||||
this.testFactErrors();
|
|
||||||
|
|
||||||
return this.getTestResults();
|
|
||||||
}
|
|
||||||
|
|
||||||
testBasicFacts() {
|
|
||||||
console.log('Testing Basic Facts...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `fact hasRole(user: User, role: string)`,
|
|
||||||
description: 'Simple fact with two parameters'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact isMember(user: User, group: Group)`,
|
|
||||||
description: 'Fact with custom types'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact owns(user: User, doc: Document)`,
|
|
||||||
description: 'Fact with multiple custom types'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact isActive(user: User)`,
|
|
||||||
description: 'Fact with single parameter'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact hasPermission(user: User, resource: Resource, action: string)`,
|
|
||||||
description: 'Fact with three parameters'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const result = this.compiler.compile(input, `test-basic-fact-${Date.now()}`);
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
this.assert(result.program.facts.length > 0, 'Should have facts');
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.fail(`Basic fact test: ${description}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testFactProperties() {
|
|
||||||
console.log('Testing Fact Properties...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `fact isMember(user: User, group: Group) transitive`,
|
|
||||||
description: 'Transitive fact'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact isFriend(user: User, friend: User) symmetrical`,
|
|
||||||
description: 'Symmetrical fact'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact isMember(user: User, group: Group) transitive symmetrical`,
|
|
||||||
description: 'Fact with multiple properties'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact isColleague(user: User, colleague: User) symmetrical`,
|
|
||||||
description: 'Symmetrical relationship fact'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact isParentOf(parent: User, child: User) transitive`,
|
|
||||||
description: 'Transitive hierarchical fact'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const result = this.compiler.compile(input, `test-fact-property-${Date.now()}`);
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.fail(`Fact property test: ${description}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testFactCaching() {
|
|
||||||
console.log('Testing Fact Caching...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `fact hasRole(user: User, role: string) CACHE eager`,
|
|
||||||
description: 'Eager cached fact'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact isMember(user: User, group: Group) CACHE lazy`,
|
|
||||||
description: 'Lazy cached fact'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact isMember(user: User, group: Group) transitive CACHE eager`,
|
|
||||||
description: 'Transitive fact with eager caching'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact isFriend(user: User, friend: User) symmetrical CACHE lazy`,
|
|
||||||
description: 'Symmetrical fact with lazy caching'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact hasPermission(user: User, resource: Resource, action: string) CACHE eager`,
|
|
||||||
description: 'Multi-parameter fact with eager caching'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const result = this.compiler.compile(input, `test-fact-cache-${Date.now()}`);
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.fail(`Fact cache test: ${description}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testFactLimits() {
|
|
||||||
console.log('Testing Fact Limits...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `fact isMember(user: User, group: Group) limit 10`,
|
|
||||||
description: 'Fact with simple limit'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact isFriend(user: User, friend: User) limit 100`,
|
|
||||||
description: 'Fact with higher limit'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact isMember(user: User, group: Group) transitive limit 5`,
|
|
||||||
description: 'Transitive fact with limit'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact isFriend(user: User, friend: User) symmetrical limit 50`,
|
|
||||||
description: 'Symmetrical fact with limit'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact isMember(user: User, group: Group) transitive CACHE lazy limit 3`,
|
|
||||||
description: 'Fact with properties, caching, and limit'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const result = this.compiler.compile(input, `test-fact-limit-${Date.now()}`);
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.fail(`Fact limit test: ${description}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testParameterTypes() {
|
|
||||||
console.log('Testing Parameter Types...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{ type: 'string', description: 'String parameter' },
|
|
||||||
{ type: 'number', description: 'Number parameter' },
|
|
||||||
{ type: 'boolean', description: 'Boolean parameter' },
|
|
||||||
{ type: 'timestamp', description: 'Timestamp parameter' },
|
|
||||||
{ type: 'User', description: 'Custom type parameter' },
|
|
||||||
{ type: 'Group', description: 'Another custom type parameter' },
|
|
||||||
{ type: 'Permission[]', description: 'Array type parameter' }
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ type, description }) => {
|
|
||||||
try {
|
|
||||||
const dsl = `fact test(param: ${type})`;
|
|
||||||
const result = this.compiler.compile(dsl, `test-param-type-${Date.now()}`);
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.fail(`Parameter type test: ${description}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testComplexFacts() {
|
|
||||||
console.log('Testing Complex Facts...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `fact hasRole(user: User, role: string) CACHE eager
|
|
||||||
fact isMember(user: User, group: Group) transitive CACHE lazy limit 10
|
|
||||||
fact isFriend(user: User, friend: User) symmetrical CACHE eager limit 100
|
|
||||||
fact owns(user: User, doc: Document) CACHE eager
|
|
||||||
fact isSuspended(user: User) CACHE lazy`,
|
|
||||||
description: 'Multiple facts with different configurations'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact hasPermission(user: User, resource: Resource, action: string) CACHE eager
|
|
||||||
fact isAdmin(user: User) CACHE eager
|
|
||||||
fact isOwner(user: User, resource: Resource) CACHE eager
|
|
||||||
fact hasAccess(user: User, resource: Resource, level: string) CACHE lazy`,
|
|
||||||
description: 'Permission-related facts'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact isMember(user: User, group: Group) transitive CACHE lazy limit 5
|
|
||||||
fact isFriend(user: User, friend: User) symmetrical CACHE eager limit 50
|
|
||||||
fact isColleague(user: User, colleague: User) symmetrical CACHE lazy limit 20
|
|
||||||
fact isParentOf(parent: User, child: User) transitive CACHE eager limit 3`,
|
|
||||||
description: 'Relationship facts with various properties'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const result = this.compiler.compile(input, `test-complex-facts-${Date.now()}`);
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
this.assert(result.program.facts.length > 0, 'Should have facts');
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.fail(`Complex facts test: ${description}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testFactErrors() {
|
|
||||||
console.log('Testing Fact Error Handling...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `fact hasRole(user: User, role: string`,
|
|
||||||
description: 'Missing closing parenthesis should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact hasRole(user: User, )`,
|
|
||||||
description: 'Missing parameter name should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact hasRole(user: User, role: )`,
|
|
||||||
description: 'Missing parameter type should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact hasRole(, role: string)`,
|
|
||||||
description: 'Missing parameter name should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact hasRole(user: User, role: string) CACHE`,
|
|
||||||
description: 'Incomplete cache directive should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact hasRole(user: User, role: string) limit`,
|
|
||||||
description: 'Incomplete limit should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact hasRole(user: User, role: string) invalid`,
|
|
||||||
description: 'Invalid property should fail'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const result = this.compiler.compile(input, `test-fact-error-${Date.now()}`);
|
|
||||||
this.assert(!result.success, `${description} should fail to parse`);
|
|
||||||
console.log(` ✓ ${description} (correctly failed)`);
|
|
||||||
} catch (error) {
|
|
||||||
// Expected to fail
|
|
||||||
console.log(` ✓ ${description} (correctly failed)`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(condition, message) {
|
|
||||||
if (!condition) {
|
|
||||||
throw new Error(`Assertion failed: ${message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fail(testName, error) {
|
|
||||||
console.log(` ✗ ${testName} failed: ${error.message}`);
|
|
||||||
this.testResults.push({
|
|
||||||
test: testName,
|
|
||||||
status: 'FAILED',
|
|
||||||
error: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getTestResults() {
|
|
||||||
const passed = this.testResults.filter(r => r.status === 'PASSED').length;
|
|
||||||
const failed = this.testResults.filter(r => r.status === 'FAILED').length;
|
|
||||||
const total = this.testResults.length;
|
|
||||||
|
|
||||||
return {
|
|
||||||
total: total,
|
|
||||||
passed: passed,
|
|
||||||
failed: failed,
|
|
||||||
success: failed === 0,
|
|
||||||
results: this.testResults
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function runFactTests(arbiter) {
|
|
||||||
const test = new FactTests();
|
|
||||||
test.setup(arbiter);
|
|
||||||
return test.runAllTests();
|
|
||||||
}
|
|
||||||
@@ -1,635 +0,0 @@
|
|||||||
/**
|
|
||||||
* Integration Tests
|
|
||||||
*
|
|
||||||
* Tests complex combinations of multiple language features,
|
|
||||||
* simulating real-world authorization scenarios.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { DSLCompiler } from '../DSLCompiler.js';
|
|
||||||
|
|
||||||
export class IntegrationTests {
|
|
||||||
constructor() {
|
|
||||||
this.arbiter = null;
|
|
||||||
this.compiler = null;
|
|
||||||
this.testResults = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
setup(arbiter) {
|
|
||||||
this.arbiter = arbiter;
|
|
||||||
this.compiler = new DSLCompiler(arbiter);
|
|
||||||
}
|
|
||||||
|
|
||||||
runAllTests() {
|
|
||||||
console.log('=== Integration Tests ===\n');
|
|
||||||
|
|
||||||
this.testCompleteAuthorizationSystem();
|
|
||||||
this.testMultiDomainSystem();
|
|
||||||
this.testHierarchicalAccess();
|
|
||||||
this.testSimilarityBasedAccess();
|
|
||||||
this.testTemporalAccess();
|
|
||||||
this.testComplexBehaviors();
|
|
||||||
this.testPerformanceScenarios();
|
|
||||||
|
|
||||||
return this.getTestResults();
|
|
||||||
}
|
|
||||||
|
|
||||||
testCompleteAuthorizationSystem() {
|
|
||||||
console.log('Testing Complete Authorization System...');
|
|
||||||
|
|
||||||
const completeSystem = `
|
|
||||||
// Type definitions with complex behaviors
|
|
||||||
definition User {
|
|
||||||
role: string
|
|
||||||
isActive: boolean
|
|
||||||
lastActive: timestamp BEHAVES {
|
|
||||||
decaying down hourly
|
|
||||||
} CACHE lazy
|
|
||||||
isSuspended: boolean
|
|
||||||
balance: number BEHAVES {
|
|
||||||
decaying down hourly
|
|
||||||
} CACHE eager
|
|
||||||
score: number BEHAVES {
|
|
||||||
blurring adaptive confidence_95
|
|
||||||
} CACHE lazy
|
|
||||||
session: string BEHAVES {
|
|
||||||
ttl 24h
|
|
||||||
} CACHE eager
|
|
||||||
clearance: string BEHAVES {
|
|
||||||
blurring fixed
|
|
||||||
} CACHE eager
|
|
||||||
reputation: number BEHAVES {
|
|
||||||
decaying up daily
|
|
||||||
} CACHE lazy
|
|
||||||
}
|
|
||||||
|
|
||||||
definition Group {
|
|
||||||
name: string
|
|
||||||
permissions: Permission[]
|
|
||||||
level: string
|
|
||||||
isPublic: boolean CACHE eager
|
|
||||||
created: timestamp BEHAVES {
|
|
||||||
decaying stable monthly
|
|
||||||
} CACHE lazy
|
|
||||||
}
|
|
||||||
|
|
||||||
definition Document {
|
|
||||||
level: string
|
|
||||||
owner: User
|
|
||||||
tags: string[]
|
|
||||||
content: string BEHAVES {
|
|
||||||
blurring fixed
|
|
||||||
} CACHE lazy
|
|
||||||
accessCount: number BEHAVES {
|
|
||||||
decaying up daily
|
|
||||||
} CACHE eager
|
|
||||||
expiresAt: timestamp BEHAVES {
|
|
||||||
ttl 30d
|
|
||||||
} CACHE eager
|
|
||||||
isPublic: boolean CACHE eager
|
|
||||||
}
|
|
||||||
|
|
||||||
definition Resource {
|
|
||||||
level: string
|
|
||||||
owner: User
|
|
||||||
permissions: Permission[]
|
|
||||||
isPublic: boolean CACHE eager
|
|
||||||
accessCount: number BEHAVES {
|
|
||||||
decaying up daily
|
|
||||||
} CACHE eager
|
|
||||||
}
|
|
||||||
|
|
||||||
// Facts with various properties and caching
|
|
||||||
fact hasRole(user: User, role: string) CACHE eager
|
|
||||||
fact isMember(user: User, group: Group) transitive CACHE lazy limit 10
|
|
||||||
fact isFriend(user: User, friend: User) symmetrical CACHE eager limit 100
|
|
||||||
fact owns(user: User, doc: Document) CACHE eager
|
|
||||||
fact isSuspended(user: User) CACHE lazy
|
|
||||||
fact hasPermission(user: User, resource: Resource, action: string) CACHE eager
|
|
||||||
fact isAdmin(user: User) CACHE eager
|
|
||||||
fact isOwner(user: User, resource: Resource) CACHE eager
|
|
||||||
fact hasAccess(user: User, resource: Resource, level: string) CACHE lazy
|
|
||||||
fact isColleague(user: User, colleague: User) symmetrical CACHE lazy limit 50
|
|
||||||
fact isParentOf(parent: User, child: User) transitive CACHE eager limit 3
|
|
||||||
|
|
||||||
// Evidence rules with complex logic
|
|
||||||
evidence canRead(user: User, doc: Document) {
|
|
||||||
owns(user, doc)
|
|
||||||
|
|
||||||
isMember(user, *group) {
|
|
||||||
canRead(group, doc)
|
|
||||||
} limit 5
|
|
||||||
|
|
||||||
parentOf(user, *parent) {
|
|
||||||
canRead(parent, doc)
|
|
||||||
} limit 3
|
|
||||||
|
|
||||||
similar(doc, *similar) |similarity| {
|
|
||||||
canRead(user, similar)
|
|
||||||
} with similarity > 0.7 limit 5
|
|
||||||
|
|
||||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
evidence canWrite(user: User, doc: Document) {
|
|
||||||
owns(user, doc)
|
|
||||||
|
|
||||||
isMember(user, *group) {
|
|
||||||
canWrite(group, doc)
|
|
||||||
} limit 3
|
|
||||||
|
|
||||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
|
||||||
|
|
||||||
REQUIRES user.isActive
|
|
||||||
}
|
|
||||||
|
|
||||||
evidence canDelete(user: User, doc: Document) {
|
|
||||||
owns(user, doc)
|
|
||||||
|
|
||||||
ALWAYS user.isActive
|
|
||||||
|
|
||||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
|
||||||
|
|
||||||
REQUIRES user.isActive
|
|
||||||
}
|
|
||||||
|
|
||||||
evidence canAccessCritical(user: User, resource: Resource) {
|
|
||||||
fusion min {
|
|
||||||
hasClearance(user, resource.level)
|
|
||||||
user.isActive
|
|
||||||
NOT user.isBlacklisted
|
|
||||||
}
|
|
||||||
|
|
||||||
fusion max {
|
|
||||||
hasRole(user, 'admin')
|
|
||||||
fusion majority {
|
|
||||||
hasClearance(user, 'secret')
|
|
||||||
user.isTrusted
|
|
||||||
user.lastActive within 1hr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
evidence canAccessSensitive(user: User, doc: Document) {
|
|
||||||
ALWAYS user.isActive
|
|
||||||
|
|
||||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
|
||||||
|
|
||||||
REQUIRES hasClearance(user, doc.level)
|
|
||||||
|
|
||||||
fusion majority {
|
|
||||||
user.isTrusted
|
|
||||||
user.hasRecentActivity
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Measures for computed values
|
|
||||||
measure userRole(user: User) {
|
|
||||||
user.role
|
|
||||||
} PROVIDES string
|
|
||||||
|
|
||||||
measure userPermissions(user: User) {
|
|
||||||
fusion max {
|
|
||||||
user.role.permissions
|
|
||||||
user.group.permissions
|
|
||||||
}
|
|
||||||
} PROVIDES Permission[]
|
|
||||||
|
|
||||||
measure effectiveClearance(user: User) {
|
|
||||||
fusion majority {
|
|
||||||
user.clearance
|
|
||||||
user.role.clearance
|
|
||||||
user.group.clearance
|
|
||||||
}
|
|
||||||
} PROVIDES string
|
|
||||||
|
|
||||||
measure userTrustScore(user: User) {
|
|
||||||
fusion average {
|
|
||||||
user.reputation
|
|
||||||
user.activityScore
|
|
||||||
user.verificationLevel
|
|
||||||
}
|
|
||||||
} PROVIDES number
|
|
||||||
|
|
||||||
measure userBalance(user: User) {
|
|
||||||
user.balance
|
|
||||||
} PROVIDES number
|
|
||||||
|
|
||||||
measure userScore(user: User) {
|
|
||||||
user.score
|
|
||||||
} PROVIDES number
|
|
||||||
`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = this.compiler.compile(completeSystem, 'test-complete-system');
|
|
||||||
this.assert(result.success, 'Complete authorization system should compile successfully');
|
|
||||||
this.assert(result.program.definitions.length >= 4, 'Should have multiple definitions');
|
|
||||||
this.assert(result.program.facts.length >= 10, 'Should have multiple facts');
|
|
||||||
this.assert(result.program.evidence.length >= 5, 'Should have multiple evidence rules');
|
|
||||||
this.assert(result.program.measures.length >= 6, 'Should have multiple measures');
|
|
||||||
console.log(' ✓ Complete authorization system');
|
|
||||||
} catch (error) {
|
|
||||||
this.fail('Complete authorization system test', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testMultiDomainSystem() {
|
|
||||||
console.log('Testing Multi-Domain System...');
|
|
||||||
|
|
||||||
const multiDomain = `
|
|
||||||
// Authentication domain
|
|
||||||
definition User {
|
|
||||||
role: string
|
|
||||||
isActive: boolean
|
|
||||||
lastActive: timestamp BEHAVES { decaying down hourly } CACHE lazy
|
|
||||||
session: string BEHAVES { ttl 24h } CACHE eager
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasRole(user: User, role: string) CACHE eager
|
|
||||||
fact isActive(user: User) CACHE eager
|
|
||||||
|
|
||||||
evidence canAuthenticate(user: User) {
|
|
||||||
user.isActive
|
|
||||||
user.session within 24h
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authorization domain
|
|
||||||
definition Resource {
|
|
||||||
level: string
|
|
||||||
owner: User
|
|
||||||
permissions: Permission[]
|
|
||||||
}
|
|
||||||
|
|
||||||
fact owns(user: User, resource: Resource) CACHE eager
|
|
||||||
fact hasPermission(user: User, resource: Resource, action: string) CACHE eager
|
|
||||||
|
|
||||||
evidence canAccess(user: User, resource: Resource) {
|
|
||||||
owns(user, resource)
|
|
||||||
hasPermission(user, resource, 'read')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finance domain
|
|
||||||
definition Account {
|
|
||||||
balance: number BEHAVES { decaying down hourly } CACHE eager
|
|
||||||
owner: User
|
|
||||||
isActive: boolean CACHE eager
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasAccount(user: User, account: Account) CACHE eager
|
|
||||||
fact hasBalance(user: User, amount: number) CACHE eager
|
|
||||||
|
|
||||||
evidence canWithdraw(user: User, amount: number) {
|
|
||||||
hasBalance(user, amount)
|
|
||||||
user.isActive
|
|
||||||
}
|
|
||||||
|
|
||||||
// Social domain
|
|
||||||
definition Group {
|
|
||||||
name: string
|
|
||||||
members: User[]
|
|
||||||
isPublic: boolean CACHE eager
|
|
||||||
}
|
|
||||||
|
|
||||||
fact isMember(user: User, group: Group) transitive CACHE lazy limit 10
|
|
||||||
fact isFriend(user: User, friend: User) symmetrical CACHE eager limit 100
|
|
||||||
|
|
||||||
evidence canAccessGroup(user: User, group: Group) {
|
|
||||||
isMember(user, group)
|
|
||||||
group.isPublic
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = this.compiler.compile(multiDomain, 'test-multi-domain');
|
|
||||||
this.assert(result.success, 'Multi-domain system should compile successfully');
|
|
||||||
this.assert(result.program.definitions.length >= 4, 'Should have multiple domain definitions');
|
|
||||||
this.assert(result.program.facts.length >= 8, 'Should have multiple domain facts');
|
|
||||||
this.assert(result.program.evidence.length >= 4, 'Should have multiple domain evidence rules');
|
|
||||||
console.log(' ✓ Multi-domain system');
|
|
||||||
} catch (error) {
|
|
||||||
this.fail('Multi-domain system test', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testHierarchicalAccess() {
|
|
||||||
console.log('Testing Hierarchical Access...');
|
|
||||||
|
|
||||||
const hierarchicalSystem = `
|
|
||||||
definition User {
|
|
||||||
role: string
|
|
||||||
level: string
|
|
||||||
isActive: boolean
|
|
||||||
clearance: string
|
|
||||||
}
|
|
||||||
|
|
||||||
definition Organization {
|
|
||||||
name: string
|
|
||||||
level: string
|
|
||||||
parent: Organization
|
|
||||||
}
|
|
||||||
|
|
||||||
fact isMember(user: User, org: Organization) transitive CACHE lazy limit 5
|
|
||||||
fact isParentOf(parent: Organization, child: Organization) transitive CACHE eager limit 3
|
|
||||||
fact hasRole(user: User, role: string) CACHE eager
|
|
||||||
fact hasClearance(user: User, level: string) CACHE eager
|
|
||||||
|
|
||||||
evidence canAccessOrg(user: User, org: Organization) {
|
|
||||||
isMember(user, org)
|
|
||||||
|
|
||||||
isParentOf(org, *parentOrg) {
|
|
||||||
canAccessOrg(user, parentOrg)
|
|
||||||
} limit 3
|
|
||||||
|
|
||||||
WHEN hasRole(user, 'admin') UNLESS user.isSuspended
|
|
||||||
}
|
|
||||||
|
|
||||||
evidence canAccessResource(user: User, resource: Resource) {
|
|
||||||
isMember(user, *org) {
|
|
||||||
canAccessResource(org, resource)
|
|
||||||
} limit 5
|
|
||||||
|
|
||||||
parentOf(user, *parent) {
|
|
||||||
canAccessResource(parent, resource)
|
|
||||||
} limit 2
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = this.compiler.compile(hierarchicalSystem, 'test-hierarchical');
|
|
||||||
this.assert(result.success, 'Hierarchical access system should compile successfully');
|
|
||||||
console.log(' ✓ Hierarchical access system');
|
|
||||||
} catch (error) {
|
|
||||||
this.fail('Hierarchical access test', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testSimilarityBasedAccess() {
|
|
||||||
console.log('Testing Similarity-Based Access...');
|
|
||||||
|
|
||||||
const similaritySystem = `
|
|
||||||
definition User {
|
|
||||||
profile: string
|
|
||||||
interests: string[]
|
|
||||||
isActive: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
definition Document {
|
|
||||||
content: string
|
|
||||||
tags: string[]
|
|
||||||
isPublic: boolean
|
|
||||||
owner: User
|
|
||||||
}
|
|
||||||
|
|
||||||
fact isFriend(user: User, friend: User) symmetrical CACHE eager limit 100
|
|
||||||
fact hasInterest(user: User, interest: string) CACHE lazy
|
|
||||||
fact hasTag(doc: Document, tag: string) CACHE lazy
|
|
||||||
|
|
||||||
evidence canRead(user: User, doc: Document) {
|
|
||||||
owns(user, doc)
|
|
||||||
|
|
||||||
similar(doc, *similar) |similarity| {
|
|
||||||
canRead(user, similar)
|
|
||||||
similar.isPublic
|
|
||||||
} with similarity > 0.7 limit 10
|
|
||||||
|
|
||||||
isFriend(user, *friend) {
|
|
||||||
canRead(friend, doc)
|
|
||||||
} limit 5
|
|
||||||
|
|
||||||
fusion majority {
|
|
||||||
user.interests
|
|
||||||
doc.tags
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
evidence canRecommend(user: User, doc: Document) {
|
|
||||||
similar(user, *similarUser) |similarity| {
|
|
||||||
canRead(similarUser, doc)
|
|
||||||
} with similarity > 0.8 limit 20
|
|
||||||
|
|
||||||
fusion average {
|
|
||||||
user.profile
|
|
||||||
doc.content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = this.compiler.compile(similaritySystem, 'test-similarity');
|
|
||||||
this.assert(result.success, 'Similarity-based access system should compile successfully');
|
|
||||||
console.log(' ✓ Similarity-based access system');
|
|
||||||
} catch (error) {
|
|
||||||
this.fail('Similarity-based access test', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testTemporalAccess() {
|
|
||||||
console.log('Testing Temporal Access...');
|
|
||||||
|
|
||||||
const temporalSystem = `
|
|
||||||
definition User {
|
|
||||||
lastActive: timestamp BEHAVES { decaying down hourly } CACHE lazy
|
|
||||||
session: string BEHAVES { ttl 24h } CACHE eager
|
|
||||||
isActive: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
definition Event {
|
|
||||||
startTime: timestamp
|
|
||||||
endTime: timestamp
|
|
||||||
isPublic: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasAccess(user: User, event: Event) CACHE lazy
|
|
||||||
fact isParticipant(user: User, event: Event) CACHE eager
|
|
||||||
|
|
||||||
evidence canAccessEvent(user: User, event: Event) {
|
|
||||||
user.lastActive within 1h
|
|
||||||
|
|
||||||
isParticipant(user, event)
|
|
||||||
|
|
||||||
WHEN event.isPublic UNLESS user.isSuspended
|
|
||||||
|
|
||||||
fusion min {
|
|
||||||
user.session within 24h
|
|
||||||
user.isActive
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
evidence canAccessHistorical(user: User, event: Event) {
|
|
||||||
user.lastActive within 24h
|
|
||||||
|
|
||||||
fusion majority {
|
|
||||||
user.isActive
|
|
||||||
user.session within 24h
|
|
||||||
event.isPublic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = this.compiler.compile(temporalSystem, 'test-temporal');
|
|
||||||
this.assert(result.success, 'Temporal access system should compile successfully');
|
|
||||||
console.log(' ✓ Temporal access system');
|
|
||||||
} catch (error) {
|
|
||||||
this.fail('Temporal access test', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testComplexBehaviors() {
|
|
||||||
console.log('Testing Complex Behaviors...');
|
|
||||||
|
|
||||||
const behaviorSystem = `
|
|
||||||
definition User {
|
|
||||||
balance: number BEHAVES { decaying down hourly } CACHE eager
|
|
||||||
score: number BEHAVES { blurring adaptive confidence_95 } CACHE lazy
|
|
||||||
session: string BEHAVES { ttl 24h } CACHE eager
|
|
||||||
reputation: number BEHAVES { decaying up daily } CACHE lazy
|
|
||||||
clearance: string BEHAVES { blurring fixed } CACHE eager
|
|
||||||
lastActive: timestamp BEHAVES { decaying down hourly } CACHE lazy
|
|
||||||
}
|
|
||||||
|
|
||||||
definition Document {
|
|
||||||
content: string BEHAVES { blurring fixed } CACHE lazy
|
|
||||||
accessCount: number BEHAVES { decaying up daily } CACHE eager
|
|
||||||
expiresAt: timestamp BEHAVES { ttl 30d } CACHE eager
|
|
||||||
isPublic: boolean CACHE eager
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasBalance(user: User, amount: number) CACHE eager
|
|
||||||
fact hasScore(user: User, score: number) CACHE lazy
|
|
||||||
fact hasReputation(user: User, reputation: number) CACHE lazy
|
|
||||||
|
|
||||||
evidence canAccessDocument(user: User, doc: Document) {
|
|
||||||
user.balance > 0
|
|
||||||
|
|
||||||
user.score > 0.5
|
|
||||||
|
|
||||||
user.reputation > 0.3
|
|
||||||
|
|
||||||
doc.accessCount < 1000
|
|
||||||
|
|
||||||
fusion majority {
|
|
||||||
user.isActive
|
|
||||||
user.lastActive within 1h
|
|
||||||
doc.isPublic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
measure userEffectiveScore(user: User) {
|
|
||||||
fusion average {
|
|
||||||
user.score
|
|
||||||
user.reputation
|
|
||||||
user.balance
|
|
||||||
}
|
|
||||||
} PROVIDES number
|
|
||||||
|
|
||||||
measure documentPopularity(doc: Document) {
|
|
||||||
doc.accessCount
|
|
||||||
} PROVIDES number
|
|
||||||
`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = this.compiler.compile(behaviorSystem, 'test-behaviors');
|
|
||||||
this.assert(result.success, 'Complex behaviors system should compile successfully');
|
|
||||||
console.log(' ✓ Complex behaviors system');
|
|
||||||
} catch (error) {
|
|
||||||
this.fail('Complex behaviors test', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testPerformanceScenarios() {
|
|
||||||
console.log('Testing Performance Scenarios...');
|
|
||||||
|
|
||||||
const performanceSystem = `
|
|
||||||
definition User {
|
|
||||||
role: string
|
|
||||||
isActive: boolean
|
|
||||||
permissions: Permission[] CACHE eager
|
|
||||||
}
|
|
||||||
|
|
||||||
definition Resource {
|
|
||||||
level: string
|
|
||||||
owner: User
|
|
||||||
permissions: Permission[] CACHE eager
|
|
||||||
}
|
|
||||||
|
|
||||||
// High-frequency facts with limits
|
|
||||||
fact isMember(user: User, group: Group) transitive CACHE lazy limit 5
|
|
||||||
fact isFriend(user: User, friend: User) symmetrical CACHE eager limit 50
|
|
||||||
fact hasPermission(user: User, resource: Resource, action: string) CACHE eager
|
|
||||||
fact owns(user: User, resource: Resource) CACHE eager
|
|
||||||
|
|
||||||
// Optimized evidence rules
|
|
||||||
evidence canAccess(user: User, resource: Resource) {
|
|
||||||
owns(user, resource)
|
|
||||||
|
|
||||||
isMember(user, *group) {
|
|
||||||
canAccess(group, resource)
|
|
||||||
} limit 3
|
|
||||||
|
|
||||||
WHEN hasPermission(user, resource, 'read')
|
|
||||||
}
|
|
||||||
|
|
||||||
evidence canModify(user: User, resource: Resource) {
|
|
||||||
owns(user, resource)
|
|
||||||
|
|
||||||
isMember(user, *group) {
|
|
||||||
canModify(group, resource)
|
|
||||||
} limit 2
|
|
||||||
|
|
||||||
WHEN hasPermission(user, resource, 'write')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Efficient measures
|
|
||||||
measure userEffectivePermissions(user: User) {
|
|
||||||
user.permissions
|
|
||||||
} PROVIDES Permission[]
|
|
||||||
|
|
||||||
measure resourceAccessLevel(resource: Resource) {
|
|
||||||
resource.level
|
|
||||||
} PROVIDES string
|
|
||||||
`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = this.compiler.compile(performanceSystem, 'test-performance');
|
|
||||||
this.assert(result.success, 'Performance scenarios should compile successfully');
|
|
||||||
console.log(' ✓ Performance scenarios');
|
|
||||||
} catch (error) {
|
|
||||||
this.fail('Performance scenarios test', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(condition, message) {
|
|
||||||
if (!condition) {
|
|
||||||
throw new Error(`Assertion failed: ${message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fail(testName, error) {
|
|
||||||
console.log(` ✗ ${testName} failed: ${error.message}`);
|
|
||||||
this.testResults.push({
|
|
||||||
test: testName,
|
|
||||||
status: 'FAILED',
|
|
||||||
error: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getTestResults() {
|
|
||||||
const passed = this.testResults.filter(r => r.status === 'PASSED').length;
|
|
||||||
const failed = this.testResults.filter(r => r.status === 'FAILED').length;
|
|
||||||
const total = this.testResults.length;
|
|
||||||
|
|
||||||
return {
|
|
||||||
total: total,
|
|
||||||
passed: passed,
|
|
||||||
failed: failed,
|
|
||||||
success: failed === 0,
|
|
||||||
results: this.testResults
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function runIntegrationTests(arbiter) {
|
|
||||||
const test = new IntegrationTests();
|
|
||||||
test.setup(arbiter);
|
|
||||||
return test.runAllTests();
|
|
||||||
}
|
|
||||||
@@ -1,393 +0,0 @@
|
|||||||
/**
|
|
||||||
* Measure Definition Tests
|
|
||||||
*
|
|
||||||
* Tests the measure system of the Evidence DSL,
|
|
||||||
* including aggregation, fusion, return types, and value computation.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { DSLCompiler } from '../DSLCompiler.js';
|
|
||||||
|
|
||||||
export class MeasureTests {
|
|
||||||
constructor() {
|
|
||||||
this.arbiter = null;
|
|
||||||
this.compiler = null;
|
|
||||||
this.testResults = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
setup(arbiter) {
|
|
||||||
this.arbiter = arbiter;
|
|
||||||
this.compiler = new DSLCompiler(arbiter);
|
|
||||||
}
|
|
||||||
|
|
||||||
runAllTests() {
|
|
||||||
console.log('=== Measure Definition Tests ===\n');
|
|
||||||
|
|
||||||
this.testBasicMeasures();
|
|
||||||
this.testMeasureReturnTypes();
|
|
||||||
this.testMeasureAggregation();
|
|
||||||
this.testMeasureFusion();
|
|
||||||
this.testComplexMeasures();
|
|
||||||
this.testMeasureErrors();
|
|
||||||
|
|
||||||
return this.getTestResults();
|
|
||||||
}
|
|
||||||
|
|
||||||
testBasicMeasures() {
|
|
||||||
console.log('Testing Basic Measures...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `measure userRole(user: User) {
|
|
||||||
user.role
|
|
||||||
} PROVIDES string`,
|
|
||||||
description: 'Simple measure with attribute access'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userBalance(user: User) {
|
|
||||||
user.balance
|
|
||||||
} PROVIDES number`,
|
|
||||||
description: 'Measure accessing numeric attribute'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure isUserActive(user: User) {
|
|
||||||
user.isActive
|
|
||||||
} PROVIDES boolean`,
|
|
||||||
description: 'Measure accessing boolean attribute'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userPermissions(user: User) {
|
|
||||||
user.permissions
|
|
||||||
} PROVIDES Permission[]`,
|
|
||||||
description: 'Measure accessing array attribute'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userScore(user: User) {
|
|
||||||
user.score
|
|
||||||
} PROVIDES number`,
|
|
||||||
description: 'Measure with behavior-inherited attribute'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const result = this.compiler.compile(input, `test-basic-measure-${Date.now()}`);
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
this.assert(result.program.measures.length > 0, 'Should have measures');
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.fail(`Basic measure test: ${description}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testMeasureReturnTypes() {
|
|
||||||
console.log('Testing Measure Return Types...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{ type: 'string', description: 'String return type' },
|
|
||||||
{ type: 'number', description: 'Number return type' },
|
|
||||||
{ type: 'boolean', description: 'Boolean return type' },
|
|
||||||
{ type: 'timestamp', description: 'Timestamp return type' },
|
|
||||||
{ type: 'Permission[]', description: 'Array return type' },
|
|
||||||
{ type: 'User', description: 'Custom type return' },
|
|
||||||
{ type: 'Group[]', description: 'Custom array return type' }
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ type, description }) => {
|
|
||||||
try {
|
|
||||||
const dsl = `measure test() { true } PROVIDES ${type}`;
|
|
||||||
const result = this.compiler.compile(dsl, `test-measure-return-${Date.now()}`);
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.fail(`Measure return type test: ${description}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testMeasureAggregation() {
|
|
||||||
console.log('Testing Measure Aggregation...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `measure userPermissions(user: User) {
|
|
||||||
aggregate {
|
|
||||||
user.role.permissions
|
|
||||||
user.group.permissions
|
|
||||||
} USING majority
|
|
||||||
} PROVIDES Permission[]`,
|
|
||||||
description: 'Aggregation with majority strategy'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userClearance(user: User) {
|
|
||||||
aggregate {
|
|
||||||
user.clearance
|
|
||||||
user.role.clearance
|
|
||||||
user.group.clearance
|
|
||||||
} USING max
|
|
||||||
} PROVIDES string`,
|
|
||||||
description: 'Aggregation with max strategy'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userScore(user: User) {
|
|
||||||
aggregate {
|
|
||||||
user.reputation
|
|
||||||
user.activityScore
|
|
||||||
user.verificationLevel
|
|
||||||
} USING average
|
|
||||||
} PROVIDES number`,
|
|
||||||
description: 'Aggregation with average strategy'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userTrust(user: User) {
|
|
||||||
aggregate {
|
|
||||||
user.reputation
|
|
||||||
user.activityScore
|
|
||||||
user.verificationLevel
|
|
||||||
user.socialProof
|
|
||||||
} USING min
|
|
||||||
} PROVIDES number`,
|
|
||||||
description: 'Aggregation with min strategy'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const result = this.compiler.compile(input, `test-measure-aggregation-${Date.now()}`);
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.fail(`Measure aggregation test: ${description}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testMeasureFusion() {
|
|
||||||
console.log('Testing Measure Fusion...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `measure effectiveClearance(user: User) {
|
|
||||||
fusion max {
|
|
||||||
user.clearance
|
|
||||||
user.role.clearance
|
|
||||||
user.group.clearance
|
|
||||||
}
|
|
||||||
} PROVIDES string`,
|
|
||||||
description: 'Fusion with max strategy'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userPermissions(user: User) {
|
|
||||||
fusion min {
|
|
||||||
user.role.permissions
|
|
||||||
user.group.permissions
|
|
||||||
}
|
|
||||||
} PROVIDES Permission[]`,
|
|
||||||
description: 'Fusion with min strategy'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userScore(user: User) {
|
|
||||||
fusion majority {
|
|
||||||
user.reputation
|
|
||||||
user.activityScore
|
|
||||||
user.verificationLevel
|
|
||||||
}
|
|
||||||
} PROVIDES number`,
|
|
||||||
description: 'Fusion with majority strategy'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userTrust(user: User) {
|
|
||||||
fusion average {
|
|
||||||
user.reputation
|
|
||||||
user.activityScore
|
|
||||||
user.verificationLevel
|
|
||||||
user.socialProof
|
|
||||||
}
|
|
||||||
} PROVIDES number`,
|
|
||||||
description: 'Fusion with average strategy'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const result = this.compiler.compile(input, `test-measure-fusion-${Date.now()}`);
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.fail(`Measure fusion test: ${description}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testComplexMeasures() {
|
|
||||||
console.log('Testing Complex Measures...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `measure userEffectivePermissions(user: User) {
|
|
||||||
aggregate {
|
|
||||||
user.role.permissions
|
|
||||||
user.group.permissions
|
|
||||||
user.directPermissions
|
|
||||||
} USING majority
|
|
||||||
} PROVIDES Permission[]`,
|
|
||||||
description: 'Complex aggregation with multiple sources'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userTrustScore(user: User) {
|
|
||||||
fusion average {
|
|
||||||
user.reputation
|
|
||||||
user.activityScore
|
|
||||||
user.verificationLevel
|
|
||||||
user.socialProof
|
|
||||||
user.peerRatings
|
|
||||||
}
|
|
||||||
} PROVIDES number`,
|
|
||||||
description: 'Complex fusion with multiple metrics'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userAccessLevel(user: User) {
|
|
||||||
fusion max {
|
|
||||||
user.clearance
|
|
||||||
user.role.clearance
|
|
||||||
user.group.clearance
|
|
||||||
user.temporaryClearance
|
|
||||||
}
|
|
||||||
} PROVIDES string`,
|
|
||||||
description: 'Complex clearance calculation'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userSimilarity(user1: User, user2: User) {
|
|
||||||
similar(user1, user2) |similarity| {
|
|
||||||
similarity
|
|
||||||
} with similarity > 0.5
|
|
||||||
} PROVIDES number`,
|
|
||||||
description: 'Similarity measure with pattern matching'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userEffectiveRole(user: User) {
|
|
||||||
fusion majority {
|
|
||||||
user.role
|
|
||||||
user.temporaryRole
|
|
||||||
user.actingRole
|
|
||||||
}
|
|
||||||
} PROVIDES string`,
|
|
||||||
description: 'Role determination with multiple sources'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const result = this.compiler.compile(input, `test-complex-measure-${Date.now()}`);
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.fail(`Complex measure test: ${description}`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testMeasureErrors() {
|
|
||||||
console.log('Testing Measure Error Handling...');
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `measure userRole(user: User) {
|
|
||||||
user.role
|
|
||||||
}`,
|
|
||||||
description: 'Missing PROVIDES clause should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userRole(user: User) {
|
|
||||||
user.role
|
|
||||||
} PROVIDES`,
|
|
||||||
description: 'Incomplete PROVIDES clause should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userRole(user: User) {
|
|
||||||
user.role
|
|
||||||
} PROVIDES string`,
|
|
||||||
description: 'Valid measure should succeed'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userPermissions(user: User) {
|
|
||||||
aggregate {
|
|
||||||
user.role.permissions
|
|
||||||
user.group.permissions
|
|
||||||
} USING
|
|
||||||
} PROVIDES Permission[]`,
|
|
||||||
description: 'Incomplete USING clause should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userScore(user: User) {
|
|
||||||
fusion {
|
|
||||||
user.reputation
|
|
||||||
user.activityScore
|
|
||||||
}
|
|
||||||
} PROVIDES number`,
|
|
||||||
description: 'Missing fusion strategy should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userRole(user: User) {
|
|
||||||
invalid syntax here
|
|
||||||
} PROVIDES string`,
|
|
||||||
description: 'Invalid syntax should fail'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const result = this.compiler.compile(input, `test-measure-error-${Date.now()}`);
|
|
||||||
if (description.includes('should succeed')) {
|
|
||||||
this.assert(result.success, `${description} should parse successfully`);
|
|
||||||
console.log(` ✓ ${description}`);
|
|
||||||
} else {
|
|
||||||
this.assert(!result.success, `${description} should fail to parse`);
|
|
||||||
console.log(` ✓ ${description} (correctly failed)`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (description.includes('should succeed')) {
|
|
||||||
this.fail(`Measure error test: ${description}`, error);
|
|
||||||
} else {
|
|
||||||
// Expected to fail
|
|
||||||
console.log(` ✓ ${description} (correctly failed)`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(condition, message) {
|
|
||||||
if (!condition) {
|
|
||||||
throw new Error(`Assertion failed: ${message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fail(testName, error) {
|
|
||||||
console.log(` ✗ ${testName} failed: ${error.message}`);
|
|
||||||
this.testResults.push({
|
|
||||||
test: testName,
|
|
||||||
status: 'FAILED',
|
|
||||||
error: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getTestResults() {
|
|
||||||
const passed = this.testResults.filter(r => r.status === 'PASSED').length;
|
|
||||||
const failed = this.testResults.filter(r => r.status === 'FAILED').length;
|
|
||||||
const total = this.testResults.length;
|
|
||||||
|
|
||||||
return {
|
|
||||||
total: total,
|
|
||||||
passed: passed,
|
|
||||||
failed: failed,
|
|
||||||
success: failed === 0,
|
|
||||||
results: this.testResults
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function runMeasureTests(arbiter) {
|
|
||||||
const test = new MeasureTests();
|
|
||||||
test.setup(arbiter);
|
|
||||||
return test.runAllTests();
|
|
||||||
}
|
|
||||||
@@ -1,249 +0,0 @@
|
|||||||
# Evidence DSL Test Suite
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This comprehensive test suite follows a **structural linguistic approach** to validate the Evidence DSL (Domain Specific Language) for authorization policies. The tests are organized incrementally from basic language primitives to complex integration scenarios.
|
|
||||||
|
|
||||||
## Test Structure
|
|
||||||
|
|
||||||
### 1. Structural Linguistic Tests (`StructuralLinguisticTests.js`)
|
|
||||||
**Level: Comprehensive**
|
|
||||||
- **Lexical Primitives**: Identifiers, literals, keywords, whitespace
|
|
||||||
- **Basic Expressions**: Arithmetic, logical, comparison, temporal
|
|
||||||
- **Type System**: Definitions, fields, behaviors, caching
|
|
||||||
- **Fact System**: Declarations, properties, caching, limits
|
|
||||||
- **Evidence System**: Rules, defeasible logic, pattern matching
|
|
||||||
- **Measure System**: Aggregation, fusion, return types
|
|
||||||
- **Complex Integration**: Multi-feature combinations
|
|
||||||
|
|
||||||
### 2. Expression Tests (`ExpressionTests.js`)
|
|
||||||
**Level: Focused**
|
|
||||||
- Arithmetic operator precedence
|
|
||||||
- Logical operator precedence
|
|
||||||
- Comparison operators
|
|
||||||
- Temporal expressions
|
|
||||||
- Unary operators
|
|
||||||
- Attribute access
|
|
||||||
- Function calls
|
|
||||||
- Complex expressions
|
|
||||||
- Error handling
|
|
||||||
|
|
||||||
### 3. Definition Tests (`DefinitionTests.js`)
|
|
||||||
**Level: Focused**
|
|
||||||
- Basic type definitions
|
|
||||||
- Field types (string, number, boolean, timestamp, custom)
|
|
||||||
- Array types
|
|
||||||
- Behaviors (decay, blur, TTL)
|
|
||||||
- Caching (eager, lazy)
|
|
||||||
- Complex definitions
|
|
||||||
- Error handling
|
|
||||||
|
|
||||||
### 4. Fact Tests (`FactTests.js`)
|
|
||||||
**Level: Focused**
|
|
||||||
- Basic fact declarations
|
|
||||||
- Fact properties (transitive, symmetrical)
|
|
||||||
- Fact caching
|
|
||||||
- Fact limits
|
|
||||||
- Parameter types
|
|
||||||
- Complex facts
|
|
||||||
- Error handling
|
|
||||||
|
|
||||||
### 5. Evidence Tests (`EvidenceTests.js`)
|
|
||||||
**Level: Focused**
|
|
||||||
- Basic evidence rules
|
|
||||||
- Defeasible logic (ALWAYS, WHEN/UNLESS, REQUIRES)
|
|
||||||
- Pattern matching with wildcards
|
|
||||||
- Fusion strategies (min, max, majority, average)
|
|
||||||
- Complex evidence composition
|
|
||||||
- Error handling
|
|
||||||
|
|
||||||
### 6. Measure Tests (`MeasureTests.js`)
|
|
||||||
**Level: Focused**
|
|
||||||
- Basic measure definitions
|
|
||||||
- Return types
|
|
||||||
- Aggregation with different strategies
|
|
||||||
- Fusion with different strategies
|
|
||||||
- Complex measures
|
|
||||||
- Error handling
|
|
||||||
|
|
||||||
### 7. Integration Tests (`IntegrationTests.js`)
|
|
||||||
**Level: Integration**
|
|
||||||
- Complete authorization systems
|
|
||||||
- Multi-domain systems
|
|
||||||
- Hierarchical access patterns
|
|
||||||
- Similarity-based access
|
|
||||||
- Temporal access patterns
|
|
||||||
- Complex behaviors
|
|
||||||
- Performance scenarios
|
|
||||||
|
|
||||||
## Test Runner (`TestRunner.js`)
|
|
||||||
|
|
||||||
The test runner orchestrates all test suites and provides:
|
|
||||||
- **Comprehensive Testing**: Run all test suites
|
|
||||||
- **Selective Testing**: Run specific test suites
|
|
||||||
- **Level-based Testing**: Run tests by complexity level
|
|
||||||
- **Detailed Reporting**: Summary and detailed results
|
|
||||||
- **Coverage Analysis**: Language feature coverage
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Run All Tests
|
|
||||||
```javascript
|
|
||||||
import { runAllTests } from './tests/TestRunner.js';
|
|
||||||
|
|
||||||
const results = runAllTests(arbiter);
|
|
||||||
console.log(`Tests: ${results.passed}/${results.total} passed`);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run Specific Test Suites
|
|
||||||
```javascript
|
|
||||||
import { runSpecificTests } from './tests/TestRunner.js';
|
|
||||||
|
|
||||||
const results = runSpecificTests(arbiter, [
|
|
||||||
'Expression Tests',
|
|
||||||
'Definition Tests'
|
|
||||||
]);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run Tests by Level
|
|
||||||
```javascript
|
|
||||||
import { runTestsByLevel } from './tests/TestRunner.js';
|
|
||||||
|
|
||||||
// Run only focused tests
|
|
||||||
const results = runTestsByLevel(arbiter, 'focused');
|
|
||||||
|
|
||||||
// Run only integration tests
|
|
||||||
const results = runTestsByLevel(arbiter, 'integration');
|
|
||||||
```
|
|
||||||
|
|
||||||
## Language Feature Coverage
|
|
||||||
|
|
||||||
### ✅ Lexical Primitives
|
|
||||||
- Identifiers (simple, with underscores, with numbers)
|
|
||||||
- Literals (string, number, boolean, duration)
|
|
||||||
- Keywords (reserved words)
|
|
||||||
- Whitespace and comments
|
|
||||||
|
|
||||||
### ✅ Expression System
|
|
||||||
- Arithmetic operators (+, -, *, /) with precedence
|
|
||||||
- Logical operators (&&, ||, NOT) with precedence
|
|
||||||
- Comparison operators (==, !=, >, <, >=, <=)
|
|
||||||
- Temporal expressions (within)
|
|
||||||
- Unary operators (NOT, !)
|
|
||||||
- Attribute access (object.attribute)
|
|
||||||
- Function calls (predicate(args))
|
|
||||||
|
|
||||||
### ✅ Type System
|
|
||||||
- Type definitions with fields
|
|
||||||
- Field types (string, number, boolean, timestamp, custom)
|
|
||||||
- Array types (Type[])
|
|
||||||
- Behaviors (decay, blur, TTL)
|
|
||||||
- Caching directives (eager, lazy)
|
|
||||||
|
|
||||||
### ✅ Fact System
|
|
||||||
- Fact declarations with parameters
|
|
||||||
- Fact properties (transitive, symmetrical)
|
|
||||||
- Caching directives
|
|
||||||
- Limits for performance
|
|
||||||
- Parameter types
|
|
||||||
|
|
||||||
### ✅ Evidence System
|
|
||||||
- Basic evidence rules
|
|
||||||
- Defeasible logic (ALWAYS, WHEN/UNLESS, REQUIRES)
|
|
||||||
- Pattern matching with wildcards (*)
|
|
||||||
- Binding clauses (|variable|)
|
|
||||||
- With clauses (with condition)
|
|
||||||
- Limits for pattern matching
|
|
||||||
- Fusion strategies (min, max, majority, average)
|
|
||||||
|
|
||||||
### ✅ Measure System
|
|
||||||
- Measure definitions
|
|
||||||
- Return type specifications (PROVIDES)
|
|
||||||
- Aggregation with strategies (USING)
|
|
||||||
- Fusion with strategies
|
|
||||||
- Complex value computation
|
|
||||||
|
|
||||||
### ✅ Integration Features
|
|
||||||
- Multi-domain systems
|
|
||||||
- Hierarchical access patterns
|
|
||||||
- Similarity-based access
|
|
||||||
- Temporal access patterns
|
|
||||||
- Complex behavior combinations
|
|
||||||
- Performance optimization scenarios
|
|
||||||
|
|
||||||
## Test Philosophy
|
|
||||||
|
|
||||||
### Structural Linguistic Approach
|
|
||||||
The tests follow a structural linguistic methodology:
|
|
||||||
|
|
||||||
1. **Phonological Level**: Basic lexical elements (identifiers, literals)
|
|
||||||
2. **Morphological Level**: Word formation (operators, keywords)
|
|
||||||
3. **Syntactic Level**: Grammar rules (expressions, statements)
|
|
||||||
4. **Semantic Level**: Meaning (types, behaviors, logic)
|
|
||||||
5. **Pragmatic Level**: Usage (integration, real-world scenarios)
|
|
||||||
|
|
||||||
### Incremental Complexity
|
|
||||||
Tests progress from simple to complex:
|
|
||||||
- **Level 1**: Lexical primitives
|
|
||||||
- **Level 2**: Basic expressions
|
|
||||||
- **Level 3**: Type system
|
|
||||||
- **Level 4**: Fact system
|
|
||||||
- **Level 5**: Evidence system
|
|
||||||
- **Level 6**: Measure system
|
|
||||||
- **Level 7**: Complex integration
|
|
||||||
|
|
||||||
### Comprehensive Coverage
|
|
||||||
Each language feature is tested for:
|
|
||||||
- **Valid cases**: Correct syntax and semantics
|
|
||||||
- **Invalid cases**: Error handling and recovery
|
|
||||||
- **Edge cases**: Boundary conditions
|
|
||||||
- **Integration**: Multi-feature combinations
|
|
||||||
|
|
||||||
## Running Tests
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
- Node.js environment
|
|
||||||
- Arbiter instance for testing
|
|
||||||
- All dependencies installed
|
|
||||||
|
|
||||||
### Basic Usage
|
|
||||||
```bash
|
|
||||||
# Run all tests
|
|
||||||
npm test
|
|
||||||
|
|
||||||
# Run specific test file
|
|
||||||
node src/ast/tests/StructuralLinguisticTests.js
|
|
||||||
|
|
||||||
# Run with specific arbiter
|
|
||||||
node -e "
|
|
||||||
import { runAllTests } from './src/ast/tests/TestRunner.js';
|
|
||||||
const results = runAllTests(arbiter);
|
|
||||||
console.log(results);
|
|
||||||
"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test Output
|
|
||||||
The test runner provides:
|
|
||||||
- **Progress indicators**: Real-time test execution
|
|
||||||
- **Detailed results**: Pass/fail status for each test
|
|
||||||
- **Error reporting**: Specific error messages for failures
|
|
||||||
- **Performance metrics**: Execution time for each suite
|
|
||||||
- **Coverage analysis**: Language feature coverage
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
When adding new tests:
|
|
||||||
1. Follow the structural linguistic approach
|
|
||||||
2. Test both valid and invalid cases
|
|
||||||
3. Include error handling tests
|
|
||||||
4. Document test purpose and expected behavior
|
|
||||||
5. Maintain incremental complexity
|
|
||||||
6. Update coverage documentation
|
|
||||||
|
|
||||||
## Test Maintenance
|
|
||||||
|
|
||||||
- **Regular Updates**: Keep tests current with language changes
|
|
||||||
- **Performance Monitoring**: Track test execution time
|
|
||||||
- **Coverage Analysis**: Ensure comprehensive feature coverage
|
|
||||||
- **Error Handling**: Validate error messages and recovery
|
|
||||||
- **Integration Testing**: Test real-world scenarios
|
|
||||||
@@ -1,413 +0,0 @@
|
|||||||
/**
|
|
||||||
* Comprehensive Test Runner for Evidence DSL
|
|
||||||
*
|
|
||||||
* Orchestrates all test suites in a structural linguistic approach,
|
|
||||||
* from basic primitives to complex integration scenarios.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { runStructuralLinguisticTests } from './StructuralLinguisticTests.js';
|
|
||||||
import { runExpressionTests } from './ExpressionTests.js';
|
|
||||||
import { runDefinitionTests } from './DefinitionTests.js';
|
|
||||||
import { runFactTests } from './FactTests.js';
|
|
||||||
import { runEvidenceTests } from './EvidenceTests.js';
|
|
||||||
import { runMeasureTests } from './MeasureTests.js';
|
|
||||||
import { runIntegrationTests } from './IntegrationTests.js';
|
|
||||||
|
|
||||||
export class TestRunner {
|
|
||||||
constructor() {
|
|
||||||
this.arbiter = null;
|
|
||||||
this.testSuites = [];
|
|
||||||
this.results = {
|
|
||||||
total: 0,
|
|
||||||
passed: 0,
|
|
||||||
failed: 0,
|
|
||||||
success: false,
|
|
||||||
suites: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
setup(arbiter) {
|
|
||||||
this.arbiter = arbiter;
|
|
||||||
this.testSuites = [
|
|
||||||
{
|
|
||||||
name: 'Structural Linguistic Tests',
|
|
||||||
description: 'Comprehensive tests from basic primitives to complex features',
|
|
||||||
runner: runStructuralLinguisticTests,
|
|
||||||
level: 'comprehensive'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Expression Tests',
|
|
||||||
description: 'Expression parsing, operator precedence, and complex expressions',
|
|
||||||
runner: runExpressionTests,
|
|
||||||
level: 'focused'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Definition Tests',
|
|
||||||
description: 'Type definitions, fields, behaviors, and caching',
|
|
||||||
runner: runDefinitionTests,
|
|
||||||
level: 'focused'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Fact Tests',
|
|
||||||
description: 'Fact declarations, properties, and caching',
|
|
||||||
runner: runFactTests,
|
|
||||||
level: 'focused'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Evidence Tests',
|
|
||||||
description: 'Evidence rules, defeasible logic, and pattern matching',
|
|
||||||
runner: runEvidenceTests,
|
|
||||||
level: 'focused'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Measure Tests',
|
|
||||||
description: 'Measure definitions, aggregation, and fusion',
|
|
||||||
runner: runMeasureTests,
|
|
||||||
level: 'focused'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Integration Tests',
|
|
||||||
description: 'Complex multi-feature integration scenarios',
|
|
||||||
runner: runIntegrationTests,
|
|
||||||
level: 'integration'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run all test suites
|
|
||||||
* @returns {Object} Comprehensive test results
|
|
||||||
*/
|
|
||||||
runAllTests() {
|
|
||||||
console.log('='.repeat(80));
|
|
||||||
console.log('EVIDENCE DSL COMPREHENSIVE TEST SUITE');
|
|
||||||
console.log('='.repeat(80));
|
|
||||||
console.log('Structural Linguistic Approach: Testing from primitives to integration\n');
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
for (const suite of this.testSuites) {
|
|
||||||
console.log(`\n${'='.repeat(60)}`);
|
|
||||||
console.log(`Running: ${suite.name}`);
|
|
||||||
console.log(`Level: ${suite.level.toUpperCase()}`);
|
|
||||||
console.log(`Description: ${suite.description}`);
|
|
||||||
console.log(`${'='.repeat(60)}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const suiteStartTime = Date.now();
|
|
||||||
const suiteResults = suite.runner(this.arbiter);
|
|
||||||
const suiteEndTime = Date.now();
|
|
||||||
const suiteDuration = suiteEndTime - suiteStartTime;
|
|
||||||
|
|
||||||
this.results.suites.push({
|
|
||||||
name: suite.name,
|
|
||||||
level: suite.level,
|
|
||||||
duration: suiteDuration,
|
|
||||||
results: suiteResults
|
|
||||||
});
|
|
||||||
|
|
||||||
this.results.total += suiteResults.total;
|
|
||||||
this.results.passed += suiteResults.passed;
|
|
||||||
this.results.failed += suiteResults.failed;
|
|
||||||
|
|
||||||
console.log(`\n${suite.name} completed in ${suiteDuration}ms`);
|
|
||||||
console.log(`Results: ${suiteResults.passed}/${suiteResults.total} passed, ${suiteResults.failed} failed`);
|
|
||||||
|
|
||||||
if (suiteResults.success) {
|
|
||||||
console.log(`✓ ${suite.name} PASSED`);
|
|
||||||
} else {
|
|
||||||
console.log(`✗ ${suite.name} FAILED`);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`\n✗ ${suite.name} ERROR: ${error.message}`);
|
|
||||||
this.results.suites.push({
|
|
||||||
name: suite.name,
|
|
||||||
level: suite.level,
|
|
||||||
duration: 0,
|
|
||||||
results: {
|
|
||||||
total: 0,
|
|
||||||
passed: 0,
|
|
||||||
failed: 1,
|
|
||||||
success: false,
|
|
||||||
error: error.message
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.results.failed += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const endTime = Date.now();
|
|
||||||
const totalDuration = endTime - startTime;
|
|
||||||
|
|
||||||
this.results.success = this.results.failed === 0;
|
|
||||||
|
|
||||||
this.printSummary(totalDuration);
|
|
||||||
this.printDetailedResults();
|
|
||||||
|
|
||||||
return this.results;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run specific test suites
|
|
||||||
* @param {string[]} suiteNames - Names of test suites to run
|
|
||||||
* @returns {Object} Test results for specified suites
|
|
||||||
*/
|
|
||||||
runSpecificTests(suiteNames) {
|
|
||||||
console.log('='.repeat(80));
|
|
||||||
console.log('EVIDENCE DSL SELECTIVE TEST SUITE');
|
|
||||||
console.log('='.repeat(80));
|
|
||||||
console.log(`Running: ${suiteNames.join(', ')}\n`);
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
const selectedSuites = this.testSuites.filter(suite => suiteNames.includes(suite.name));
|
|
||||||
|
|
||||||
for (const suite of selectedSuites) {
|
|
||||||
console.log(`\n${'='.repeat(60)}`);
|
|
||||||
console.log(`Running: ${suite.name}`);
|
|
||||||
console.log(`${'='.repeat(60)}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const suiteStartTime = Date.now();
|
|
||||||
const suiteResults = suite.runner(this.arbiter);
|
|
||||||
const suiteEndTime = Date.now();
|
|
||||||
const suiteDuration = suiteEndTime - suiteStartTime;
|
|
||||||
|
|
||||||
this.results.suites.push({
|
|
||||||
name: suite.name,
|
|
||||||
level: suite.level,
|
|
||||||
duration: suiteDuration,
|
|
||||||
results: suiteResults
|
|
||||||
});
|
|
||||||
|
|
||||||
this.results.total += suiteResults.total;
|
|
||||||
this.results.passed += suiteResults.passed;
|
|
||||||
this.results.failed += suiteResults.failed;
|
|
||||||
|
|
||||||
console.log(`\n${suite.name} completed in ${suiteDuration}ms`);
|
|
||||||
console.log(`Results: ${suiteResults.passed}/${suiteResults.total} passed, ${suiteResults.failed} failed`);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`\n✗ ${suite.name} ERROR: ${error.message}`);
|
|
||||||
this.results.failed += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const endTime = Date.now();
|
|
||||||
const totalDuration = endTime - startTime;
|
|
||||||
|
|
||||||
this.results.success = this.results.failed === 0;
|
|
||||||
this.printSummary(totalDuration);
|
|
||||||
|
|
||||||
return this.results;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run tests by level
|
|
||||||
* @param {string} level - Test level to run ('comprehensive', 'focused', 'integration')
|
|
||||||
* @returns {Object} Test results for specified level
|
|
||||||
*/
|
|
||||||
runTestsByLevel(level) {
|
|
||||||
const levelSuites = this.testSuites.filter(suite => suite.level === level);
|
|
||||||
const suiteNames = levelSuites.map(suite => suite.name);
|
|
||||||
return this.runSpecificTests(suiteNames);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Print test summary
|
|
||||||
* @param {number} totalDuration - Total test duration in milliseconds
|
|
||||||
*/
|
|
||||||
printSummary(totalDuration) {
|
|
||||||
console.log('\n' + '='.repeat(80));
|
|
||||||
console.log('TEST SUMMARY');
|
|
||||||
console.log('='.repeat(80));
|
|
||||||
console.log(`Total Tests: ${this.results.total}`);
|
|
||||||
console.log(`Passed: ${this.results.passed}`);
|
|
||||||
console.log(`Failed: ${this.results.failed}`);
|
|
||||||
console.log(`Success Rate: ${((this.results.passed / this.results.total) * 100).toFixed(2)}%`);
|
|
||||||
console.log(`Total Duration: ${totalDuration}ms`);
|
|
||||||
console.log(`Status: ${this.results.success ? '✓ ALL TESTS PASSED' : '✗ SOME TESTS FAILED'}`);
|
|
||||||
console.log('='.repeat(80));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Print detailed results for each test suite
|
|
||||||
*/
|
|
||||||
printDetailedResults() {
|
|
||||||
console.log('\n' + '='.repeat(80));
|
|
||||||
console.log('DETAILED RESULTS');
|
|
||||||
console.log('='.repeat(80));
|
|
||||||
|
|
||||||
this.results.suites.forEach(suite => {
|
|
||||||
console.log(`\n${suite.name} (${suite.level}):`);
|
|
||||||
console.log(` Duration: ${suite.duration}ms`);
|
|
||||||
console.log(` Total: ${suite.results.total}`);
|
|
||||||
console.log(` Passed: ${suite.results.passed}`);
|
|
||||||
console.log(` Failed: ${suite.results.failed}`);
|
|
||||||
console.log(` Success: ${suite.results.success ? '✓' : '✗'}`);
|
|
||||||
|
|
||||||
if (suite.results.error) {
|
|
||||||
console.log(` Error: ${suite.results.error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (suite.results.results && suite.results.results.length > 0) {
|
|
||||||
console.log(' Individual Results:');
|
|
||||||
suite.results.results.forEach(result => {
|
|
||||||
const status = result.status === 'PASSED' ? '✓' : '✗';
|
|
||||||
console.log(` ${status} ${result.test}`);
|
|
||||||
if (result.error) {
|
|
||||||
console.log(` Error: ${result.error}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get test coverage report
|
|
||||||
* @returns {Object} Coverage report
|
|
||||||
*/
|
|
||||||
getCoverageReport() {
|
|
||||||
const coverage = {
|
|
||||||
lexical: {
|
|
||||||
identifiers: 'tested',
|
|
||||||
literals: 'tested',
|
|
||||||
keywords: 'tested',
|
|
||||||
whitespace: 'tested'
|
|
||||||
},
|
|
||||||
expressions: {
|
|
||||||
arithmetic: 'tested',
|
|
||||||
logical: 'tested',
|
|
||||||
comparison: 'tested',
|
|
||||||
temporal: 'tested',
|
|
||||||
unary: 'tested',
|
|
||||||
attributeAccess: 'tested',
|
|
||||||
functionCalls: 'tested'
|
|
||||||
},
|
|
||||||
types: {
|
|
||||||
definitions: 'tested',
|
|
||||||
fields: 'tested',
|
|
||||||
behaviors: 'tested',
|
|
||||||
caching: 'tested',
|
|
||||||
arrays: 'tested'
|
|
||||||
},
|
|
||||||
facts: {
|
|
||||||
declarations: 'tested',
|
|
||||||
properties: 'tested',
|
|
||||||
caching: 'tested',
|
|
||||||
limits: 'tested',
|
|
||||||
parameters: 'tested'
|
|
||||||
},
|
|
||||||
evidence: {
|
|
||||||
basic: 'tested',
|
|
||||||
defeasibleLogic: 'tested',
|
|
||||||
patternMatching: 'tested',
|
|
||||||
fusion: 'tested',
|
|
||||||
complex: 'tested'
|
|
||||||
},
|
|
||||||
measures: {
|
|
||||||
basic: 'tested',
|
|
||||||
aggregation: 'tested',
|
|
||||||
fusion: 'tested',
|
|
||||||
returnTypes: 'tested',
|
|
||||||
complex: 'tested'
|
|
||||||
},
|
|
||||||
integration: {
|
|
||||||
completeSystems: 'tested',
|
|
||||||
multiDomain: 'tested',
|
|
||||||
hierarchical: 'tested',
|
|
||||||
similarity: 'tested',
|
|
||||||
temporal: 'tested',
|
|
||||||
behaviors: 'tested',
|
|
||||||
performance: 'tested'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return coverage;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get language feature coverage
|
|
||||||
* @returns {Object} Feature coverage report
|
|
||||||
*/
|
|
||||||
getFeatureCoverage() {
|
|
||||||
return {
|
|
||||||
languagePrimitives: {
|
|
||||||
identifiers: '✓',
|
|
||||||
literals: '✓',
|
|
||||||
keywords: '✓',
|
|
||||||
operators: '✓',
|
|
||||||
expressions: '✓'
|
|
||||||
},
|
|
||||||
typeSystem: {
|
|
||||||
definitions: '✓',
|
|
||||||
fields: '✓',
|
|
||||||
behaviors: '✓',
|
|
||||||
caching: '✓',
|
|
||||||
arrays: '✓'
|
|
||||||
},
|
|
||||||
factSystem: {
|
|
||||||
declarations: '✓',
|
|
||||||
properties: '✓',
|
|
||||||
caching: '✓',
|
|
||||||
limits: '✓',
|
|
||||||
parameters: '✓'
|
|
||||||
},
|
|
||||||
evidenceSystem: {
|
|
||||||
rules: '✓',
|
|
||||||
defeasibleLogic: '✓',
|
|
||||||
patternMatching: '✓',
|
|
||||||
fusion: '✓',
|
|
||||||
complex: '✓'
|
|
||||||
},
|
|
||||||
measureSystem: {
|
|
||||||
definitions: '✓',
|
|
||||||
aggregation: '✓',
|
|
||||||
fusion: '✓',
|
|
||||||
returnTypes: '✓',
|
|
||||||
complex: '✓'
|
|
||||||
},
|
|
||||||
integration: {
|
|
||||||
multiFeature: '✓',
|
|
||||||
realWorld: '✓',
|
|
||||||
performance: '✓',
|
|
||||||
scalability: '✓'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run all tests
|
|
||||||
* @param {Object} arbiter - Arbiter instance for testing
|
|
||||||
* @returns {Object} Comprehensive test results
|
|
||||||
*/
|
|
||||||
export function runAllTests(arbiter) {
|
|
||||||
const runner = new TestRunner();
|
|
||||||
runner.setup(arbiter);
|
|
||||||
return runner.runAllTests();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run specific test suites
|
|
||||||
* @param {Object} arbiter - Arbiter instance for testing
|
|
||||||
* @param {string[]} suiteNames - Names of test suites to run
|
|
||||||
* @returns {Object} Test results for specified suites
|
|
||||||
*/
|
|
||||||
export function runSpecificTests(arbiter, suiteNames) {
|
|
||||||
const runner = new TestRunner();
|
|
||||||
runner.setup(arbiter);
|
|
||||||
return runner.runSpecificTests(suiteNames);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run tests by level
|
|
||||||
* @param {Object} arbiter - Arbiter instance for testing
|
|
||||||
* @param {string} level - Test level to run
|
|
||||||
* @returns {Object} Test results for specified level
|
|
||||||
*/
|
|
||||||
export function runTestsByLevel(arbiter, level) {
|
|
||||||
const runner = new TestRunner();
|
|
||||||
runner.setup(arbiter);
|
|
||||||
return runner.runTestsByLevel(level);
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
export const DSL_PRELUDE = `
|
|
||||||
// Built-in types and relations available in every graph.
|
|
||||||
// These are intended for request-scoped auth/session evidence (partial graph inputs).
|
|
||||||
|
|
||||||
definition User {
|
|
||||||
id: string
|
|
||||||
}
|
|
||||||
|
|
||||||
definition Account {
|
|
||||||
id: string
|
|
||||||
tier: string
|
|
||||||
}
|
|
||||||
|
|
||||||
definition Device {
|
|
||||||
id: string
|
|
||||||
device_risk: number
|
|
||||||
auth_method: string
|
|
||||||
ip_address: string
|
|
||||||
user_agent: string
|
|
||||||
}
|
|
||||||
|
|
||||||
definition AuthSession {
|
|
||||||
login_time: timestamp
|
|
||||||
last_login_time: timestamp
|
|
||||||
mfa_used: boolean
|
|
||||||
auth_method: string
|
|
||||||
ip_address: string
|
|
||||||
user_agent: string
|
|
||||||
expires_at: timestamp
|
|
||||||
device_risk: number
|
|
||||||
}
|
|
||||||
|
|
||||||
fact session_for_user(user: User, session: AuthSession)
|
|
||||||
fact session_for_account(account: Account, session: AuthSession)
|
|
||||||
fact session_for_device(device: Device, session: AuthSession)
|
|
||||||
fact logged_in_as(device: Device, account: Account)
|
|
||||||
`;
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -172,7 +172,30 @@ export class AuthorizationChecker {
|
|||||||
...(includeMeta && { meta: { reason: 'threshold_not_met', threshold: effectiveThreshold, actual: directRel.possibility } }),
|
...(includeMeta && { meta: { reason: 'threshold_not_met', threshold: effectiveThreshold, actual: directRel.possibility } }),
|
||||||
reason: 'threshold_not_met'
|
reason: 'threshold_not_met'
|
||||||
};
|
};
|
||||||
} else {
|
} else if (config?.expectedValue !== undefined && directRel.value !== config.expectedValue) {
|
||||||
|
// Value gate (mirrors DirectRule): a config carrying expectedValue
|
||||||
|
// (from a DSL literal like balance(user, 5)) only grants when the
|
||||||
|
// edge carries exactly that value.
|
||||||
|
result = {
|
||||||
|
possibility: 0,
|
||||||
|
reliability: 0,
|
||||||
|
...(includeMeta && {
|
||||||
|
meta: { reason: 'value_mismatch', expectedValue: config.expectedValue, actualValue: directRel.value }
|
||||||
|
}),
|
||||||
|
reason: 'value_mismatch'
|
||||||
|
};
|
||||||
|
} else if (options.expectedValue !== undefined && directRel.value !== options.expectedValue) {
|
||||||
|
// Per-check value-object override (check(user, can_withdraw, 5)):
|
||||||
|
// the value arrives on the check options, not the config.
|
||||||
|
result = {
|
||||||
|
possibility: 0,
|
||||||
|
reliability: 0,
|
||||||
|
...(includeMeta && {
|
||||||
|
meta: { reason: 'value_mismatch', expectedValue: options.expectedValue, actualValue: directRel.value }
|
||||||
|
}),
|
||||||
|
reason: 'value_mismatch'
|
||||||
|
};
|
||||||
|
} else {
|
||||||
result = {
|
result = {
|
||||||
possibility: directRel.possibility,
|
possibility: directRel.possibility,
|
||||||
validity: includeMeta
|
validity: includeMeta
|
||||||
@@ -308,7 +331,8 @@ export class AuthorizationChecker {
|
|||||||
const canCacheRuleResult = !hasPartialGraph && !explain && !includeMeta &&
|
const canCacheRuleResult = !hasPartialGraph && !explain && !includeMeta &&
|
||||||
!binary && !temporalPinned && options.cacheRuleResult !== false && this.decisionCache.ruleEnabled;
|
!binary && !temporalPinned && options.cacheRuleResult !== false && this.decisionCache.ruleEnabled;
|
||||||
const ruleCacheKey = canCacheRuleResult
|
const ruleCacheKey = canCacheRuleResult
|
||||||
? this._getRuleResultCacheKey(userId, relation, objectId)
|
? this._getRuleResultCacheKey(userId, relation, objectId) +
|
||||||
|
(options.expectedValue !== undefined ? `:v${options.expectedValue}` : '')
|
||||||
: null;
|
: null;
|
||||||
if (canCacheRuleResult) {
|
if (canCacheRuleResult) {
|
||||||
const cached = this.decisionCache.getRule(ruleCacheKey);
|
const cached = this.decisionCache.getRule(ruleCacheKey);
|
||||||
@@ -507,6 +531,12 @@ export class AuthorizationChecker {
|
|||||||
if (res.reason === 'values_compared_comparison_true') reason = 'values_compared_comparison_true';
|
if (res.reason === 'values_compared_comparison_true') reason = 'values_compared_comparison_true';
|
||||||
if (res.reason === 'values_compared_comparison_false') reason = 'values_compared_comparison_false';
|
if (res.reason === 'values_compared_comparison_false') reason = 'values_compared_comparison_false';
|
||||||
if (res.reason === 'values_compared_comparison_insufficient') reason = 'values_compared_comparison_insufficient';
|
if (res.reason === 'values_compared_comparison_insufficient') reason = 'values_compared_comparison_insufficient';
|
||||||
|
// Add defeasible logic reasons (normal mode surfaces these when a
|
||||||
|
// defeasible rule resolves to 0 — never, requirements, or defeaters).
|
||||||
|
if (res.reason === 'defeated_by_unless') reason = 'defeated_by_unless';
|
||||||
|
if (res.reason === 'never_rule_triggered') reason = 'never_rule_triggered';
|
||||||
|
if (res.reason === 'requirements_not_met') reason = 'requirements_not_met';
|
||||||
|
if (res.reason === 'strict_rule_failed') reason = 'strict_rule_failed';
|
||||||
|
|
||||||
if (resAllowPossibility > maxAllow) {
|
if (resAllowPossibility > maxAllow) {
|
||||||
maxAllow = resAllowPossibility;
|
maxAllow = resAllowPossibility;
|
||||||
@@ -961,6 +991,15 @@ export class AuthorizationChecker {
|
|||||||
if (!this.decisionCache.directEnabled) return;
|
if (!this.decisionCache.directEnabled) return;
|
||||||
const cacheKey = this._getDirectCheckCacheKey(userKey, relation, objectKey);
|
const cacheKey = this._getDirectCheckCacheKey(userKey, relation, objectKey);
|
||||||
this.decisionCache.setDirect(cacheKey, result);
|
this.decisionCache.setDirect(cacheKey, result);
|
||||||
|
// Track the (hashed) key under the checked relation + its base relations
|
||||||
|
// + the subject/object nodes so relation- and node-level invalidation can
|
||||||
|
// delete it without parsing the key.
|
||||||
|
const arb = this.arbiter;
|
||||||
|
if (arb && typeof arb._trackDirectCheckKey === 'function') {
|
||||||
|
const srcId = arb.resolveNodeId(userKey);
|
||||||
|
const dstId = arb.resolveNodeId(objectKey);
|
||||||
|
arb._trackDirectCheckKey(relation, srcId, dstId, cacheKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_getVisitedMode(visited) {
|
_getVisitedMode(visited) {
|
||||||
|
|||||||
@@ -107,6 +107,11 @@ export class DecisionCache {
|
|||||||
if (!this.ruleEnabled) return undefined;
|
if (!this.ruleEnabled) return undefined;
|
||||||
const entry = this.arbiter.ruleResultCache.get(ruleCacheKey);
|
const entry = this.arbiter.ruleResultCache.get(ruleCacheKey);
|
||||||
if (!entry) return undefined;
|
if (!entry) return undefined;
|
||||||
|
// A graph mutation since the entry was computed invalidates it (stale
|
||||||
|
// derived result). _graphVersion increments on every relation mutation.
|
||||||
|
if (entry.graphVersion !== undefined && entry.graphVersion !== (this.arbiter._graphVersion ?? 0)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
if (this.clock() - entry.timestamp >= this.arbiter.ruleResultCacheTTL) {
|
if (this.clock() - entry.timestamp >= this.arbiter.ruleResultCacheTTL) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -117,7 +122,8 @@ export class DecisionCache {
|
|||||||
if (!this.ruleEnabled) return;
|
if (!this.ruleEnabled) return;
|
||||||
this.arbiter.ruleResultCache.set(ruleCacheKey, {
|
this.arbiter.ruleResultCache.set(ruleCacheKey, {
|
||||||
result,
|
result,
|
||||||
timestamp: this.clock()
|
timestamp: this.clock(),
|
||||||
|
graphVersion: this.arbiter._graphVersion ?? 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,32 +174,9 @@ export class DecisionCache {
|
|||||||
if (!a?.directCheckCache) return;
|
if (!a?.directCheckCache) return;
|
||||||
const nodeId = a.nodeIdByKey?.get(nodeKey);
|
const nodeId = a.nodeIdByKey?.get(nodeKey);
|
||||||
if (nodeId === undefined) return;
|
if (nodeId === undefined) return;
|
||||||
const cache = a.directCheckCache;
|
// Keys are rolling hashes (not pipe-delimited strings), so node-level
|
||||||
|
// invalidation uses the per-node key index populated at cache-set time.
|
||||||
// Pattern-based invalidation is the safest path — the cache file
|
a._invalidateDirectCheckCacheByNode?.(nodeId);
|
||||||
// owns its storage layout and decides how to enumerate keys.
|
|
||||||
if (typeof cache.invalidateByPattern === 'function') {
|
|
||||||
// Match a nodeId that appears in any position of the composite
|
|
||||||
// key. The composite key format is `${srcId}|${rel}|${dstId}` and
|
|
||||||
// components are pipe-delimited, so a digit-boundary regex is
|
|
||||||
// safer than a plain substring match.
|
|
||||||
const escaped = String(nodeId).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
||||||
const pattern = new RegExp(`(?:^|\\|)${escaped}(?:\\||$)`);
|
|
||||||
cache.invalidateByPattern(pattern);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: explicit key iteration. HyperbolicLRUCache does not
|
|
||||||
// expose `keys()`, so this branch is unreachable for that cache
|
|
||||||
// family. If a future cache implementation exposes iteration,
|
|
||||||
// we walk it without leaking storage-layout details.
|
|
||||||
if (typeof cache.keys === 'function') {
|
|
||||||
for (const key of cache.keys()) {
|
|
||||||
if (typeof key === 'string' && key.includes(String(nodeId))) {
|
|
||||||
cache.delete(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -204,6 +187,9 @@ export class DecisionCache {
|
|||||||
const a = this.arbiter;
|
const a = this.arbiter;
|
||||||
if (!a) return;
|
if (!a) return;
|
||||||
a.directCheckCache?.clear?.();
|
a.directCheckCache?.clear?.();
|
||||||
|
a.directCheckCacheKeysByRelation?.clear?.();
|
||||||
|
a.directCheckCacheKeysByNode?.clear?.();
|
||||||
|
a.directKeyToRelations?.clear?.();
|
||||||
a.invalidateAllRuleResultCache?.();
|
a.invalidateAllRuleResultCache?.();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export class RuleEvaluator {
|
|||||||
tuple_to_userset: new TupleToUsersetRule(arbiter),
|
tuple_to_userset: new TupleToUsersetRule(arbiter),
|
||||||
multi_hop: new MultiHopRule(arbiter),
|
multi_hop: new MultiHopRule(arbiter),
|
||||||
relational_comparator: new RelationalComparatorRouter(arbiter, this),
|
relational_comparator: new RelationalComparatorRouter(arbiter, this),
|
||||||
chain: new ChainRule(arbiter),
|
chain: new ChainRule(arbiter, this),
|
||||||
challenge: new ChallengeRule(arbiter)
|
challenge: new ChallengeRule(arbiter)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -33,8 +33,25 @@ export class RuleEvaluator {
|
|||||||
|
|
||||||
|
|
||||||
// Convert string keys to numeric IDs if needed
|
// Convert string keys to numeric IDs if needed
|
||||||
const numericUserId = typeof userId === 'string' ? this.arbiter.resolveNodeId(userId, options) : userId;
|
let numericUserId = typeof userId === 'string' ? this.arbiter.resolveNodeId(userId, options) : userId;
|
||||||
const numericObjectId = typeof objectId === 'string' ? this.arbiter.resolveNodeId(objectId, options) : objectId;
|
let numericObjectId = typeof objectId === 'string' ? this.arbiter.resolveNodeId(objectId, options) : objectId;
|
||||||
|
|
||||||
|
// Unary / subject-scoped rules (e.g. a DSL predicate call `banned(user)`
|
||||||
|
// inside a binary evidence) check the relation on the SUBJECT itself — the
|
||||||
|
// object is the user. The DSL generator marks these with _subjectAsObject;
|
||||||
|
// the evaluator rewrites the object to the subject so (u, banned, u) matches
|
||||||
|
// the unary fact's self-edge instead of (u, banned, object).
|
||||||
|
if (rule._subjectAsObject) {
|
||||||
|
numericObjectId = numericUserId;
|
||||||
|
objectKey = userKey;
|
||||||
|
}
|
||||||
|
// A unary call whose subject entity IS the object parameter
|
||||||
|
// (`trusted(other)` inside peer_trusted(user, other)) checks the relation
|
||||||
|
// as a self-edge on the object — rewrite the subject to the object.
|
||||||
|
if (rule._subjectIsObject) {
|
||||||
|
numericUserId = numericObjectId;
|
||||||
|
userKey = objectKey;
|
||||||
|
}
|
||||||
|
|
||||||
const needsValueContext = valueContext !== null && valueContext !== undefined
|
const needsValueContext = valueContext !== null && valueContext !== undefined
|
||||||
? true
|
? true
|
||||||
@@ -50,13 +67,18 @@ export class RuleEvaluator {
|
|||||||
!binary && !options.partialGraphContext && !includeMeta && !temporalPinned &&
|
!binary && !options.partialGraphContext && !includeMeta && !temporalPinned &&
|
||||||
options.cacheRuleResult !== false;
|
options.cacheRuleResult !== false;
|
||||||
const ruleCacheKey = canCacheRuleResult
|
const ruleCacheKey = canCacheRuleResult
|
||||||
? this._getRuleResultCacheKey(numericUserId, currentRelation, numericObjectId, rule)
|
? this._getRuleResultCacheKey(numericUserId, currentRelation, numericObjectId, rule) +
|
||||||
|
(options.expectedValue !== undefined ? `:v${options.expectedValue}` : '')
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (canCacheRuleResult) {
|
if (canCacheRuleResult) {
|
||||||
const cached = this.arbiter.ruleResultCache.get(ruleCacheKey);
|
const cached = this.arbiter.ruleResultCache.get(ruleCacheKey);
|
||||||
const cacheNow = this.arbiter.clock ? this.arbiter.clock() : Date.now();
|
const cacheNow = this.arbiter.clock ? this.arbiter.clock() : Date.now();
|
||||||
if (cached && cacheNow - cached.timestamp < this.arbiter.ruleResultCacheTTL) {
|
// A graph mutation since the entry was computed invalidates it — the
|
||||||
|
// result is derived from the graph and would be stale. _graphVersion
|
||||||
|
// increments on every relation mutation.
|
||||||
|
const freshGraph = cached && cached.graphVersion === (this.arbiter._graphVersion ?? 0);
|
||||||
|
if (freshGraph && cacheNow - cached.timestamp < this.arbiter.ruleResultCacheTTL) {
|
||||||
this.arbiter.ruleResultCacheStats.hits++;
|
this.arbiter.ruleResultCacheStats.hits++;
|
||||||
if (collectValues && finalValueContext && cached.result?.collectedValues?.length) {
|
if (collectValues && finalValueContext && cached.result?.collectedValues?.length) {
|
||||||
const ruleType = rule?.type || (rule?.union ? 'union' : rule?.intersection ? 'intersection' : rule?.exclusion ? 'exclusion' : 'rule');
|
const ruleType = rule?.type || (rule?.union ? 'union' : rule?.intersection ? 'intersection' : rule?.exclusion ? 'exclusion' : 'rule');
|
||||||
@@ -74,17 +96,17 @@ export class RuleEvaluator {
|
|||||||
|
|
||||||
if (rule.union) {
|
if (rule.union) {
|
||||||
const result = this.logicalOperators.evaluateUnion(numericUserId, userKey, numericObjectId, objectKey, rule, visited, currentRelation, enhancedOptions);
|
const result = this.logicalOperators.evaluateUnion(numericUserId, userKey, numericObjectId, objectKey, rule, visited, currentRelation, enhancedOptions);
|
||||||
return this._maybeCacheRuleResult(result, currentRelation, ruleCacheKey, canCacheRuleResult);
|
return this._maybeCacheRuleResult(this._applyNegate(result, rule), currentRelation, ruleCacheKey, canCacheRuleResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rule.intersection) {
|
if (rule.intersection) {
|
||||||
const result = this.logicalOperators.evaluateIntersection(numericUserId, userKey, numericObjectId, objectKey, rule, visited, currentRelation, enhancedOptions);
|
const result = this.logicalOperators.evaluateIntersection(numericUserId, userKey, numericObjectId, objectKey, rule, visited, currentRelation, enhancedOptions);
|
||||||
return this._maybeCacheRuleResult(result, currentRelation, ruleCacheKey, canCacheRuleResult);
|
return this._maybeCacheRuleResult(this._applyNegate(result, rule), currentRelation, ruleCacheKey, canCacheRuleResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rule.exclusion) {
|
if (rule.exclusion) {
|
||||||
const result = this.logicalOperators.evaluateExclusion(numericUserId, userKey, numericObjectId, objectKey, rule, visited, currentRelation, enhancedOptions);
|
const result = this.logicalOperators.evaluateExclusion(numericUserId, userKey, numericObjectId, objectKey, rule, visited, currentRelation, enhancedOptions);
|
||||||
return this._maybeCacheRuleResult(result, currentRelation, ruleCacheKey, canCacheRuleResult);
|
return this._maybeCacheRuleResult(this._applyNegate(result, rule), currentRelation, ruleCacheKey, canCacheRuleResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Defeasible configs ({ type: 'defeasible', when/unless/never/always })
|
// Defeasible configs ({ type: 'defeasible', when/unless/never/always })
|
||||||
@@ -163,12 +185,34 @@ export class RuleEvaluator {
|
|||||||
const cacheNow = this.arbiter.clock ? this.arbiter.clock() : Date.now();
|
const cacheNow = this.arbiter.clock ? this.arbiter.clock() : Date.now();
|
||||||
this.arbiter.ruleResultCache.set(cacheKey, {
|
this.arbiter.ruleResultCache.set(cacheKey, {
|
||||||
result,
|
result,
|
||||||
timestamp: cacheNow
|
timestamp: cacheNow,
|
||||||
|
graphVersion: this.arbiter._graphVersion ?? 0
|
||||||
});
|
});
|
||||||
this.arbiter._cacheRuleResult(relation, cacheKey);
|
this.arbiter._cacheRuleResult(relation, cacheKey);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the NOT operator's negation to a logical rule result. The DSL emits
|
||||||
|
* `NOT x` as { type:'logical', intersection:{ rules:[x], negate:true } };
|
||||||
|
* the individual union/intersection/exclusion evaluators don't honor the
|
||||||
|
* flag, so it is applied here after evaluation (possibility -> 1 - p).
|
||||||
|
*/
|
||||||
|
_applyNegate(result, rule) {
|
||||||
|
const negate = Boolean(
|
||||||
|
(rule.union && rule.union.negate) ||
|
||||||
|
(rule.intersection && rule.intersection.negate) ||
|
||||||
|
(rule.exclusion && rule.exclusion.negate)
|
||||||
|
);
|
||||||
|
if (!negate || !result || typeof result.possibility !== 'number') return result;
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
possibility: Math.max(0, 1 - result.possibility),
|
||||||
|
...(result.meta ? { meta: { ...result.meta, negated: true } } : {}),
|
||||||
|
reason: 'negated'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
_getComparatorCacheSignature(rule) {
|
_getComparatorCacheSignature(rule) {
|
||||||
const leftSig = this._getComparatorOperandSignature(rule.left || rule.leftOperand);
|
const leftSig = this._getComparatorOperandSignature(rule.left || rule.leftOperand);
|
||||||
const rightSig = this._getComparatorOperandSignature(rule.right || rule.rightOperand);
|
const rightSig = this._getComparatorOperandSignature(rule.right || rule.rightOperand);
|
||||||
|
|||||||
@@ -44,8 +44,9 @@ import { QualitativeScale } from '../../qualitative/QualitativeScale.js';
|
|||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
export class ChainRule extends BaseRule {
|
export class ChainRule extends BaseRule {
|
||||||
constructor(arbiter) {
|
constructor(arbiter, ruleEvaluator = null) {
|
||||||
super(arbiter);
|
super(arbiter);
|
||||||
|
this.ruleEvaluator = ruleEvaluator;
|
||||||
|
|
||||||
// Chain-specific caching with HyperbolicLRUCache for better memory management
|
// Chain-specific caching with HyperbolicLRUCache for better memory management
|
||||||
this.maxCacheSize = 2000;
|
this.maxCacheSize = 2000;
|
||||||
@@ -199,7 +200,81 @@ export class ChainRule extends BaseRule {
|
|||||||
? { relation: rawStep, direction: 'out' }
|
? { relation: rawStep, direction: 'out' }
|
||||||
: rawStep;
|
: rawStep;
|
||||||
const { relation: stepRelation, direction } = step;
|
const { relation: stepRelation, direction } = step;
|
||||||
|
|
||||||
|
// CONDITION STEP (rule-based step): a step carrying a `rule` config is a
|
||||||
|
// condition-gated hop, not a plain edge traversal. The DSL compiler emits
|
||||||
|
// these when a chain step references a logical/defeasible evidence.
|
||||||
|
// - FINAL step: the object is known, so each current path's node is
|
||||||
|
// checked against the object through the referenced rule.
|
||||||
|
// - INTERMEDIATE step: the rule is EXPANDED from each current node
|
||||||
|
// (rule-based reachability) — the reachable destinations of the
|
||||||
|
// rule's base edges, filtered by its defeaters/requirements — and
|
||||||
|
// the traversal continues from each discovered node.
|
||||||
|
// The rule config is part of the chain cache key, so cache correctness
|
||||||
|
// is preserved.
|
||||||
|
if (step.rule) {
|
||||||
|
if (!this.ruleEvaluator) {
|
||||||
|
return this._createStandardResult({
|
||||||
|
possibility: 0,
|
||||||
|
reliability: 1.0,
|
||||||
|
...(includeMeta && { meta: null }),
|
||||||
|
reason: 'condition_step_requires_rule_evaluator'
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
if (stepIndex === steps.length - 1) {
|
||||||
|
const conditionPaths = [];
|
||||||
|
for (const currentPath of currentPaths) {
|
||||||
|
const condResult = this.ruleEvaluator.evaluateRule(
|
||||||
|
currentPath.id, currentPath.key, objectIdNum, objectKey,
|
||||||
|
step.rule, new Set(visited || []), currentRelation, options
|
||||||
|
);
|
||||||
|
const condPossibility = condResult.possibility ?? 0;
|
||||||
|
if (condPossibility <= 0) continue;
|
||||||
|
const nextPossibility = Math.min(currentPath.possibility, condPossibility);
|
||||||
|
if (fastPath && nextPossibility < minPossibility) continue;
|
||||||
|
conditionPaths.push({
|
||||||
|
id: objectIdNum,
|
||||||
|
key: objectKey,
|
||||||
|
possibility: nextPossibility,
|
||||||
|
reliability: (currentPath.reliability ?? 1.0) * (condResult.reliability ?? 1.0),
|
||||||
|
path: [...currentPath.path, objectKey],
|
||||||
|
pathEntities: [...currentPath.pathEntities, { id: objectIdNum, key: objectKey, source: 'condition' }]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
currentPaths = conditionPaths;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// INTERMEDIATE condition step: expand the rule from each current node.
|
||||||
|
const pathMap = new Map();
|
||||||
|
for (const currentPath of currentPaths) {
|
||||||
|
const reachable = this._expandRuleFromSrc(currentPath.id, step.rule, options);
|
||||||
|
for (const [nextId, cand] of reachable) {
|
||||||
|
const nextKey = this.arbiter.resolveKey(nextId, options);
|
||||||
|
if (!nextKey) continue;
|
||||||
|
const nextPossibility = Math.min(currentPath.possibility, cand.possibility);
|
||||||
|
if (fastPath && nextPossibility < minPossibility) continue;
|
||||||
|
const nextReliability = (currentPath.reliability ?? 1.0) * cand.reliability;
|
||||||
|
const existing = pathMap.get(nextId);
|
||||||
|
if (existing && existing.possibility > nextPossibility) continue;
|
||||||
|
if (existing && existing.possibility === nextPossibility && existing.reliability >= nextReliability) continue;
|
||||||
|
pathMap.set(nextId, {
|
||||||
|
id: nextId,
|
||||||
|
key: nextKey,
|
||||||
|
possibility: nextPossibility,
|
||||||
|
reliability: nextReliability,
|
||||||
|
path: [...currentPath.path, nextKey],
|
||||||
|
pathEntities: [...currentPath.pathEntities, { id: nextId, key: nextKey, source: 'condition' }]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentPaths = Array.from(pathMap.values());
|
||||||
|
if (currentPaths.length === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!stepRelation || !direction) {
|
if (!stepRelation || !direction) {
|
||||||
currentPaths = [];
|
currentPaths = [];
|
||||||
break;
|
break;
|
||||||
@@ -408,15 +483,214 @@ export class ChainRule extends BaseRule {
|
|||||||
* Get relations for a step based on direction
|
* Get relations for a step based on direction
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_getRelationsForStep(entityId, relation, direction, options = null) {
|
_getRelationsForStep(entityId, relation, direction, options = null) {
|
||||||
if (direction === 'out') {
|
if (direction === 'out') {
|
||||||
return this.arbiter.relationManager.getRelationsFromSrc(entityId, relation, options);
|
return this.arbiter.relationManager.getRelationsFromSrc(entityId, relation, options);
|
||||||
} else if (direction === 'in') {
|
} else if (direction === 'in') {
|
||||||
return this.arbiter.relationManager.getRelationsToDst(entityId, relation, options);
|
return this.arbiter.relationManager.getRelationsToDst(entityId, relation, options);
|
||||||
} else {
|
} else {
|
||||||
// Default to 'out' for backward compatibility
|
// Default to 'out' for backward compatibility
|
||||||
return this.arbiter.relationManager.getRelationsFromSrc(entityId, relation, options);
|
return this.arbiter.relationManager.getRelationsFromSrc(entityId, relation, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Rule-based reachability: expand a rule config from a source node into its
|
||||||
|
// reachable destinations. Used by intermediate condition steps — a
|
||||||
|
// logical/defeasible evidence as a non-final chain step discovers its
|
||||||
|
// reachable nodes (the base edges' destinations, filtered by the rule's
|
||||||
|
// defeaters/requirements) instead of checking a single (src, dst) pair.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expand a rule config from a source node into Map<dstId, {possibility, reliability}>.
|
||||||
|
* Supports direct rules, logical union/intersection nodes, defeasible rules
|
||||||
|
* (when/unless/never/requires/always), and nested chains.
|
||||||
|
*/
|
||||||
|
_expandRuleFromSrc(srcId, rule, options = null) {
|
||||||
|
if (!rule || typeof rule !== 'object') return new Map();
|
||||||
|
if (rule.type === 'direct' && rule.relation) {
|
||||||
|
return this._expandDirectFromSrc(srcId, rule.relation, options);
|
||||||
}
|
}
|
||||||
|
if (rule.type === 'chain' && Array.isArray(rule.steps)) {
|
||||||
|
return this._expandChainRuleFromSrc(srcId, rule, options);
|
||||||
|
}
|
||||||
|
if (rule.type === 'tuple_to_userset') {
|
||||||
|
return this._expandTtuFromSrc(srcId, rule, options);
|
||||||
|
}
|
||||||
|
if (rule.type === 'logical') {
|
||||||
|
if (rule.when || rule.unless || rule.never || rule.requires || rule.always) {
|
||||||
|
return this._expandDefeasibleFromSrc(srcId, rule, options);
|
||||||
|
}
|
||||||
|
if (rule.union) {
|
||||||
|
return this._expandLogicalNodeFromSrc(srcId, rule.union, 'union', options);
|
||||||
|
}
|
||||||
|
if (rule.intersection) {
|
||||||
|
return this._expandLogicalNodeFromSrc(srcId, rule.intersection, 'intersection', options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
_expandDirectFromSrc(srcId, relation, options = null) {
|
||||||
|
const edges = this.arbiter.relationManager.getRelationsFromSrc(srcId, relation, options);
|
||||||
|
const out = new Map();
|
||||||
|
for (const e of edges || []) {
|
||||||
|
out.set(e.dst, { possibility: e.possibility ?? 1, reliability: e.reliability ?? 1 });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expand a tuple_to_userset config from a source node. Semantics (forward,
|
||||||
|
* the common case): src →computed→ intermediate, and intermediate ↔object
|
||||||
|
* via tupleset (direction decides which end the intermediate sits on). The
|
||||||
|
* reachable set is the OBJECTS sharing an intermediate with src. Combined
|
||||||
|
* possibility is the weakest link across the two hops.
|
||||||
|
*/
|
||||||
|
_expandTtuFromSrc(srcId, rule, options = null) {
|
||||||
|
const { tuplesetRelation, computedRelation, tuplesetDirection = 'out', reverse = false } = rule;
|
||||||
|
if (!tuplesetRelation || !computedRelation) return new Map();
|
||||||
|
// reverse swaps the roles: src is the object, intermediates come from the
|
||||||
|
// tupleset side, and computed edges go from intermediate to the user.
|
||||||
|
const srcAsUser = !reverse;
|
||||||
|
const computedRel = srcAsUser ? computedRelation : tuplesetRelation;
|
||||||
|
const tuplesetRel = srcAsUser ? tuplesetRelation : computedRelation;
|
||||||
|
const computedEdges = this.arbiter.relationManager.getRelationsFromSrc(srcId, computedRel, options);
|
||||||
|
const out = new Map();
|
||||||
|
for (const ce of computedEdges || []) {
|
||||||
|
const intermediateId = ce.dst;
|
||||||
|
const tsEdges = tuplesetDirection === 'in'
|
||||||
|
? this.arbiter.relationManager.getRelationsFromSrc(intermediateId, tuplesetRel, options)
|
||||||
|
: this.arbiter.relationManager.getRelationsToDst(intermediateId, tuplesetRel, options);
|
||||||
|
for (const te of tsEdges || []) {
|
||||||
|
const objId = tuplesetDirection === 'in' ? te.dst : te.src;
|
||||||
|
const poss = Math.min(ce.possibility ?? 1, te.possibility ?? 1);
|
||||||
|
const rel = (ce.reliability ?? 1) * (te.reliability ?? 1);
|
||||||
|
const cur = out.get(objId);
|
||||||
|
if (!cur || poss > cur.possibility) out.set(objId, { possibility: poss, reliability: rel });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
_expandLogicalNodeFromSrc(srcId, node, op, options = null) {
|
||||||
|
const rules = (node && node.rules) || [];
|
||||||
|
if (rules.length === 0) return new Map();
|
||||||
|
if (op === 'union') {
|
||||||
|
const out = new Map();
|
||||||
|
for (const r of rules) {
|
||||||
|
const m = this._expandRuleFromSrc(srcId, r, options);
|
||||||
|
for (const [id, v] of m) {
|
||||||
|
const cur = out.get(id);
|
||||||
|
if (!cur || v.possibility > cur.possibility) out.set(id, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
// intersection: nodes reachable via every branch, weakest-link combined
|
||||||
|
const maps = rules.map(r => this._expandRuleFromSrc(srcId, r, options));
|
||||||
|
const out = new Map();
|
||||||
|
for (const [id, v] of maps[0]) {
|
||||||
|
let ok = true;
|
||||||
|
let min = v.possibility;
|
||||||
|
let rel = v.reliability;
|
||||||
|
for (let i = 1; i < maps.length; i++) {
|
||||||
|
const other = maps[i].get(id);
|
||||||
|
if (!other) { ok = false; break; }
|
||||||
|
min = Math.min(min, other.possibility);
|
||||||
|
rel *= other.reliability;
|
||||||
|
}
|
||||||
|
if (ok) out.set(id, { possibility: min, reliability: rel });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
_expandDefeasibleFromSrc(srcId, rule, options = null) {
|
||||||
|
const base = this._expandBaseFromSrc(srcId, rule, options);
|
||||||
|
if (base.size === 0) return base;
|
||||||
|
const out = new Map();
|
||||||
|
const evalAt = (candId, candKey, node) => {
|
||||||
|
if (!node) return { poss: 1, rel: 1 };
|
||||||
|
const wrapped = node.union ? { union: node.union } : node;
|
||||||
|
const r = this.ruleEvaluator.evaluateRule(
|
||||||
|
srcId, null, candId, candKey, wrapped, new Set(), null, options || {}
|
||||||
|
);
|
||||||
|
return { poss: r.possibility ?? 0, rel: r.reliability ?? 1 };
|
||||||
|
};
|
||||||
|
for (const [candId, cand] of base) {
|
||||||
|
const candKey = this.arbiter.resolveKey(candId, options);
|
||||||
|
if (rule.never) {
|
||||||
|
const n = evalAt(candId, candKey, rule.never);
|
||||||
|
if (n.poss > 0.5) continue;
|
||||||
|
}
|
||||||
|
let defeat = 0;
|
||||||
|
let defeatRel = 1;
|
||||||
|
if (rule.unless) {
|
||||||
|
const u = evalAt(candId, candKey, rule.unless);
|
||||||
|
defeat = u.poss;
|
||||||
|
defeatRel = u.rel;
|
||||||
|
}
|
||||||
|
let req = 1;
|
||||||
|
let reqRel = 1;
|
||||||
|
if (rule.requires) {
|
||||||
|
const q = evalAt(candId, candKey, rule.requires);
|
||||||
|
req = q.poss;
|
||||||
|
reqRel = q.rel;
|
||||||
|
}
|
||||||
|
const poss = cand.possibility * req * (1 - defeat);
|
||||||
|
if (poss <= 0) continue;
|
||||||
|
out.set(candId, {
|
||||||
|
possibility: poss,
|
||||||
|
reliability: cand.reliability * reqRel * defeatRel
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
_expandBaseFromSrc(srcId, rule, options = null) {
|
||||||
|
if (rule.when && rule.when.intersection) {
|
||||||
|
return this._expandLogicalNodeFromSrc(srcId, rule.when.intersection, 'intersection', options);
|
||||||
|
}
|
||||||
|
if (rule.when && rule.when.union) {
|
||||||
|
return this._expandLogicalNodeFromSrc(srcId, rule.when.union, 'union', options);
|
||||||
|
}
|
||||||
|
if (rule.when && rule.when.rules) {
|
||||||
|
return this._expandLogicalNodeFromSrc(srcId, rule.when, 'union', options);
|
||||||
|
}
|
||||||
|
if (rule.union) {
|
||||||
|
return this._expandLogicalNodeFromSrc(srcId, rule.union, 'union', options);
|
||||||
|
}
|
||||||
|
if (rule.always && rule.always.direct) {
|
||||||
|
return this._expandRuleFromSrc(srcId, rule.always.direct, options);
|
||||||
|
}
|
||||||
|
return new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
_expandChainRuleFromSrc(srcId, rule, options = null) {
|
||||||
|
let current = [{ id: srcId, possibility: 1, reliability: 1 }];
|
||||||
|
for (const step of rule.steps || []) {
|
||||||
|
const stepName = typeof step === 'string' ? step : step.relation;
|
||||||
|
const stepRule = typeof step === 'string' ? null : step.rule;
|
||||||
|
const next = [];
|
||||||
|
for (const node of current) {
|
||||||
|
const m = stepRule
|
||||||
|
? this._expandRuleFromSrc(node.id, stepRule, options)
|
||||||
|
: this._expandDirectFromSrc(node.id, stepName, options);
|
||||||
|
for (const [id, v] of m) {
|
||||||
|
next.push({
|
||||||
|
id,
|
||||||
|
possibility: Math.min(node.possibility, v.possibility),
|
||||||
|
reliability: node.reliability * v.reliability
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current = next;
|
||||||
|
if (current.length === 0) break;
|
||||||
|
}
|
||||||
|
const out = new Map();
|
||||||
|
for (const n of current) out.set(n.id, { possibility: n.possibility, reliability: n.reliability });
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -433,6 +707,12 @@ export class ChainRule extends BaseRule {
|
|||||||
const key = this._getChainResultCacheKey(userId, objectId, steps);
|
const key = this._getChainResultCacheKey(userId, objectId, steps);
|
||||||
const entry = this.chainResultCache.get(key);
|
const entry = this.chainResultCache.get(key);
|
||||||
|
|
||||||
|
// A graph mutation since the entry was computed invalidates it: the
|
||||||
|
// reachability it captured is stale. The arbiter increments _graphVersion
|
||||||
|
// on every relation mutation, so any mismatch is a miss.
|
||||||
|
if (entry && entry.graphVersion !== (this.arbiter._graphVersion ?? 0)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const cacheNow = this.arbiter.clock ? this.arbiter.clock() : Date.now();
|
const cacheNow = this.arbiter.clock ? this.arbiter.clock() : Date.now();
|
||||||
if (entry && cacheNow - entry.timestamp < this.cacheTTL) {
|
if (entry && cacheNow - entry.timestamp < this.cacheTTL) {
|
||||||
return entry.result;
|
return entry.result;
|
||||||
@@ -455,7 +735,8 @@ export class ChainRule extends BaseRule {
|
|||||||
const cacheNow = this.arbiter.clock ? this.arbiter.clock() : Date.now();
|
const cacheNow = this.arbiter.clock ? this.arbiter.clock() : Date.now();
|
||||||
this.chainResultCache.set(key, {
|
this.chainResultCache.set(key, {
|
||||||
result,
|
result,
|
||||||
timestamp: cacheNow
|
timestamp: cacheNow,
|
||||||
|
graphVersion: this.arbiter._graphVersion ?? 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,26 @@ export class DirectRule extends BaseRule {
|
|||||||
})
|
})
|
||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Value gate: a rule carrying expectedValue (from a DSL literal like
|
||||||
|
// `balance(user, 5)`, or a per-check value-object override like
|
||||||
|
// `check(user, can_withdraw, 5)`) only grants when the matched edge carries
|
||||||
|
// exactly that value. Without it a value-carrying fact matches ANY edge of
|
||||||
|
// the relation, silently over-granting past the declared amount.
|
||||||
|
const expectedValue = rule.expectedValue !== undefined ? rule.expectedValue : options.expectedValue;
|
||||||
|
if (expectedValue !== undefined && directRel.value !== expectedValue) {
|
||||||
|
return this._createStandardResult({
|
||||||
|
possibility: 0,
|
||||||
|
...(includeMeta && {
|
||||||
|
meta: {
|
||||||
|
ruleType: 'direct',
|
||||||
|
reason: 'value_mismatch',
|
||||||
|
expectedValue,
|
||||||
|
actualValue: directRel.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
const relationStrength = directRel.possibility;
|
const relationStrength = directRel.possibility;
|
||||||
const _source = directRel.source || 'persistent';
|
const _source = directRel.source || 'persistent';
|
||||||
|
|||||||
@@ -805,6 +805,7 @@ export class LogicalOperators extends BaseRule {
|
|||||||
return {
|
return {
|
||||||
possibility: 0,
|
possibility: 0,
|
||||||
reliability,
|
reliability,
|
||||||
|
reason: 'never_rule_triggered',
|
||||||
collectedValues: allCollectedValues,
|
collectedValues: allCollectedValues,
|
||||||
meta: {
|
meta: {
|
||||||
...resultMeta,
|
...resultMeta,
|
||||||
@@ -856,11 +857,29 @@ export class LogicalOperators extends BaseRule {
|
|||||||
resultMeta = { ...defeatersResult.meta, defeatedBy: defeatersResult.meta };
|
resultMeta = { ...defeatersResult.meta, defeatedBy: defeatersResult.meta };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const finalPossibility = Math.max(0, Math.min(1, possibility));
|
||||||
|
|
||||||
|
// Surface a top-level reason so the AuthorizationChecker reports why a
|
||||||
|
// defeasible rule resolved to 0 instead of a generic 'no_matching_rule'.
|
||||||
|
// Ordering mirrors precedence: requirements beat defeaters; a defeater
|
||||||
|
// reason is only claimed when the base (when/strict) leg was actually
|
||||||
|
// active — otherwise the rule simply did not match.
|
||||||
|
let reason;
|
||||||
|
if (finalPossibility === 0) {
|
||||||
|
const baseWasActive = (defeasibleResult?.possibility || 0) > 0 || (strictResult?.possibility || 0) > 0;
|
||||||
|
if (requiresResult && requiresResult.possibility < 0.5) {
|
||||||
|
reason = 'requirements_not_met';
|
||||||
|
} else if (defeatersResult && defeatersResult.possibility > 0.5 && baseWasActive) {
|
||||||
|
reason = 'defeated_by_unless';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
possibility: Math.max(0, Math.min(1, possibility)),
|
possibility: finalPossibility,
|
||||||
reliability,
|
reliability,
|
||||||
validity: buildValidity('product', [], [], 2, Math.max(0, Math.min(1, possibility))),
|
reason,
|
||||||
|
validity: buildValidity('product', [], [], 2, finalPossibility),
|
||||||
collectedValues: allCollectedValues,
|
collectedValues: allCollectedValues,
|
||||||
meta: {
|
meta: {
|
||||||
...resultMeta,
|
...resultMeta,
|
||||||
|
|||||||
@@ -579,17 +579,6 @@ export class QualitativeRelationalComparatorRule extends BaseRule {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use _aggregateCrispValues instead. Removal after Stage 2.
|
|
||||||
*/
|
|
||||||
_aggregateBlurredValues(blurredValues, operandConfig, scale) {
|
|
||||||
if (!this._warnedAggregateBlurredValues) {
|
|
||||||
this._warnedAggregateBlurredValues = true;
|
|
||||||
console.warn('[QualitativeRelationalComparatorRule] _aggregateBlurredValues is deprecated; use _aggregateCrispValues instead.');
|
|
||||||
}
|
|
||||||
return this._aggregateCrispValues(blurredValues, operandConfig, scale);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compare blurred qualitative intervals
|
* Compare blurred qualitative intervals
|
||||||
* @private
|
* @private
|
||||||
|
|||||||
+97
-10
@@ -25,6 +25,13 @@ export class Arbiter {
|
|||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
|
|
||||||
|
// Monotonic graph mutation counter: incremented on every relation edge
|
||||||
|
// mutation (and node removal). Derived-result caches (e.g. the ChainRule
|
||||||
|
// result cache) stamp entries with the version they were computed at and
|
||||||
|
// treat any newer version as a miss, so a graph change can never serve a
|
||||||
|
// stale chain/authorization result.
|
||||||
|
this._graphVersion = 0;
|
||||||
|
|
||||||
// Audit affordance (caller-wired, zero cost when absent): invoked once
|
// Audit affordance (caller-wired, zero cost when absent): invoked once
|
||||||
// per check with a minimal decision record. The engine does NOT store
|
// per check with a minimal decision record. The engine does NOT store
|
||||||
// audit state — the caller owns persistence and retention. The record
|
// audit state — the caller owns persistence and retention. The record
|
||||||
@@ -63,6 +70,16 @@ export class Arbiter {
|
|||||||
// Direct check cache - use HyperbolicLRUCache for better memory management
|
// Direct check cache - use HyperbolicLRUCache for better memory management
|
||||||
this.directCheckCacheSize = options.directCheckCacheSize || 10000;
|
this.directCheckCacheSize = options.directCheckCacheSize || 10000;
|
||||||
this.directCheckCacheTTL = options.directCheckCacheTTL || 60000; // 1 minute
|
this.directCheckCacheTTL = options.directCheckCacheTTL || 60000; // 1 minute
|
||||||
|
|
||||||
|
// Direct-check cache key indexes (keys are rolling hashes, so relation-
|
||||||
|
// and node-level invalidation cannot parse them): relation -> keys,
|
||||||
|
// nodeId -> keys, key -> relations. Populated via _trackDirectCheckKey.
|
||||||
|
this.directCheckCacheKeysByRelation = new Map();
|
||||||
|
this.directCheckCacheKeysByNode = new Map();
|
||||||
|
this.directKeyToRelations = new Map();
|
||||||
|
// Reverse of dependencyIndex: checked relation -> Set of base relations it
|
||||||
|
// reads (so a base-relation mutation invalidates overridden-config keys).
|
||||||
|
this.dependencyIndexByRelation = new Map();
|
||||||
|
|
||||||
// Only create cache if caching is not disabled
|
// Only create cache if caching is not disabled
|
||||||
if (!this.disableCaching && !this.disableDirectCaching) {
|
if (!this.disableCaching && !this.disableDirectCaching) {
|
||||||
@@ -219,6 +236,14 @@ export class Arbiter {
|
|||||||
this.dependencyIndex.set(baseRel, entry);
|
this.dependencyIndex.set(baseRel, entry);
|
||||||
}
|
}
|
||||||
entry.all.add(relation);
|
entry.all.add(relation);
|
||||||
|
// Reverse map: the checked relation reads baseRel, so a baseRel
|
||||||
|
// mutation must invalidate this relation's direct-check keys.
|
||||||
|
let bases = this.dependencyIndexByRelation.get(relation);
|
||||||
|
if (!bases) {
|
||||||
|
bases = new Set();
|
||||||
|
this.dependencyIndexByRelation.set(relation, bases);
|
||||||
|
}
|
||||||
|
bases.add(baseRel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -426,20 +451,23 @@ export class Arbiter {
|
|||||||
for (const rel of entry.all) affected.add(rel);
|
for (const rel of entry.all) affected.add(rel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keys are rolling hashes (no pipe-delimited string form to match), so
|
||||||
|
// invalidation uses the per-relation key index populated at cache-set
|
||||||
|
// time: every direct-check result is tracked under the CHECKED relation
|
||||||
|
// AND its base relations (via _trackDirectCheckKey), so deleting the
|
||||||
|
// tracked keys for an affected relation covers overridden-config checks.
|
||||||
for (const rel of affected) {
|
for (const rel of affected) {
|
||||||
// Composite cache keys are `<stringId|relationId|stringId>` — the
|
const keys = this.directCheckCacheKeysByRelation.get(rel);
|
||||||
// middle component is the relation ID, so match on that (a name-based
|
if (!keys) continue;
|
||||||
// pattern can never hit). This also covers keys built from either
|
for (const key of keys) {
|
||||||
// string ids or node ids, since only the middle component matters.
|
this._deleteFromCache(this.directCheckCache, key);
|
||||||
if (typeof this.directCheckCache.invalidateByPattern === 'function') {
|
this.directKeyToRelations.delete(key);
|
||||||
const relId = this.keyManager._getRelationId(rel);
|
|
||||||
const pattern = new RegExp(`^[^|]*\\|${relId}\\|[^|]*$`);
|
|
||||||
this.directCheckCache.invalidateByPattern(pattern);
|
|
||||||
}
|
}
|
||||||
|
keys.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Explicit composite-key deletes (kept for cache implementations that
|
// Explicit composite-key deletes (recomputed hashes) — cheap belt for the
|
||||||
// lack invalidateByPattern).
|
// exact (src, relation, dst) pair.
|
||||||
const srcId = this.keyManager.getStringId(srcKey);
|
const srcId = this.keyManager.getStringId(srcKey);
|
||||||
const dstId = this.keyManager.getStringId(dstKey);
|
const dstId = this.keyManager.getStringId(dstKey);
|
||||||
const key1 = this.keyManager.createCompositeKey(srcId, relation, dstId);
|
const key1 = this.keyManager.createCompositeKey(srcId, relation, dstId);
|
||||||
@@ -455,6 +483,65 @@ export class Arbiter {
|
|||||||
this.invalidateRuleResultCacheByRelation(relation);
|
this.invalidateRuleResultCacheByRelation(relation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record a direct-check cache key so relation- and node-level invalidation
|
||||||
|
* can find it without parsing the (hashed) key. Tracked under the CHECKED
|
||||||
|
* relation and its base relations (so a base-relation mutation invalidates
|
||||||
|
* overridden-config checks) and under the subject/object node ids.
|
||||||
|
*/
|
||||||
|
_trackDirectCheckKey(relation, srcNodeId, dstNodeId, cacheKey) {
|
||||||
|
if (!cacheKey) return;
|
||||||
|
const rels = new Set([relation]);
|
||||||
|
const baseEntry = this.dependencyIndexByRelation.get(relation);
|
||||||
|
if (baseEntry) for (const base of baseEntry) rels.add(base);
|
||||||
|
for (const rel of rels) {
|
||||||
|
let set = this.directCheckCacheKeysByRelation.get(rel);
|
||||||
|
if (!set) {
|
||||||
|
set = new Set();
|
||||||
|
this.directCheckCacheKeysByRelation.set(rel, set);
|
||||||
|
}
|
||||||
|
set.add(cacheKey);
|
||||||
|
}
|
||||||
|
if (srcNodeId !== undefined && srcNodeId !== null) {
|
||||||
|
let set = this.directCheckCacheKeysByNode.get(srcNodeId);
|
||||||
|
if (!set) {
|
||||||
|
set = new Set();
|
||||||
|
this.directCheckCacheKeysByNode.set(srcNodeId, set);
|
||||||
|
}
|
||||||
|
set.add(cacheKey);
|
||||||
|
}
|
||||||
|
if (dstNodeId !== undefined && dstNodeId !== null && dstNodeId !== srcNodeId) {
|
||||||
|
let set = this.directCheckCacheKeysByNode.get(dstNodeId);
|
||||||
|
if (!set) {
|
||||||
|
set = new Set();
|
||||||
|
this.directCheckCacheKeysByNode.set(dstNodeId, set);
|
||||||
|
}
|
||||||
|
set.add(cacheKey);
|
||||||
|
}
|
||||||
|
let byKey = this.directKeyToRelations.get(cacheKey);
|
||||||
|
if (!byKey) {
|
||||||
|
byKey = new Set();
|
||||||
|
this.directKeyToRelations.set(cacheKey, byKey);
|
||||||
|
}
|
||||||
|
for (const rel of rels) byKey.add(rel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate direct-check entries involving a node id (node removal).
|
||||||
|
*/
|
||||||
|
_invalidateDirectCheckCacheByNode(nodeId) {
|
||||||
|
if (!this.directCheckCache || nodeId === undefined) return;
|
||||||
|
const keys = this.directCheckCacheKeysByNode.get(nodeId);
|
||||||
|
if (!keys) return;
|
||||||
|
for (const key of keys) {
|
||||||
|
this._deleteFromCache(this.directCheckCache, key);
|
||||||
|
const rels = this.directKeyToRelations.get(key);
|
||||||
|
if (rels) for (const rel of rels) this.directCheckCacheKeysByRelation.get(rel)?.delete(key);
|
||||||
|
this.directKeyToRelations.delete(key);
|
||||||
|
}
|
||||||
|
keys.clear();
|
||||||
|
}
|
||||||
|
|
||||||
_cacheRuleResult(relation, cacheKey) {
|
_cacheRuleResult(relation, cacheKey) {
|
||||||
if (!this.ruleResultCache || !cacheKey) return;
|
if (!this.ruleResultCache || !cacheKey) return;
|
||||||
let set = this.ruleResultCacheKeysByRelation.get(relation);
|
let set = this.ruleResultCacheKeysByRelation.get(relation);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { RelationCSR } from './relation/RelationCSR.js';
|
|
||||||
import { RelationSnapshotAccess } from './relation/RelationSnapshotAccess.js';
|
import { RelationSnapshotAccess } from './relation/RelationSnapshotAccess.js';
|
||||||
import { RelationCaches } from './relation/RelationCaches.js';
|
import { RelationCaches } from './relation/RelationCaches.js';
|
||||||
import { RelationLookup } from './relation/RelationLookup.js';
|
import { RelationLookup } from './relation/RelationLookup.js';
|
||||||
@@ -22,16 +21,10 @@ export class RelationManager {
|
|||||||
this._relationKeyToIndex = new Map();
|
this._relationKeyToIndex = new Map();
|
||||||
this._relationNameToId = new Map();
|
this._relationNameToId = new Map();
|
||||||
|
|
||||||
this._relationCsr = new RelationCSR(this);
|
|
||||||
this._snapshotAccess = new RelationSnapshotAccess(this);
|
this._snapshotAccess = new RelationSnapshotAccess(this);
|
||||||
this._lookup = new RelationLookup(this);
|
this._lookup = new RelationLookup(this);
|
||||||
this._updates = new RelationUpdates(this);
|
this._updates = new RelationUpdates(this);
|
||||||
this._relationGraph = new RelationGraphTraversal(this);
|
this._relationGraph = new RelationGraphTraversal(this);
|
||||||
if (this._relationCsr.enabled && !RelationManager._warnedRelationCsrDeprecated) {
|
|
||||||
RelationManager._warnedRelationCsrDeprecated = true;
|
|
||||||
console.warn('[RelationManager] relation CSR index is deprecated; avoid useRelationCsrIndex and related options.');
|
|
||||||
}
|
|
||||||
|
|
||||||
this._cacheHits = 0;
|
this._cacheHits = 0;
|
||||||
this._cacheMisses = 0;
|
this._cacheMisses = 0;
|
||||||
|
|
||||||
@@ -60,26 +53,6 @@ export class RelationManager {
|
|||||||
return this._relationGraph.shouldUseTraversal(nodeId, relation, reverse);
|
return this._relationGraph.shouldUseTraversal(nodeId, relation, reverse);
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildRelationCsrIndex(relation) {
|
|
||||||
return this._relationCsr.buildIndex(relation);
|
|
||||||
}
|
|
||||||
|
|
||||||
_getRelationCsrIndex(relation) {
|
|
||||||
return this._relationCsr.getIndex(relation);
|
|
||||||
}
|
|
||||||
|
|
||||||
_recordRelationCsrAdd(relationObj) {
|
|
||||||
this._relationCsr.recordAdd(relationObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
_recordRelationCsrRemove(srcId, relation, dstId) {
|
|
||||||
this._relationCsr.recordRemove(srcId, relation, dstId);
|
|
||||||
}
|
|
||||||
|
|
||||||
_getRelationsFromCsr(csr, nodeId, reverse = false) {
|
|
||||||
return this._relationCsr.getRelationsFromCsr(csr, nodeId, reverse);
|
|
||||||
}
|
|
||||||
|
|
||||||
_getRelationDegreeFromIndices(nodeId, relation, reverse = false) {
|
_getRelationDegreeFromIndices(nodeId, relation, reverse = false) {
|
||||||
this._ensureIndicesBuilt();
|
this._ensureIndicesBuilt();
|
||||||
const indices = this.arbiter.indices;
|
const indices = this.arbiter.indices;
|
||||||
@@ -525,17 +498,6 @@ export class RelationManager {
|
|||||||
return this.arbiter.valueManager.aggregateCrispValues(intervalValues, aggregator);
|
return this.arbiter.valueManager.aggregateCrispValues(intervalValues, aggregator);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use getAggregatedIntervalValue instead. Removal after Stage 2.
|
|
||||||
*/
|
|
||||||
getAggregatedBlurredValue(srcId, relation, aggregator = 'max') {
|
|
||||||
if (!this._warnedAggregatedBlurredValue) {
|
|
||||||
this._warnedAggregatedBlurredValue = true;
|
|
||||||
console.warn('[RelationManager] getAggregatedBlurredValue is deprecated; use getAggregatedIntervalValue instead.');
|
|
||||||
}
|
|
||||||
return this.getAggregatedIntervalValue(srcId, relation, aggregator);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compare values between two relations using interval arithmetic
|
* Compare values between two relations using interval arithmetic
|
||||||
* @param {Object} leftRelation - Left relation object or { srcId, relation, dstId }
|
* @param {Object} leftRelation - Left relation object or { srcId, relation, dstId }
|
||||||
@@ -734,6 +696,12 @@ export class RelationManager {
|
|||||||
* Cache management methods
|
* Cache management methods
|
||||||
*/
|
*/
|
||||||
_invalidateRelationCaches(srcId, relation, dstId) {
|
_invalidateRelationCaches(srcId, relation, dstId) {
|
||||||
|
// Any relation mutation invalidates the graph version so derived-result
|
||||||
|
// caches (ChainRule result cache) stamp/validate against it — a graph
|
||||||
|
// change can never serve a stale chain result.
|
||||||
|
if (this.arbiter && typeof this.arbiter._graphVersion === 'number') {
|
||||||
|
this.arbiter._graphVersion++;
|
||||||
|
}
|
||||||
this._caches.invalidateRelationCaches(srcId, relation, dstId);
|
this._caches.invalidateRelationCaches(srcId, relation, dstId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -747,7 +715,6 @@ export class RelationManager {
|
|||||||
|
|
||||||
_clearAllCaches() {
|
_clearAllCaches() {
|
||||||
this._caches.clearAll();
|
this._caches.clearAll();
|
||||||
this._relationCsr.clear();
|
|
||||||
this._relationGraph.clear();
|
this._relationGraph.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -90,38 +90,41 @@ export class UnifiedKeyManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const relationId = this._getRelationId(relation);
|
const relationId = this._getRelationId(relation);
|
||||||
return `${srcId}|${relationId}|${dstId}`;
|
// Rolling hash (no string concatenation). Exact-key caches recompute the
|
||||||
|
// key to delete; the direct-check cache additionally tracks keys by
|
||||||
|
// relation/node so relation-level invalidation does not need the
|
||||||
|
// pipe-delimited string form.
|
||||||
|
return this._rollingHash(srcId, relationId, dstId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create source-relation key
|
* Create source-relation key
|
||||||
* @param {number} srcId - Source ID
|
* @param {number} srcId - Source ID
|
||||||
* @param {string} relation - Relation name
|
* @param {string} relation - Relation name
|
||||||
* @returns {string} Composite key
|
* @returns {number} Rolling-hash key
|
||||||
*/
|
*/
|
||||||
createSrcRelKey(srcId, relation) {
|
createSrcRelKey(srcId, relation) {
|
||||||
if (srcId > this.options.maxSrcId) {
|
if (srcId > this.options.maxSrcId) {
|
||||||
throw new Error(`Source ID ${srcId} exceeds max range ${this.options.maxSrcId}`);
|
throw new Error(`Source ID ${srcId} exceeds max range ${this.options.maxSrcId}`);
|
||||||
}
|
}
|
||||||
const relationId = this._getRelationId(relation);
|
const relationId = this._getRelationId(relation);
|
||||||
// Direction marker: fromSrc and toDst lookups share the relation lookup
|
// Direction marker keeps fromSrc and toDst lookups distinct (identical
|
||||||
// cache, and identical `id|relId` keys made the second-direction lookup
|
// srcId|relationId pairs previously shared keys across directions).
|
||||||
// return the first direction's cached edges.
|
return this._rollingHash(srcId, relationId, 0x73); // 's'
|
||||||
return `${srcId}|${relationId}|s`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create destination-relation key
|
* Create destination-relation key
|
||||||
* @param {number} dstId - Destination ID
|
* @param {number} dstId - Destination ID
|
||||||
* @param {string} relation - Relation name
|
* @param {string} relation - Relation name
|
||||||
* @returns {number} Composite key
|
* @returns {number} Rolling-hash key
|
||||||
*/
|
*/
|
||||||
createDstRelKey(dstId, relation) {
|
createDstRelKey(dstId, relation) {
|
||||||
if (dstId > this.options.maxDstId) {
|
if (dstId > this.options.maxDstId) {
|
||||||
throw new Error(`Destination ID ${dstId} exceeds max range ${this.options.maxDstId}`);
|
throw new Error(`Destination ID ${dstId} exceeds max range ${this.options.maxDstId}`);
|
||||||
}
|
}
|
||||||
const relationId = this._getRelationId(relation);
|
const relationId = this._getRelationId(relation);
|
||||||
return `${dstId}|${relationId}|d`;
|
return this._rollingHash(dstId, relationId, 0x64); // 'd'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -129,7 +132,7 @@ export class UnifiedKeyManager {
|
|||||||
* @param {number} userId - User ID
|
* @param {number} userId - User ID
|
||||||
* @param {number} objectId - Object ID
|
* @param {number} objectId - Object ID
|
||||||
* @param {Array} steps - Chain steps
|
* @param {Array} steps - Chain steps
|
||||||
* @returns {number} Composite key
|
* @returns {number} Rolling-hash composite key
|
||||||
*/
|
*/
|
||||||
createChainKey(userId, objectId, steps) {
|
createChainKey(userId, objectId, steps) {
|
||||||
if (userId > this.options.maxSrcId) {
|
if (userId > this.options.maxSrcId) {
|
||||||
@@ -139,7 +142,79 @@ export class UnifiedKeyManager {
|
|||||||
throw new Error(`Object ID ${objectId} exceeds max range ${this.options.maxDstId}`);
|
throw new Error(`Object ID ${objectId} exceeds max range ${this.options.maxDstId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return JSON.stringify([userId, objectId, steps]);
|
// Rolling hash instead of JSON.stringify: no string allocation, no
|
||||||
|
// serialization of possibly-nested step configs. Steps may be strings,
|
||||||
|
// { relation, direction } objects, or { rule: <config>, conditionStep }
|
||||||
|
// objects — each feeds the hash incrementally.
|
||||||
|
return this._rollingHash(userId, objectId, steps);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 53-bit rolling hash over heterogeneous components (numbers, strings,
|
||||||
|
* booleans, arrays, nested objects). Dual 32-bit FNV-1a lanes combined into
|
||||||
|
* a single exact integer (< 2^53) usable as a Map key — no string
|
||||||
|
* concatenation or serialization on the hot path. Integers feed their
|
||||||
|
* high/low 32-bit halves; floats feed their exact 64-bit byte pattern;
|
||||||
|
* objects feed their sorted key/value pairs so ordering is stable.
|
||||||
|
* @returns {number} exact integer in [0, 2^53)
|
||||||
|
*/
|
||||||
|
_rollingHash(...parts) {
|
||||||
|
let h1 = 0x811c9dc5;
|
||||||
|
let h2 = 0x9e3779b1;
|
||||||
|
const floatBuf = new Float64Array(1);
|
||||||
|
const floatBytes = new Uint8Array(floatBuf.buffer);
|
||||||
|
const mix1 = (h, x) => Math.imul(h ^ x, 0x01000193) >>> 0;
|
||||||
|
const mix2 = (h, x) => Math.imul((h ^ x) ^ 0x5bd1e995, 0x01000193) >>> 0;
|
||||||
|
const feed = (v) => {
|
||||||
|
if (typeof v === 'number') {
|
||||||
|
if (Number.isInteger(v)) {
|
||||||
|
const hi = Math.floor(v / 0x100000000);
|
||||||
|
const lo = v >>> 0;
|
||||||
|
h1 = mix1(h1, hi);
|
||||||
|
h2 = mix2(h2, lo);
|
||||||
|
h1 = mix1(h1, lo);
|
||||||
|
h2 = mix2(h2, hi ^ 0x9e37);
|
||||||
|
} else {
|
||||||
|
floatBuf[0] = v;
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
h1 = mix1(h1, floatBytes[i]);
|
||||||
|
h2 = mix2(h2, floatBytes[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (typeof v === 'string') {
|
||||||
|
h1 = mix1(h1, v.length | 0x8000);
|
||||||
|
h2 = mix2(h2, v.length ^ 0x55aa);
|
||||||
|
for (let i = 0; i < v.length; i++) {
|
||||||
|
h1 = mix1(h1, v.charCodeAt(i));
|
||||||
|
h2 = mix2(h2, v.charCodeAt(i) ^ 0xa5);
|
||||||
|
}
|
||||||
|
} else if (typeof v === 'boolean') {
|
||||||
|
h1 = mix1(h1, v ? 0x1111 : 0x2222);
|
||||||
|
h2 = mix2(h2, v ? 0x3333 : 0x4444);
|
||||||
|
} else if (v === null) {
|
||||||
|
h1 = mix1(h1, 0xaaaa);
|
||||||
|
h2 = mix2(h2, 0xbbbb);
|
||||||
|
} else if (v === undefined) {
|
||||||
|
h1 = mix1(h1, 0xcccc);
|
||||||
|
h2 = mix2(h2, 0xdddd);
|
||||||
|
} else if (Array.isArray(v)) {
|
||||||
|
h1 = mix1(h1, v.length | 0x800000);
|
||||||
|
h2 = mix2(h2, v.length ^ 0x1234);
|
||||||
|
for (const x of v) feed(x);
|
||||||
|
} else if (typeof v === 'object') {
|
||||||
|
h1 = mix1(h1, 0x5eed);
|
||||||
|
h2 = mix2(h2, 0xcafe);
|
||||||
|
const keys = Object.keys(v).sort();
|
||||||
|
for (const k of keys) {
|
||||||
|
feed(k);
|
||||||
|
feed(v[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (const p of parts) feed(p);
|
||||||
|
// Combine both 32-bit lanes into an exact < 2^53 integer:
|
||||||
|
// h1 * 2^21 (h1 < 2^32 => product < 2^53) + top 21 bits of h2.
|
||||||
|
return h1 * 0x200000 + (h2 >>> 11);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1169,17 +1169,6 @@ export class ValueManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use aggregateCrispValues instead. Removal after Stage 2.
|
|
||||||
*/
|
|
||||||
aggregateBlurredValues(blurredValues, aggregator = 'max') {
|
|
||||||
if (!this._warnedAggregateBlurredValues) {
|
|
||||||
this._warnedAggregateBlurredValues = true;
|
|
||||||
console.warn('[ValueManager] aggregateBlurredValues is deprecated; use aggregateCrispValues instead.');
|
|
||||||
}
|
|
||||||
return this.aggregateCrispValues(blurredValues, aggregator);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compare two intervals using a comparator
|
* Compare two intervals using a comparator
|
||||||
* @param {Object} leftInterval - { min, max }
|
* @param {Object} leftInterval - { min, max }
|
||||||
|
|||||||
@@ -1,154 +0,0 @@
|
|||||||
export class RelationCSR {
|
|
||||||
constructor(manager) {
|
|
||||||
this.manager = manager;
|
|
||||||
const options = manager.arbiter?.options || {};
|
|
||||||
this.enabled = !!options.useRelationCsrIndex;
|
|
||||||
this.deltaThreshold = Number.isFinite(options.relationCsrDeltaThreshold)
|
|
||||||
? options.relationCsrDeltaThreshold
|
|
||||||
: 1000;
|
|
||||||
this.minDegree = Number.isFinite(options.relationCsrMinDegree)
|
|
||||||
? options.relationCsrMinDegree
|
|
||||||
: 200;
|
|
||||||
this.byName = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
buildIndex(relation) {
|
|
||||||
const relSet = this.manager.arbiter.indices?.relationsByRel?.get(relation);
|
|
||||||
const relations = relSet ? Array.from(relSet) : [];
|
|
||||||
const bySrcMap = new Map();
|
|
||||||
const byDstMap = new Map();
|
|
||||||
|
|
||||||
for (const rel of relations) {
|
|
||||||
let srcList = bySrcMap.get(rel.src);
|
|
||||||
if (!srcList) {
|
|
||||||
srcList = [];
|
|
||||||
bySrcMap.set(rel.src, srcList);
|
|
||||||
}
|
|
||||||
srcList.push(rel);
|
|
||||||
|
|
||||||
let dstList = byDstMap.get(rel.dst);
|
|
||||||
if (!dstList) {
|
|
||||||
dstList = [];
|
|
||||||
byDstMap.set(rel.dst, dstList);
|
|
||||||
}
|
|
||||||
dstList.push(rel);
|
|
||||||
}
|
|
||||||
|
|
||||||
const bySrcEntries = [];
|
|
||||||
const bySrcOffsets = new Map();
|
|
||||||
for (const [srcId, list] of bySrcMap.entries()) {
|
|
||||||
const start = bySrcEntries.length;
|
|
||||||
for (const rel of list) bySrcEntries.push(rel);
|
|
||||||
bySrcOffsets.set(srcId, { start, end: bySrcEntries.length });
|
|
||||||
}
|
|
||||||
|
|
||||||
const byDstEntries = [];
|
|
||||||
const byDstOffsets = new Map();
|
|
||||||
for (const [dstId, list] of byDstMap.entries()) {
|
|
||||||
const start = byDstEntries.length;
|
|
||||||
for (const rel of list) byDstEntries.push(rel);
|
|
||||||
byDstOffsets.set(dstId, { start, end: byDstEntries.length });
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
relation,
|
|
||||||
bySrcEntries,
|
|
||||||
bySrcOffsets,
|
|
||||||
byDstEntries,
|
|
||||||
byDstOffsets,
|
|
||||||
addDeltaBySrc: new Map(),
|
|
||||||
addDeltaByDst: new Map(),
|
|
||||||
removeDelta: new Set(),
|
|
||||||
deltaCount: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getIndex(relation) {
|
|
||||||
if (!this.enabled) return null;
|
|
||||||
let csr = this.byName.get(relation);
|
|
||||||
if (!csr || csr.deltaCount > this.deltaThreshold) {
|
|
||||||
csr = this.buildIndex(relation);
|
|
||||||
this.byName.set(relation, csr);
|
|
||||||
}
|
|
||||||
return csr;
|
|
||||||
}
|
|
||||||
|
|
||||||
recordAdd(relationObj) {
|
|
||||||
if (!this.enabled) return;
|
|
||||||
const csr = this.byName.get(relationObj.rel);
|
|
||||||
if (!csr) return;
|
|
||||||
const key = this.manager._makeRelationKey(relationObj.src, relationObj.rel, relationObj.dst);
|
|
||||||
if (csr.removeDelta.has(key)) {
|
|
||||||
csr.removeDelta.delete(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
let srcList = csr.addDeltaBySrc.get(relationObj.src);
|
|
||||||
if (!srcList) {
|
|
||||||
srcList = [];
|
|
||||||
csr.addDeltaBySrc.set(relationObj.src, srcList);
|
|
||||||
}
|
|
||||||
srcList.push(relationObj);
|
|
||||||
|
|
||||||
let dstList = csr.addDeltaByDst.get(relationObj.dst);
|
|
||||||
if (!dstList) {
|
|
||||||
dstList = [];
|
|
||||||
csr.addDeltaByDst.set(relationObj.dst, dstList);
|
|
||||||
}
|
|
||||||
dstList.push(relationObj);
|
|
||||||
|
|
||||||
csr.deltaCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
recordRemove(srcId, relation, dstId) {
|
|
||||||
if (!this.enabled) return;
|
|
||||||
const csr = this.byName.get(relation);
|
|
||||||
if (!csr) return;
|
|
||||||
const key = this.manager._makeRelationKey(srcId, relation, dstId);
|
|
||||||
csr.removeDelta.add(key);
|
|
||||||
csr.deltaCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
getRelationsFromCsr(csr, nodeId, reverse = false) {
|
|
||||||
const entries = reverse ? csr.byDstEntries : csr.bySrcEntries;
|
|
||||||
const offsets = reverse ? csr.byDstOffsets : csr.bySrcOffsets;
|
|
||||||
const deltaMap = reverse ? csr.addDeltaByDst : csr.addDeltaBySrc;
|
|
||||||
const range = offsets.get(nodeId);
|
|
||||||
const removeDelta = csr.removeDelta;
|
|
||||||
|
|
||||||
const result = [];
|
|
||||||
if (range) {
|
|
||||||
for (let i = range.start; i < range.end; i++) {
|
|
||||||
const rel = entries[i];
|
|
||||||
const key = this.manager._makeRelationKey(rel.src, rel.rel, rel.dst);
|
|
||||||
if (!removeDelta.has(key)) {
|
|
||||||
result.push(rel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const delta = deltaMap.get(nodeId);
|
|
||||||
if (delta && delta.length) {
|
|
||||||
for (const rel of delta) {
|
|
||||||
const key = this.manager._makeRelationKey(rel.src, rel.rel, rel.dst);
|
|
||||||
if (!removeDelta.has(key)) {
|
|
||||||
result.push(rel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
getRelationsForNode(nodeId, relation, reverse = false) {
|
|
||||||
if (!this.enabled) return null;
|
|
||||||
const degree = this.manager._getRelationDegreeFromIndices(nodeId, relation, reverse);
|
|
||||||
if (degree < this.minDegree) return null;
|
|
||||||
const csr = this.getIndex(relation);
|
|
||||||
if (!csr) return null;
|
|
||||||
return this.getRelationsFromCsr(csr, nodeId, reverse);
|
|
||||||
}
|
|
||||||
|
|
||||||
clear() {
|
|
||||||
this.byName.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -66,11 +66,11 @@ export class RelationCaches {
|
|||||||
}
|
}
|
||||||
|
|
||||||
makeValueCacheKeyBySrc(srcId, relationId) {
|
makeValueCacheKeyBySrc(srcId, relationId) {
|
||||||
return `${srcId}|${relationId}`;
|
return this.manager.arbiter.keyManager._rollingHash(srcId, relationId, 0x5352); // 'SR'
|
||||||
}
|
}
|
||||||
|
|
||||||
makeValueCacheKeyByDst(dstId, relationId) {
|
makeValueCacheKeyByDst(dstId, relationId) {
|
||||||
return `${dstId}|${relationId}`;
|
return this.manager.arbiter.keyManager._rollingHash(dstId, relationId, 0x4454); // 'DT'
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteFromCache(cache, key) {
|
deleteFromCache(cache, key) {
|
||||||
|
|||||||
@@ -47,10 +47,6 @@ export class RelationLookup {
|
|||||||
const persistent = this.manager.arbiter.indices.getRelationsFromSrc(srcId, relation);
|
const persistent = this.manager.arbiter.indices.getRelationsFromSrc(srcId, relation);
|
||||||
return this.manager._mergeRelationLists(persistent, partial);
|
return this.manager._mergeRelationLists(persistent, partial);
|
||||||
}
|
}
|
||||||
const csrResult = this.manager._relationCsr.getRelationsForNode(srcId, relation, false);
|
|
||||||
if (csrResult !== null) {
|
|
||||||
return csrResult;
|
|
||||||
}
|
|
||||||
// Try cache first for frequently accessed patterns - use numeric key
|
// Try cache first for frequently accessed patterns - use numeric key
|
||||||
const cacheKey = this.manager._makeSrcRelCacheKey(srcId, relation);
|
const cacheKey = this.manager._makeSrcRelCacheKey(srcId, relation);
|
||||||
const cached = this.manager._caches.relationLookupCache.get(cacheKey);
|
const cached = this.manager._caches.relationLookupCache.get(cacheKey);
|
||||||
@@ -84,10 +80,6 @@ export class RelationLookup {
|
|||||||
const persistent = this.manager.arbiter.indices.getRelationsToDst(dstId, relation);
|
const persistent = this.manager.arbiter.indices.getRelationsToDst(dstId, relation);
|
||||||
return this.manager._mergeRelationLists(persistent, partial);
|
return this.manager._mergeRelationLists(persistent, partial);
|
||||||
}
|
}
|
||||||
const csrResult = this.manager._relationCsr.getRelationsForNode(dstId, relation, true);
|
|
||||||
if (csrResult !== null) {
|
|
||||||
return csrResult;
|
|
||||||
}
|
|
||||||
// Try cache first - use numeric key
|
// Try cache first - use numeric key
|
||||||
const cacheKey = this.manager._makeDstRelCacheKey(dstId, relation);
|
const cacheKey = this.manager._makeDstRelCacheKey(dstId, relation);
|
||||||
const cached = this.manager._caches.relationLookupCache.get(cacheKey);
|
const cached = this.manager._caches.relationLookupCache.get(cacheKey);
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ export class RelationUpdates {
|
|||||||
this.manager.arbiter.indicesBuilt = true;
|
this.manager.arbiter.indicesBuilt = true;
|
||||||
|
|
||||||
this.manager._addRelationToGraph(srcId, relation, dstId);
|
this.manager._addRelationToGraph(srcId, relation, dstId);
|
||||||
this.manager._recordRelationCsrAdd(relationObj);
|
|
||||||
|
|
||||||
// Update PLTC indices incrementally (if initialized and not in batch mode)
|
// Update PLTC indices incrementally (if initialized and not in batch mode)
|
||||||
if (!this.manager.arbiter.batchUpdateInProgress) {
|
if (!this.manager.arbiter.batchUpdateInProgress) {
|
||||||
@@ -124,7 +123,6 @@ export class RelationUpdates {
|
|||||||
this.manager._invalidateValueRelationCaches(srcId, dstId, relationName);
|
this.manager._invalidateValueRelationCaches(srcId, dstId, relationName);
|
||||||
|
|
||||||
this.manager._removeRelationFromGraph(srcId, relationName, dstId);
|
this.manager._removeRelationFromGraph(srcId, relationName, dstId);
|
||||||
this.manager._recordRelationCsrRemove(srcId, relationName, dstId);
|
|
||||||
|
|
||||||
// Notify ValueManager about the removed relation's last state
|
// Notify ValueManager about the removed relation's last state
|
||||||
if (relationObjectToRemove.value !== undefined && relationObjectToRemove.stateId) {
|
if (relationObjectToRemove.value !== undefined && relationObjectToRemove.stateId) {
|
||||||
@@ -368,7 +366,6 @@ export class RelationUpdates {
|
|||||||
this.manager.arbiter.indicesBuilt = true;
|
this.manager.arbiter.indicesBuilt = true;
|
||||||
|
|
||||||
this.manager._addRelationToGraph(srcId, relation, dstId);
|
this.manager._addRelationToGraph(srcId, relation, dstId);
|
||||||
this.manager._recordRelationCsrAdd(relationObj);
|
|
||||||
|
|
||||||
// Update PLTC indices incrementally (if initialized and not in batch mode)
|
// Update PLTC indices incrementally (if initialized and not in batch mode)
|
||||||
// For updates, only update if it's a new relation (not an existing one being modified)
|
// For updates, only update if it's a new relation (not an existing one being modified)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export { Arbiter } from './core/Arbiter.js';
|
export { Arbiter } from './core/Arbiter.js';
|
||||||
|
export { PartialGraphContext } from './core/PartialGraphContext.js';
|
||||||
export { GraphIndices } from './core/GraphIndices.js';
|
export { GraphIndices } from './core/GraphIndices.js';
|
||||||
export { NodeManager } from './core/NodeManager.js';
|
export { NodeManager } from './core/NodeManager.js';
|
||||||
export { RelationManager } from './core/RelationManager.js';
|
export { RelationManager } from './core/RelationManager.js';
|
||||||
|
|||||||
@@ -1,165 +0,0 @@
|
|||||||
/**
|
|
||||||
* Optimized IP Address Utilities
|
|
||||||
*
|
|
||||||
* High-performance versions for hot paths
|
|
||||||
*/
|
|
||||||
|
|
||||||
// CIDR cache for repeated lookups
|
|
||||||
const cidrCache = new Map();
|
|
||||||
const CIDR_CACHE_SIZE = 1000;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fast IPv4 check - less strict but much faster
|
|
||||||
* Only validates format, not strict numeric ranges
|
|
||||||
*/
|
|
||||||
export function isIPv4Fast(ip) {
|
|
||||||
if (typeof ip !== 'string') return false;
|
|
||||||
|
|
||||||
// Quick length check (min: 7 for "0.0.0.0", max: 15 for "255.255.255.255")
|
|
||||||
if (ip.length < 7 || ip.length > 15) return false;
|
|
||||||
|
|
||||||
let dots = 0;
|
|
||||||
for (let i = 0; i < ip.length; i++) {
|
|
||||||
const c = ip.charCodeAt(i);
|
|
||||||
if (c === 46) { // '.'
|
|
||||||
dots++;
|
|
||||||
} else if (c < 48 || c > 57) { // not 0-9
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dots === 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ultra-fast IP to integer conversion
|
|
||||||
* Direct character parsing, no string splitting
|
|
||||||
*/
|
|
||||||
export function ipToIntFast(ip) {
|
|
||||||
let result = 0;
|
|
||||||
let octet = 0;
|
|
||||||
let shift = 24;
|
|
||||||
|
|
||||||
for (let i = 0; i < ip.length; i++) {
|
|
||||||
const c = ip.charCodeAt(i);
|
|
||||||
if (c === 46) { // '.'
|
|
||||||
result |= (octet << shift);
|
|
||||||
octet = 0;
|
|
||||||
shift -= 8;
|
|
||||||
} else {
|
|
||||||
octet = octet * 10 + (c - 48);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (result | octet) >>> 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fast CIDR parsing with caching
|
|
||||||
*/
|
|
||||||
export function parseCidrCached(cidr) {
|
|
||||||
// Check cache first
|
|
||||||
let cached = cidrCache.get(cidr);
|
|
||||||
if (cached) return cached;
|
|
||||||
|
|
||||||
// Parse and cache
|
|
||||||
const slashIdx = cidr.indexOf('/');
|
|
||||||
if (slashIdx === -1) return null;
|
|
||||||
|
|
||||||
const ip = cidr.slice(0, slashIdx);
|
|
||||||
const prefix = parseInt(cidr.slice(slashIdx + 1), 10);
|
|
||||||
const mask = -1 << (32 - prefix);
|
|
||||||
|
|
||||||
cached = {
|
|
||||||
network: ipToIntFast(ip),
|
|
||||||
mask,
|
|
||||||
prefix
|
|
||||||
};
|
|
||||||
|
|
||||||
// Simple LRU - clear if too big
|
|
||||||
if (cidrCache.size >= CIDR_CACHE_SIZE) {
|
|
||||||
cidrCache.clear();
|
|
||||||
}
|
|
||||||
cidrCache.set(cidr, cached);
|
|
||||||
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ultra-fast IP in CIDR check
|
|
||||||
* Uses caching and optimized parsing
|
|
||||||
*/
|
|
||||||
export function isIpInCidrFast(ip, cidr) {
|
|
||||||
const cached = parseCidrCached(cidr);
|
|
||||||
if (!cached) return false;
|
|
||||||
|
|
||||||
const ipInt = ipToIntFast(ip);
|
|
||||||
return (ipInt & cached.mask) === (cached.network & cached.mask);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fast private IP check using bit manipulation
|
|
||||||
*/
|
|
||||||
export function isPrivateIpFast(ip) {
|
|
||||||
const ipInt = ipToIntFast(ip);
|
|
||||||
|
|
||||||
// 10.0.0.0/8: 0x0A000000 to 0x0AFFFFFF
|
|
||||||
if ((ipInt >>> 24) === 10) return true;
|
|
||||||
|
|
||||||
// 172.16.0.0/12: 0xAC100000 to 0xAC1FFFFF
|
|
||||||
const high16 = ipInt >>> 16;
|
|
||||||
if (high16 >= 0xAC10 && high16 <= 0xAC1F) return true;
|
|
||||||
|
|
||||||
// 192.168.0.0/16: 0xC0A80000 to 0xC0A8FFFF
|
|
||||||
if (high16 === 0xC0A8) return true;
|
|
||||||
|
|
||||||
// 127.0.0.0/8: 0x7F000000 to 0x7FFFFFFF
|
|
||||||
if ((ipInt >>> 24) === 127) return true;
|
|
||||||
|
|
||||||
// 169.254.0.0/16: 0xA9FE0000 to 0xA9FEFFFF
|
|
||||||
if (high16 === 0xA9FE) return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optimized built-in function evaluator
|
|
||||||
* Direct dispatch without object lookups
|
|
||||||
*/
|
|
||||||
export function evaluateBuiltInFast(name, args) {
|
|
||||||
switch (name) {
|
|
||||||
case 'ip_in_cidr':
|
|
||||||
return isIpInCidrFast(args[0], args[1]);
|
|
||||||
case 'ip_is_private':
|
|
||||||
return isPrivateIpFast(args[0]);
|
|
||||||
case 'ip_is_loopback':
|
|
||||||
return (ipToIntFast(args[0]) >>> 24) === 127;
|
|
||||||
case 'ip_version':
|
|
||||||
return isIPv4Fast(args[0]) ? 4 : (args[0].includes(':') ? 6 : null);
|
|
||||||
case 'ip_equals':
|
|
||||||
return args[0] === args[1];
|
|
||||||
case 'contains':
|
|
||||||
return String(args[0]).includes(String(args[1]));
|
|
||||||
case 'starts_with':
|
|
||||||
return String(args[0]).startsWith(String(args[1]));
|
|
||||||
case 'ends_with':
|
|
||||||
return String(args[0]).endsWith(String(args[1]));
|
|
||||||
case 'equals':
|
|
||||||
return args[0] === args[1];
|
|
||||||
case 'greater_than':
|
|
||||||
return args[0] > args[1];
|
|
||||||
case 'less_than':
|
|
||||||
return args[0] < args[1];
|
|
||||||
case 'in_range':
|
|
||||||
return args[0] >= args[1] && args[0] <= args[2];
|
|
||||||
case 'hour_of_day':
|
|
||||||
return new Date(args[0]).getHours();
|
|
||||||
case 'day_of_week':
|
|
||||||
return new Date(args[0]).getDay();
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown: ${name}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-export original functions for compatibility
|
|
||||||
export { isIPv4, isIPv6, isLoopbackIp, getIpVersion, normalizeIp } from './ip-utils.js';
|
|
||||||
export { isIpInCidr, isPrivateIp } from './ip-utils.js';
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
/**
|
|
||||||
* IP Address Utilities
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { createRequire } from 'node:module';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if string is IPv4
|
|
||||||
*/
|
|
||||||
export function isIPv4(ip) {
|
|
||||||
if (typeof ip !== 'string') return false;
|
|
||||||
const parts = ip.split('.');
|
|
||||||
if (parts.length !== 4) return false;
|
|
||||||
|
|
||||||
return parts.every(part => {
|
|
||||||
const num = parseInt(part, 10);
|
|
||||||
return !isNaN(num) && num >= 0 && num <= 255 && part === String(num);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if string is IPv6
|
|
||||||
*/
|
|
||||||
export function isIPv6(ip) {
|
|
||||||
if (typeof ip !== 'string') return false;
|
|
||||||
// Simple check - contains colons and valid hex
|
|
||||||
return ip.includes(':') && /^[0-9a-fA-F:]+$/.test(ip);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if IP is in private range (RFC 1918)
|
|
||||||
*/
|
|
||||||
export function isPrivateIp(ip) {
|
|
||||||
if (!isIPv4(ip)) return false;
|
|
||||||
|
|
||||||
const parts = ip.split('.').map(Number);
|
|
||||||
const [a, b, c, d] = parts;
|
|
||||||
|
|
||||||
// 10.0.0.0/8
|
|
||||||
if (a === 10) return true;
|
|
||||||
|
|
||||||
// 172.16.0.0/12
|
|
||||||
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
||||||
|
|
||||||
// 192.168.0.0/16
|
|
||||||
if (a === 192 && b === 168) return true;
|
|
||||||
|
|
||||||
// 127.0.0.0/8 (loopback)
|
|
||||||
if (a === 127) return true;
|
|
||||||
|
|
||||||
// 169.254.0.0/16 (link-local)
|
|
||||||
if (a === 169 && b === 254) return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if IP is loopback
|
|
||||||
*/
|
|
||||||
export function isLoopbackIp(ip) {
|
|
||||||
if (!isIPv4(ip)) return false;
|
|
||||||
return ip.startsWith('127.');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert IP to integer for range comparison
|
|
||||||
*/
|
|
||||||
export function ipToInt(ip) {
|
|
||||||
return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse CIDR notation
|
|
||||||
*/
|
|
||||||
export function parseCidr(cidr) {
|
|
||||||
const [ip, prefix] = cidr.split('/');
|
|
||||||
const mask = -1 << (32 - parseInt(prefix, 10));
|
|
||||||
return { ip: ipToInt(ip), mask };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if IP is in CIDR range
|
|
||||||
*/
|
|
||||||
export function isIpInCidr(ip, cidr) {
|
|
||||||
if (!isIPv4(ip)) return false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const ipInt = ipToInt(ip);
|
|
||||||
const { ip: networkInt, mask } = parseCidr(cidr);
|
|
||||||
|
|
||||||
return (ipInt & mask) === (networkInt & mask);
|
|
||||||
} catch (err) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if two IPs are equal
|
|
||||||
*/
|
|
||||||
export function ipEquals(ip1, ip2) {
|
|
||||||
return ip1 === ip2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get IP version (4 or 6)
|
|
||||||
*/
|
|
||||||
export function getIpVersion(ip) {
|
|
||||||
if (isIPv4(ip)) return 4;
|
|
||||||
if (isIPv6(ip)) return 6;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize IP (remove leading zeros, etc.)
|
|
||||||
*/
|
|
||||||
export function normalizeIp(ip) {
|
|
||||||
if (!isIPv4(ip)) return ip;
|
|
||||||
|
|
||||||
return ip
|
|
||||||
.split('.')
|
|
||||||
.map(part => parseInt(part, 10).toString())
|
|
||||||
.join('.');
|
|
||||||
}
|
|
||||||
@@ -1,265 +0,0 @@
|
|||||||
import { describe, test } from 'node:test';
|
|
||||||
import assert from 'node:assert/strict';
|
|
||||||
import { DSLCompiler } from '../../src/ast/DSLCompiler.js';
|
|
||||||
import { parse } from '../../src/ast/parser/GeneratedParser.js';
|
|
||||||
import { RuleGenerator } from '../../src/ast/generator/RuleGenerator.js';
|
|
||||||
|
|
||||||
function createMockArbiter() {
|
|
||||||
const relationConfigs = new Map();
|
|
||||||
return {
|
|
||||||
relationConfigs,
|
|
||||||
setRelationConfig(relation, config) {
|
|
||||||
relationConfigs.set(relation, config);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('DSL Compiler', () => {
|
|
||||||
const arbiter = createMockArbiter();
|
|
||||||
const compiler = new DSLCompiler(arbiter);
|
|
||||||
|
|
||||||
test('Basic parsing', () => {
|
|
||||||
const dsl = `
|
|
||||||
definition Employee {
|
|
||||||
role: string
|
|
||||||
isActive: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasRole(user: Employee, role: string)
|
|
||||||
|
|
||||||
evidence canRead(user: Employee, doc: Account) {
|
|
||||||
hasRole(user, 'admin')
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const result = compiler.compile(dsl, 'test-basic');
|
|
||||||
assert.ok(result.success, 'Basic parsing should succeed');
|
|
||||||
assert.ok(result.program !== null, 'Program should be created');
|
|
||||||
assert.ok(result.generatedRules.size > 0, 'Rules should be generated');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Basic compilation', () => {
|
|
||||||
const dsl = `
|
|
||||||
definition Employee {
|
|
||||||
role: string
|
|
||||||
isActive: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasRole(user: Employee, role: string) CACHE eager
|
|
||||||
|
|
||||||
evidence canRead(user: Employee, doc: Account) {
|
|
||||||
hasRole(user, 'admin')
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const result = compiler.compile(dsl, 'test-compilation');
|
|
||||||
assert.ok(result.success, 'Basic compilation should succeed');
|
|
||||||
assert.ok(result.generatedRules.has('canRead'), 'canRead rule should be generated');
|
|
||||||
|
|
||||||
const canReadConfig = result.generatedRules.get('canRead');
|
|
||||||
assert.ok(canReadConfig.type === 'direct', 'canRead should be direct rule');
|
|
||||||
assert.ok(canReadConfig.relation === 'hasRole', 'canRead should use hasRole relation');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Complex DSL compilation', () => {
|
|
||||||
const dsl = `
|
|
||||||
definition Employee {
|
|
||||||
role: string
|
|
||||||
isActive: boolean
|
|
||||||
clearance: string BEHAVES {
|
|
||||||
blurring adaptive confidence_95
|
|
||||||
} CACHE eager
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasRole(user: Employee, role: string) CACHE eager
|
|
||||||
fact isMember(user: Employee, group: Device) transitive CACHE lazy
|
|
||||||
fact owns(user: Employee, doc: Account) CACHE eager
|
|
||||||
|
|
||||||
evidence canRead(user: Employee, doc: Account) {
|
|
||||||
owns(user, doc)
|
|
||||||
hasRole(user, 'admin')
|
|
||||||
}
|
|
||||||
|
|
||||||
evidence canAccessCritical(user: Employee, resource: AuthSession) {
|
|
||||||
fusion min {
|
|
||||||
hasRole(user, 'admin'),
|
|
||||||
hasRole(user, 'superadmin')
|
|
||||||
}
|
|
||||||
|
|
||||||
fusion max {
|
|
||||||
hasRole(user, 'admin'),
|
|
||||||
hasRole(user, 'secret')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const result = compiler.compile(dsl, 'test-complex');
|
|
||||||
assert.ok(result.success, 'Complex DSL compilation should succeed');
|
|
||||||
assert.ok(result.generatedRules.has('canRead'), 'canRead rule should be generated');
|
|
||||||
assert.ok(result.generatedRules.has('canAccessCritical'), 'canAccessCritical rule should be generated');
|
|
||||||
|
|
||||||
const canReadConfig = result.generatedRules.get('canRead');
|
|
||||||
assert.ok(canReadConfig.type === 'logical', 'canRead should be logical rule');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Error handling', () => {
|
|
||||||
const invalidDSL = `
|
|
||||||
definition Employee {
|
|
||||||
role: string
|
|
||||||
// Missing closing brace
|
|
||||||
|
|
||||||
fact hasRole(user: Employee, role: string)
|
|
||||||
// Missing semicolon
|
|
||||||
|
|
||||||
evidence canRead(user: Employee, doc: Account) {
|
|
||||||
// Invalid syntax
|
|
||||||
invalid syntax here
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const result = compiler.compile(invalidDSL, 'test-error');
|
|
||||||
assert.ok(!result.success, 'Invalid DSL should fail');
|
|
||||||
assert.ok(result.errors.length > 0, 'Should have error messages');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Program management', () => {
|
|
||||||
const dsl1 = `
|
|
||||||
definition Employee {
|
|
||||||
role: string
|
|
||||||
isActive: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasRole(user: Employee, role: string)
|
|
||||||
evidence canRead(user: Employee, doc: Account) {
|
|
||||||
hasRole(user, 'admin')
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const dsl2 = `
|
|
||||||
definition Employee {
|
|
||||||
role: string
|
|
||||||
isActive: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasBalance(user: Employee, amount: number)
|
|
||||||
evidence canWithdraw(user: Employee, amount: number) {
|
|
||||||
hasBalance(user, amount)
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const result1 = compiler.compile(dsl1, 'test-auth');
|
|
||||||
assert.ok(result1.success, 'First program should compile');
|
|
||||||
|
|
||||||
const programs = { 'auth': dsl1, 'finance': dsl2 };
|
|
||||||
const result2 = compiler.compileMultiple(programs);
|
|
||||||
assert.ok(result2.success, 'Multiple programs should compile');
|
|
||||||
|
|
||||||
const authProgram = compiler.getCompiledProgram('test-auth');
|
|
||||||
assert.ok(authProgram !== null, 'Should retrieve compiled program');
|
|
||||||
|
|
||||||
const removed = compiler.removeCompiledProgram('test-auth');
|
|
||||||
assert.ok(removed, 'Should remove program');
|
|
||||||
|
|
||||||
compiler.clearCompiledPrograms();
|
|
||||||
const allPrograms = compiler.getAllCompiledPrograms();
|
|
||||||
assert.ok(allPrograms.size === 0, 'Should clear all programs');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Validation', () => {
|
|
||||||
const validDSL = `
|
|
||||||
definition Employee {
|
|
||||||
role: string
|
|
||||||
isActive: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasRole(user: Employee, role: string)
|
|
||||||
|
|
||||||
evidence canRead(user: Employee, doc: Account) {
|
|
||||||
hasRole(user, 'admin')
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const invalidDSL = `
|
|
||||||
definition Employee {
|
|
||||||
role: string
|
|
||||||
// Missing closing brace
|
|
||||||
|
|
||||||
fact hasRole(user: Employee, role: string)
|
|
||||||
// Missing semicolon
|
|
||||||
`;
|
|
||||||
|
|
||||||
const validResult = compiler.validate(validDSL);
|
|
||||||
assert.ok(validResult.success, 'Valid DSL should pass validation');
|
|
||||||
|
|
||||||
const invalidResult = compiler.validate(invalidDSL);
|
|
||||||
assert.ok(!invalidResult.success, 'Invalid DSL should fail validation');
|
|
||||||
assert.ok(invalidResult.errors.length > 0, 'Should have validation errors');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Rule generation', () => {
|
|
||||||
const dsl = `
|
|
||||||
definition Employee {
|
|
||||||
role: string
|
|
||||||
isActive: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasRole(user: Employee, role: string) CACHE eager
|
|
||||||
fact isMember(user: Employee, group: Device) transitive CACHE lazy
|
|
||||||
|
|
||||||
evidence canRead(user: Employee, doc: Account) {
|
|
||||||
hasRole(user, 'admin')
|
|
||||||
}
|
|
||||||
|
|
||||||
evidence canAccess(user: Employee, doc: Account) {
|
|
||||||
hasRole(user, 'reader')
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const result = compiler.compile(dsl, 'test-rules');
|
|
||||||
assert.ok(result.success, 'Rule generation should succeed');
|
|
||||||
|
|
||||||
const canReadConfig = result.generatedRules.get('canRead');
|
|
||||||
assert.ok(canReadConfig.type === 'direct', 'canRead should be direct rule');
|
|
||||||
|
|
||||||
const canAccessConfig = result.generatedRules.get('canAccess');
|
|
||||||
assert.ok(canAccessConfig.type === 'direct', 'canAccess should be direct rule');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Multiple programs', () => {
|
|
||||||
const programs = {
|
|
||||||
'auth': `
|
|
||||||
definition Employee {
|
|
||||||
role: string
|
|
||||||
isActive: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasRole(user: Employee, role: string)
|
|
||||||
evidence canRead(user: Employee, doc: Account) {
|
|
||||||
hasRole(user, 'admin')
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
'finance': `
|
|
||||||
definition Employee {
|
|
||||||
role: string
|
|
||||||
isActive: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasBalance(user: Employee, amount: number)
|
|
||||||
evidence canWithdraw(user: Employee, amount: number) {
|
|
||||||
hasBalance(user, amount)
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
'invalid': `
|
|
||||||
// Invalid syntax
|
|
||||||
invalid syntax here
|
|
||||||
`
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = compiler.compileMultiple(programs);
|
|
||||||
assert.ok(!result.success, 'Should fail due to invalid program');
|
|
||||||
assert.ok(result.errors.length > 0, 'Should have errors');
|
|
||||||
assert.ok(result.results.auth.success, 'Auth program should succeed');
|
|
||||||
assert.ok(result.results.finance.success, 'Finance program should succeed');
|
|
||||||
assert.ok(!result.results.invalid.success, 'Invalid program should fail');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,283 +0,0 @@
|
|||||||
import { describe, test } from 'node:test';
|
|
||||||
import assert from 'node:assert/strict';
|
|
||||||
import { DSLCompiler } from '../../src/ast/DSLCompiler.js';
|
|
||||||
|
|
||||||
function createMockArbiter() {
|
|
||||||
const relationConfigs = new Map();
|
|
||||||
return {
|
|
||||||
relationConfigs,
|
|
||||||
setRelationConfig(relation, config) {
|
|
||||||
relationConfigs.set(relation, config);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Type Definitions', () => {
|
|
||||||
const arbiter = createMockArbiter();
|
|
||||||
const compiler = new DSLCompiler(arbiter);
|
|
||||||
|
|
||||||
test('Basic definitions', () => {
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `definition User { role: string }`,
|
|
||||||
description: 'Simple definition with one field'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
role: string
|
|
||||||
isActive: boolean
|
|
||||||
}`,
|
|
||||||
description: 'Definition with multiple fields'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition Group {
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
created: timestamp
|
|
||||||
}`,
|
|
||||||
description: 'Definition with different field types'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
const result = compiler.compile(input, `test-basic-def-${Date.now()}`);
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
assert.ok(result.program.definitions.length > 0, 'Should have definitions');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Field types', () => {
|
|
||||||
const testCases = [
|
|
||||||
{ type: 'string', description: 'String field type' },
|
|
||||||
{ type: 'number', description: 'Number field type' },
|
|
||||||
{ type: 'boolean', description: 'Boolean field type' },
|
|
||||||
{ type: 'timestamp', description: 'Timestamp field type' },
|
|
||||||
{ type: 'User', description: 'Custom type field' },
|
|
||||||
{ type: 'Permission', description: 'Another custom type field' }
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ type, description }) => {
|
|
||||||
const dsl = `definition Test { field: ${type} }`;
|
|
||||||
const result = compiler.compile(dsl, `test-field-type-${Date.now()}`);
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Array types', () => {
|
|
||||||
const testCases = [
|
|
||||||
{ type: 'string[]', description: 'String array' },
|
|
||||||
{ type: 'number[]', description: 'Number array' },
|
|
||||||
{ type: 'boolean[]', description: 'Boolean array' },
|
|
||||||
{ type: 'Permission[]', description: 'Custom type array' },
|
|
||||||
{ type: 'User[]', description: 'User array' }
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ type, description }) => {
|
|
||||||
const dsl = `definition Test { items: ${type} }`;
|
|
||||||
const result = compiler.compile(dsl, `test-array-${Date.now()}`);
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Behaviors', () => {
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
balance: number BEHAVES { decaying down hourly }
|
|
||||||
}`,
|
|
||||||
description: 'Decay behavior - down hourly'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
reputation: number BEHAVES { decaying up daily }
|
|
||||||
}`,
|
|
||||||
description: 'Decay behavior - up daily'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
score: number BEHAVES { decaying neutral weekly }
|
|
||||||
}`,
|
|
||||||
description: 'Decay behavior - neutral weekly'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
stability: number BEHAVES { decaying stable monthly }
|
|
||||||
}`,
|
|
||||||
description: 'Decay behavior - stable monthly'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
confidence: number BEHAVES { blurring fixed }
|
|
||||||
}`,
|
|
||||||
description: 'Blur behavior - fixed'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
accuracy: number BEHAVES { blurring adaptive }
|
|
||||||
}`,
|
|
||||||
description: 'Blur behavior - adaptive'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
precision: number BEHAVES { blurring confidence confidence_90 }
|
|
||||||
}`,
|
|
||||||
description: 'Blur behavior - confidence with level'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
session: string BEHAVES { ttl 1h }
|
|
||||||
}`,
|
|
||||||
description: 'TTL behavior - hours'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
token: string BEHAVES { ttl 24h }
|
|
||||||
}`,
|
|
||||||
description: 'TTL behavior - 24 hours'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
cache: string BEHAVES { ttl 7d }
|
|
||||||
}`,
|
|
||||||
description: 'TTL behavior - days'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
const result = compiler.compile(input, `test-behavior-${Date.now()}`);
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Caching', () => {
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
role: string CACHE eager
|
|
||||||
}`,
|
|
||||||
description: 'Eager caching'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
score: number CACHE lazy
|
|
||||||
}`,
|
|
||||||
description: 'Lazy caching'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
balance: number BEHAVES { decaying down hourly } CACHE eager
|
|
||||||
}`,
|
|
||||||
description: 'Behavior with eager caching'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
reputation: number BEHAVES { blurring adaptive } CACHE lazy
|
|
||||||
}`,
|
|
||||||
description: 'Behavior with lazy caching'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
const result = compiler.compile(input, `test-cache-${Date.now()}`);
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Complex definitions', () => {
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `definition User {
|
|
||||||
role: string
|
|
||||||
isActive: boolean
|
|
||||||
lastActive: timestamp BEHAVES {
|
|
||||||
decaying down hourly
|
|
||||||
} CACHE lazy
|
|
||||||
isSuspended: boolean
|
|
||||||
balance: number BEHAVES {
|
|
||||||
decaying down hourly
|
|
||||||
} CACHE eager
|
|
||||||
score: number BEHAVES {
|
|
||||||
blurring adaptive confidence_95
|
|
||||||
} CACHE lazy
|
|
||||||
session: string BEHAVES {
|
|
||||||
ttl 24h
|
|
||||||
} CACHE eager
|
|
||||||
}`,
|
|
||||||
description: 'Complex definition with multiple behaviors and caching'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition Group {
|
|
||||||
name: string
|
|
||||||
permissions: Permission[]
|
|
||||||
members: User[]
|
|
||||||
created: timestamp BEHAVES {
|
|
||||||
decaying stable monthly
|
|
||||||
} CACHE lazy
|
|
||||||
isPublic: boolean CACHE eager
|
|
||||||
}`,
|
|
||||||
description: 'Definition with arrays and mixed behaviors'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition Document {
|
|
||||||
level: string
|
|
||||||
owner: User
|
|
||||||
tags: string[]
|
|
||||||
content: string BEHAVES {
|
|
||||||
blurring fixed
|
|
||||||
} CACHE lazy
|
|
||||||
accessCount: number BEHAVES {
|
|
||||||
decaying up daily
|
|
||||||
} CACHE eager
|
|
||||||
expiresAt: timestamp BEHAVES {
|
|
||||||
ttl 30d
|
|
||||||
} CACHE eager
|
|
||||||
}`,
|
|
||||||
description: 'Definition with all behavior types'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
const result = compiler.compile(input, `test-complex-def-${Date.now()}`);
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
assert.ok(result.program.definitions.length > 0, 'Should have definitions');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Definition error handling', () => {
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `definition User { role: string`,
|
|
||||||
description: 'Missing closing brace should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User { role: }`,
|
|
||||||
description: 'Missing field type should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User { : string }`,
|
|
||||||
description: 'Missing field name should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User { role: string BEHAVES { }`,
|
|
||||||
description: 'Incomplete behavior should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User { role: string CACHE }`,
|
|
||||||
description: 'Incomplete cache directive should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `definition User { role: string BEHAVES { invalid } }`,
|
|
||||||
description: 'Invalid behavior should fail'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const result = compiler.compile(input, `test-def-error-${Date.now()}`);
|
|
||||||
assert.ok(!result.success, `${description} should fail to parse`);
|
|
||||||
} catch {
|
|
||||||
// Expected to fail
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,374 +0,0 @@
|
|||||||
import { describe, test } from 'node:test';
|
|
||||||
import assert from 'node:assert/strict';
|
|
||||||
import { DSLCompiler } from '../../src/ast/DSLCompiler.js';
|
|
||||||
|
|
||||||
function createMockArbiter() {
|
|
||||||
const relationConfigs = new Map();
|
|
||||||
return {
|
|
||||||
relationConfigs,
|
|
||||||
setRelationConfig(relation, config) {
|
|
||||||
relationConfigs.set(relation, config);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Evidence Rules', () => {
|
|
||||||
const arbiter = createMockArbiter();
|
|
||||||
const compiler = new DSLCompiler(arbiter);
|
|
||||||
|
|
||||||
test('Basic evidence', () => {
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `evidence canRead(user: User, doc: Document) {
|
|
||||||
hasRole(user, 'admin')
|
|
||||||
}`,
|
|
||||||
description: 'Simple evidence with function call'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canAccess(user: User, resource: Resource) {
|
|
||||||
user.isActive
|
|
||||||
}`,
|
|
||||||
description: 'Evidence with attribute access'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canModify(user: User, doc: Document) {
|
|
||||||
user.isActive
|
|
||||||
hasRole(user, 'admin')
|
|
||||||
}`,
|
|
||||||
description: 'Evidence with multiple conditions'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canDelete(user: User, doc: Document) {
|
|
||||||
owns(user, doc)
|
|
||||||
user.isActive
|
|
||||||
}`,
|
|
||||||
description: 'Evidence with ownership and status'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
const result = compiler.compile(input, `test-basic-evidence-${Date.now()}`);
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
assert.ok(result.program.evidence.length > 0, 'Should have evidence');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Defeasible logic', () => {
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `evidence canAccess(user: User, resource: Resource) {
|
|
||||||
ALWAYS user.isActive
|
|
||||||
}`,
|
|
||||||
description: 'ALWAYS rule - strict requirement'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canAccess(user: User, resource: Resource) {
|
|
||||||
WHEN hasRole(user, 'admin')
|
|
||||||
}`,
|
|
||||||
description: 'WHEN rule - defeasible condition'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canAccess(user: User, resource: Resource) {
|
|
||||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
|
||||||
}`,
|
|
||||||
description: 'WHEN/UNLESS rule - defeasible with defeater'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canAccess(user: User, resource: Resource) {
|
|
||||||
REQUIRES hasClearance(user, resource.level)
|
|
||||||
}`,
|
|
||||||
description: 'REQUIRES rule - inverse defeater'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canAccessCritical(user: User, resource: Resource) {
|
|
||||||
ALWAYS user.isActive
|
|
||||||
|
|
||||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
|
||||||
|
|
||||||
REQUIRES hasClearance(user, resource.level)
|
|
||||||
}`,
|
|
||||||
description: 'Complex defeasible logic with all rule types'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canAccessSensitive(user: User, doc: Document) {
|
|
||||||
ALWAYS user.isActive
|
|
||||||
|
|
||||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
|
||||||
|
|
||||||
REQUIRES hasClearance(user, doc.level)
|
|
||||||
|
|
||||||
fusion majority {
|
|
||||||
user.isTrusted
|
|
||||||
user.hasRecentActivity
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
description: 'Defeasible logic with fusion'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
const result = compiler.compile(input, `test-defeasible-${Date.now()}`);
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Pattern matching', () => {
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `evidence canRead(user: User, doc: Document) {
|
|
||||||
isMember(user, *group) {
|
|
||||||
canRead(group, doc)
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
description: 'Basic pattern matching with wildcard'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canRead(user: User, doc: Document) {
|
|
||||||
isMember(user, *group) {
|
|
||||||
canRead(group, doc)
|
|
||||||
} limit 5
|
|
||||||
}`,
|
|
||||||
description: 'Pattern matching with limit'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canRead(user: User, doc: Document) {
|
|
||||||
similar(doc, *similar) |similarity| {
|
|
||||||
canRead(user, similar)
|
|
||||||
} with similarity > 0.7
|
|
||||||
}`,
|
|
||||||
description: 'Pattern matching with binding and condition'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canRead(user: User, doc: Document) {
|
|
||||||
similar(doc, *similar) |similarity| {
|
|
||||||
canRead(user, similar)
|
|
||||||
} with similarity > 0.7 limit 5
|
|
||||||
}`,
|
|
||||||
description: 'Pattern matching with binding, condition, and limit'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canRead(user: User, doc: Document) {
|
|
||||||
isMember(user, *group) {
|
|
||||||
isMember(group, *parentGroup) {
|
|
||||||
canRead(parentGroup, doc)
|
|
||||||
} limit 2
|
|
||||||
} limit 3
|
|
||||||
}`,
|
|
||||||
description: 'Nested pattern matching'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canRead(user: User, doc: Document) {
|
|
||||||
isFriend(user, *friend) {
|
|
||||||
isMember(friend, *group) {
|
|
||||||
canRead(group, doc)
|
|
||||||
} limit 1
|
|
||||||
} limit 5
|
|
||||||
}`,
|
|
||||||
description: 'Multi-hop pattern matching'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
const result = compiler.compile(input, `test-pattern-${Date.now()}`);
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Fusion', () => {
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `evidence canAccess(user: User, resource: Resource) {
|
|
||||||
fusion min {
|
|
||||||
hasClearance(user, resource.level)
|
|
||||||
user.isActive
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
description: 'Min fusion - all conditions must be true'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canAccess(user: User, resource: Resource) {
|
|
||||||
fusion max {
|
|
||||||
hasRole(user, 'admin')
|
|
||||||
hasRole(user, 'superuser')
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
description: 'Max fusion - any condition can be true'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canAccess(user: User, resource: Resource) {
|
|
||||||
fusion majority {
|
|
||||||
hasClearance(user, 'secret')
|
|
||||||
user.isTrusted
|
|
||||||
user.hasRecentActivity
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
description: 'Majority fusion - most conditions must be true'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canAccessCritical(user: User, resource: Resource) {
|
|
||||||
fusion min {
|
|
||||||
hasClearance(user, resource.level)
|
|
||||||
user.isActive
|
|
||||||
NOT user.isBlacklisted
|
|
||||||
}
|
|
||||||
|
|
||||||
fusion max {
|
|
||||||
hasRole(user, 'admin')
|
|
||||||
fusion majority {
|
|
||||||
hasClearance(user, 'secret')
|
|
||||||
user.isTrusted
|
|
||||||
user.lastActive within 1hr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
description: 'Nested fusion with different strategies'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canAccess(user: User, resource: Resource) {
|
|
||||||
fusion average {
|
|
||||||
user.reputation
|
|
||||||
user.activityScore
|
|
||||||
user.verificationLevel
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
description: 'Average fusion for numeric values'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
const result = compiler.compile(input, `test-fusion-${Date.now()}`);
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Complex evidence', () => {
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `evidence canRead(user: User, doc: Document) {
|
|
||||||
owns(user, doc)
|
|
||||||
|
|
||||||
isMember(user, *group) {
|
|
||||||
canRead(group, doc)
|
|
||||||
} limit 5
|
|
||||||
|
|
||||||
parentOf(user, *parent) {
|
|
||||||
canRead(parent, doc)
|
|
||||||
} limit 3
|
|
||||||
|
|
||||||
similar(doc, *similar) |similarity| {
|
|
||||||
canRead(user, similar)
|
|
||||||
} with similarity > 0.7 limit 5
|
|
||||||
|
|
||||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
|
||||||
}`,
|
|
||||||
description: 'Complex evidence with all features'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canAccessCritical(user: User, resource: Resource) {
|
|
||||||
ALWAYS user.isActive
|
|
||||||
|
|
||||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
|
||||||
|
|
||||||
REQUIRES hasClearance(user, resource.level)
|
|
||||||
|
|
||||||
fusion min {
|
|
||||||
hasClearance(user, resource.level)
|
|
||||||
user.isActive
|
|
||||||
NOT user.isBlacklisted
|
|
||||||
}
|
|
||||||
|
|
||||||
fusion max {
|
|
||||||
hasRole(user, 'admin')
|
|
||||||
fusion majority {
|
|
||||||
hasClearance(user, 'secret')
|
|
||||||
user.isTrusted
|
|
||||||
user.lastActive within 1hr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
description: 'Critical access with all rule types and fusion'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canModify(user: User, doc: Document) {
|
|
||||||
owns(user, doc)
|
|
||||||
|
|
||||||
isMember(user, *group) {
|
|
||||||
canModify(group, doc)
|
|
||||||
} limit 3
|
|
||||||
|
|
||||||
similar(doc, *similar) |similarity| {
|
|
||||||
canModify(user, similar)
|
|
||||||
similar.isEditable
|
|
||||||
} with similarity > 0.8 limit 2
|
|
||||||
|
|
||||||
fusion majority {
|
|
||||||
user.isTrusted
|
|
||||||
user.hasRecentActivity
|
|
||||||
doc.isPublic
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
description: 'Modification access with similarity and fusion'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
const result = compiler.compile(input, `test-complex-evidence-${Date.now()}`);
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Evidence error handling', () => {
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `evidence canRead(user: User, doc: Document) {
|
|
||||||
hasRole(user, 'admin'
|
|
||||||
}`,
|
|
||||||
description: 'Missing closing parenthesis should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canRead(user: User, doc: Document) {
|
|
||||||
WHEN hasRole(user, 'admin') UNLESS
|
|
||||||
}`,
|
|
||||||
description: 'Incomplete UNLESS condition should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canRead(user: User, doc: Document) {
|
|
||||||
fusion min {
|
|
||||||
hasRole(user, 'admin')
|
|
||||||
}`,
|
|
||||||
description: 'Incomplete fusion should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canRead(user: User, doc: Document) {
|
|
||||||
isMember(user, *group) {
|
|
||||||
canRead(group, doc)
|
|
||||||
} with
|
|
||||||
}`,
|
|
||||||
description: 'Incomplete with clause should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canRead(user: User, doc: Document) {
|
|
||||||
isMember(user, *group) {
|
|
||||||
canRead(group, doc)
|
|
||||||
} limit
|
|
||||||
}`,
|
|
||||||
description: 'Incomplete limit should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `evidence canRead(user: User, doc: Document) {
|
|
||||||
invalid syntax here
|
|
||||||
}`,
|
|
||||||
description: 'Invalid syntax should fail'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const result = compiler.compile(input, `test-evidence-error-${Date.now()}`);
|
|
||||||
assert.ok(!result.success, `${description} should fail to parse`);
|
|
||||||
} catch {
|
|
||||||
// Expected to fail
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
import { describe, test } from 'node:test';
|
|
||||||
import assert from 'node:assert/strict';
|
|
||||||
import { DSLCompiler } from '../../src/ast/DSLCompiler.js';
|
|
||||||
|
|
||||||
function createMockArbiter() {
|
|
||||||
const relationConfigs = new Map();
|
|
||||||
return {
|
|
||||||
relationConfigs,
|
|
||||||
setRelationConfig(relation, config) {
|
|
||||||
relationConfigs.set(relation, config);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Expression Parsing', () => {
|
|
||||||
const arbiter = createMockArbiter();
|
|
||||||
const compiler = new DSLCompiler(arbiter);
|
|
||||||
|
|
||||||
test('Arithmetic operator precedence', () => {
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: '1 + 2 * 3',
|
|
||||||
expected: 'Should evaluate as 1 + (2 * 3) = 7',
|
|
||||||
description: 'Multiplication before addition'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: '10 - 3 * 2',
|
|
||||||
expected: 'Should evaluate as 10 - (3 * 2) = 4',
|
|
||||||
description: 'Multiplication before subtraction'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: '8 / 2 * 4',
|
|
||||||
expected: 'Should evaluate as (8 / 2) * 4 = 16',
|
|
||||||
description: 'Left-associative division and multiplication'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: '2 + 3 * 4 - 5',
|
|
||||||
expected: 'Should evaluate as 2 + (3 * 4) - 5 = 9',
|
|
||||||
description: 'Mixed arithmetic with correct precedence'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: '(1 + 2) * 3',
|
|
||||||
expected: 'Should evaluate as (1 + 2) * 3 = 9',
|
|
||||||
description: 'Parentheses override precedence'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, expected, description }) => {
|
|
||||||
const dsl = `evidence test() { ${input} }`;
|
|
||||||
const result = compiler.compile(dsl, `test-arithmetic-${Date.now()}`);
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Logical operator precedence', () => {
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: 'true && false || true',
|
|
||||||
expected: 'Should evaluate as (true && false) || true = true',
|
|
||||||
description: 'AND before OR'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'false || true && false',
|
|
||||||
expected: 'Should evaluate as false || (true && false) = false',
|
|
||||||
description: 'AND before OR (alternative)'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'NOT true && false',
|
|
||||||
expected: 'Should evaluate as (NOT true) && false = false',
|
|
||||||
description: 'NOT before AND'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'true && NOT false',
|
|
||||||
expected: 'Should evaluate as true && (NOT false) = true',
|
|
||||||
description: 'NOT before AND (alternative)'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: '(true || false) && true',
|
|
||||||
expected: 'Should evaluate as (true || false) && true = true',
|
|
||||||
description: 'Parentheses override logical precedence'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, expected, description }) => {
|
|
||||||
const dsl = `evidence test() { ${input} }`;
|
|
||||||
const result = compiler.compile(dsl, `test-logical-${Date.now()}`);
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Comparison operators', () => {
|
|
||||||
const testCases = [
|
|
||||||
{ input: '1 == 1', description: 'Equality comparison' },
|
|
||||||
{ input: '1 != 2', description: 'Inequality comparison' },
|
|
||||||
{ input: '5 > 3', description: 'Greater than' },
|
|
||||||
{ input: '3 < 5', description: 'Less than' },
|
|
||||||
{ input: '4 >= 4', description: 'Greater than or equal' },
|
|
||||||
{ input: '4 <= 4', description: 'Less than or equal' },
|
|
||||||
{ input: '1 == 1 && 2 > 1', description: 'Comparison with logical operators' },
|
|
||||||
{ input: '1 + 2 == 3', description: 'Arithmetic in comparison' }
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
const dsl = `evidence test() { ${input} }`;
|
|
||||||
const result = compiler.compile(dsl, `test-comparison-${Date.now()}`);
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Temporal expressions', () => {
|
|
||||||
const testCases = [
|
|
||||||
{ input: 'user.lastActive within 1h', description: 'Temporal within expression' },
|
|
||||||
{ input: 'user.lastLogin within 24h', description: 'Temporal within with hours' },
|
|
||||||
{ input: 'user.createdAt within 7d', description: 'Temporal within with days' },
|
|
||||||
{ input: 'user.lastActivity within 1h && user.isActive', description: 'Temporal with logical operators' }
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
const dsl = `evidence test() { ${input} }`;
|
|
||||||
const result = compiler.compile(dsl, `test-temporal-${Date.now()}`);
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Unary operators', () => {
|
|
||||||
const testCases = [
|
|
||||||
{ input: 'NOT true', description: 'NOT operator' },
|
|
||||||
{ input: '!false', description: 'Alternative NOT operator' },
|
|
||||||
{ input: 'NOT (true && false)', description: 'NOT with parenthesized expression' },
|
|
||||||
{ input: 'NOT user.isSuspended', description: 'NOT with attribute access' }
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
const dsl = `evidence test() { ${input} }`;
|
|
||||||
const result = compiler.compile(dsl, `test-unary-${Date.now()}`);
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Attribute access', () => {
|
|
||||||
const testCases = [
|
|
||||||
{ input: 'user.role', description: 'Simple attribute access' },
|
|
||||||
{ input: 'user.profile.name', description: 'Nested attribute access' },
|
|
||||||
{ input: 'user.permissions[0]', description: 'Array access' },
|
|
||||||
{ input: 'user.role.permissions[0]', description: 'Nested attribute with array access' },
|
|
||||||
{ input: 'user.isActive && user.role == "admin"', description: 'Attribute access in logical expression' }
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
const dsl = `evidence test() { ${input} }`;
|
|
||||||
const result = compiler.compile(dsl, `test-attribute-${Date.now()}`);
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Function calls', () => {
|
|
||||||
const testCases = [
|
|
||||||
{ input: 'hasRole(user, "admin")', description: 'Simple function call' },
|
|
||||||
{ input: 'isMember(user, group)', description: 'Function call with variables' },
|
|
||||||
{ input: 'hasPermission(user, resource, "read")', description: 'Function call with multiple arguments' },
|
|
||||||
{ input: 'hasRole(user, "admin") && isActive(user)', description: 'Multiple function calls' },
|
|
||||||
{ input: 'hasRole(user, user.role)', description: 'Function call with attribute access' }
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
const dsl = `evidence test() { ${input} }`;
|
|
||||||
const result = compiler.compile(dsl, `test-function-${Date.now()}`);
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Complex expressions', () => {
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: 'user.isActive && (hasRole(user, "admin") || hasPermission(user, resource, "read"))',
|
|
||||||
description: 'Complex logical expression with function calls'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'user.balance > 100 && user.isActive && NOT user.isSuspended',
|
|
||||||
description: 'Multiple conditions with NOT'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'user.lastActive within 1h && (user.role == "admin" || user.hasEmergencyAccess)',
|
|
||||||
description: 'Temporal with logical conditions'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'hasRole(user, "admin") && user.isActive && NOT (user.isSuspended || user.isBlacklisted)',
|
|
||||||
description: 'Complex negation with multiple conditions'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'user.score > 0.8 && user.isTrusted && user.lastActivity within 24h',
|
|
||||||
description: 'Multiple attribute conditions with temporal'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
const dsl = `evidence test() { ${input} }`;
|
|
||||||
const result = compiler.compile(dsl, `test-complex-${Date.now()}`);
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Expression error handling', () => {
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: 'user.role ==',
|
|
||||||
description: 'Incomplete comparison should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'user.role &&',
|
|
||||||
description: 'Incomplete logical expression should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'hasRole(user,)',
|
|
||||||
description: 'Function call with missing argument should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'user.role == "admin" &&',
|
|
||||||
description: 'Incomplete logical expression should fail'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const dsl = `evidence test() { ${input} }`;
|
|
||||||
const result = compiler.compile(dsl, `test-error-${Date.now()}`);
|
|
||||||
assert.ok(!result.success, `${description} should fail to parse`);
|
|
||||||
} catch {
|
|
||||||
// Expected to fail
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
import { describe, test } from 'node:test';
|
|
||||||
import assert from 'node:assert/strict';
|
|
||||||
import { DSLCompiler } from '../../src/ast/DSLCompiler.js';
|
|
||||||
|
|
||||||
function createMockArbiter() {
|
|
||||||
const relationConfigs = new Map();
|
|
||||||
return {
|
|
||||||
relationConfigs,
|
|
||||||
setRelationConfig(relation, config) {
|
|
||||||
relationConfigs.set(relation, config);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Fact Declarations', () => {
|
|
||||||
const arbiter = createMockArbiter();
|
|
||||||
const compiler = new DSLCompiler(arbiter);
|
|
||||||
|
|
||||||
test('Basic facts', () => {
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `fact hasRole(user: User, role: string)`,
|
|
||||||
description: 'Simple fact with two parameters'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact isMember(user: User, group: Group)`,
|
|
||||||
description: 'Fact with custom types'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact owns(user: User, doc: Document)`,
|
|
||||||
description: 'Fact with multiple custom types'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact isActive(user: User)`,
|
|
||||||
description: 'Fact with single parameter'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact hasPermission(user: User, resource: Resource, action: string)`,
|
|
||||||
description: 'Fact with three parameters'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
const result = compiler.compile(input, `test-basic-fact-${Date.now()}`);
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
assert.ok(result.program.facts.length > 0, 'Should have facts');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Fact properties', () => {
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `fact isMember(user: User, group: Group) transitive`,
|
|
||||||
description: 'Transitive fact'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact isFriend(user: User, friend: User) symmetrical`,
|
|
||||||
description: 'Symmetrical fact'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact isMember(user: User, group: Group) transitive symmetrical`,
|
|
||||||
description: 'Fact with multiple properties'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact isColleague(user: User, colleague: User) symmetrical`,
|
|
||||||
description: 'Symmetrical relationship fact'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact isParentOf(parent: User, child: User) transitive`,
|
|
||||||
description: 'Transitive hierarchical fact'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
const result = compiler.compile(input, `test-fact-property-${Date.now()}`);
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Fact caching', () => {
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `fact hasRole(user: User, role: string) CACHE eager`,
|
|
||||||
description: 'Eager cached fact'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact isMember(user: User, group: Group) CACHE lazy`,
|
|
||||||
description: 'Lazy cached fact'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact isMember(user: User, group: Group) transitive CACHE eager`,
|
|
||||||
description: 'Transitive fact with eager caching'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact isFriend(user: User, friend: User) symmetrical CACHE lazy`,
|
|
||||||
description: 'Symmetrical fact with lazy caching'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact hasPermission(user: User, resource: Resource, action: string) CACHE eager`,
|
|
||||||
description: 'Multi-parameter fact with eager caching'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
const result = compiler.compile(input, `test-fact-cache-${Date.now()}`);
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Fact limits', () => {
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `fact isMember(user: User, group: Group) limit 10`,
|
|
||||||
description: 'Fact with simple limit'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact isFriend(user: User, friend: User) limit 100`,
|
|
||||||
description: 'Fact with higher limit'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact isMember(user: User, group: Group) transitive limit 5`,
|
|
||||||
description: 'Transitive fact with limit'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact isFriend(user: User, friend: User) symmetrical limit 50`,
|
|
||||||
description: 'Symmetrical fact with limit'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact isMember(user: User, group: Group) transitive CACHE lazy limit 3`,
|
|
||||||
description: 'Fact with properties, caching, and limit'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
const result = compiler.compile(input, `test-fact-limit-${Date.now()}`);
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Parameter types', () => {
|
|
||||||
const testCases = [
|
|
||||||
{ type: 'string', description: 'String parameter' },
|
|
||||||
{ type: 'number', description: 'Number parameter' },
|
|
||||||
{ type: 'boolean', description: 'Boolean parameter' },
|
|
||||||
{ type: 'timestamp', description: 'Timestamp parameter' },
|
|
||||||
{ type: 'User', description: 'Custom type parameter' },
|
|
||||||
{ type: 'Group', description: 'Another custom type parameter' },
|
|
||||||
{ type: 'Permission[]', description: 'Array type parameter' }
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ type, description }) => {
|
|
||||||
const dsl = `fact test(param: ${type})`;
|
|
||||||
const result = compiler.compile(dsl, `test-param-type-${Date.now()}`);
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Complex facts', () => {
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `fact hasRole(user: User, role: string) CACHE eager
|
|
||||||
fact isMember(user: User, group: Group) transitive CACHE lazy limit 10
|
|
||||||
fact isFriend(user: User, friend: User) symmetrical CACHE eager limit 100
|
|
||||||
fact owns(user: User, doc: Document) CACHE eager
|
|
||||||
fact isSuspended(user: User) CACHE lazy`,
|
|
||||||
description: 'Multiple facts with different configurations'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact hasPermission(user: User, resource: Resource, action: string) CACHE eager
|
|
||||||
fact isAdmin(user: User) CACHE eager
|
|
||||||
fact isOwner(user: User, resource: Resource) CACHE eager
|
|
||||||
fact hasAccess(user: User, resource: Resource, level: string) CACHE lazy`,
|
|
||||||
description: 'Permission-related facts'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact isMember(user: User, group: Group) transitive CACHE lazy limit 5
|
|
||||||
fact isFriend(user: User, friend: User) symmetrical CACHE eager limit 50
|
|
||||||
fact isColleague(user: User, colleague: User) symmetrical CACHE lazy limit 20
|
|
||||||
fact isParentOf(parent: User, child: User) transitive CACHE eager limit 3`,
|
|
||||||
description: 'Relationship facts with various properties'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
const result = compiler.compile(input, `test-complex-facts-${Date.now()}`);
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
assert.ok(result.program.facts.length > 0, 'Should have facts');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Fact error handling', () => {
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `fact hasRole(user: User, role: string`,
|
|
||||||
description: 'Missing closing parenthesis should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact hasRole(user: User, )`,
|
|
||||||
description: 'Missing parameter name should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact hasRole(user: User, role: )`,
|
|
||||||
description: 'Missing parameter type should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact hasRole(, role: string)`,
|
|
||||||
description: 'Missing parameter name should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact hasRole(user: User, role: string) CACHE`,
|
|
||||||
description: 'Incomplete cache directive should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact hasRole(user: User, role: string) limit`,
|
|
||||||
description: 'Incomplete limit should fail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `fact hasRole(user: User, role: string) invalid`,
|
|
||||||
description: 'Invalid property should fail'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
try {
|
|
||||||
const result = compiler.compile(input, `test-fact-error-${Date.now()}`);
|
|
||||||
assert.ok(!result.success, `${description} should fail to parse`);
|
|
||||||
} catch {
|
|
||||||
// Expected to fail
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,534 +0,0 @@
|
|||||||
import { describe, test } from 'node:test';
|
|
||||||
import assert from 'node:assert/strict';
|
|
||||||
import { DSLCompiler } from '../../src/ast/DSLCompiler.js';
|
|
||||||
|
|
||||||
function createMockArbiter() {
|
|
||||||
const relationConfigs = new Map();
|
|
||||||
return {
|
|
||||||
relationConfigs,
|
|
||||||
setRelationConfig(relation, config) {
|
|
||||||
relationConfigs.set(relation, config);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Integration Tests', () => {
|
|
||||||
const arbiter = createMockArbiter();
|
|
||||||
const compiler = new DSLCompiler(arbiter);
|
|
||||||
|
|
||||||
test('Complete authorization system', () => {
|
|
||||||
const completeSystem = `
|
|
||||||
// Type definitions with complex behaviors
|
|
||||||
definition User {
|
|
||||||
role: string
|
|
||||||
isActive: boolean
|
|
||||||
lastActive: timestamp BEHAVES {
|
|
||||||
decaying down hourly
|
|
||||||
} CACHE lazy
|
|
||||||
isSuspended: boolean
|
|
||||||
balance: number BEHAVES {
|
|
||||||
decaying down hourly
|
|
||||||
} CACHE eager
|
|
||||||
score: number BEHAVES {
|
|
||||||
blurring adaptive confidence_95
|
|
||||||
} CACHE lazy
|
|
||||||
session: string BEHAVES {
|
|
||||||
ttl 24h
|
|
||||||
} CACHE eager
|
|
||||||
clearance: string BEHAVES {
|
|
||||||
blurring fixed
|
|
||||||
} CACHE eager
|
|
||||||
reputation: number BEHAVES {
|
|
||||||
decaying up daily
|
|
||||||
} CACHE lazy
|
|
||||||
}
|
|
||||||
|
|
||||||
definition Group {
|
|
||||||
name: string
|
|
||||||
permissions: Permission[]
|
|
||||||
level: string
|
|
||||||
isPublic: boolean CACHE eager
|
|
||||||
created: timestamp BEHAVES {
|
|
||||||
decaying stable monthly
|
|
||||||
} CACHE lazy
|
|
||||||
}
|
|
||||||
|
|
||||||
definition Document {
|
|
||||||
level: string
|
|
||||||
owner: User
|
|
||||||
tags: string[]
|
|
||||||
content: string BEHAVES {
|
|
||||||
blurring fixed
|
|
||||||
} CACHE lazy
|
|
||||||
accessCount: number BEHAVES {
|
|
||||||
decaying up daily
|
|
||||||
} CACHE eager
|
|
||||||
expiresAt: timestamp BEHAVES {
|
|
||||||
ttl 30d
|
|
||||||
} CACHE eager
|
|
||||||
isPublic: boolean CACHE eager
|
|
||||||
}
|
|
||||||
|
|
||||||
definition Resource {
|
|
||||||
level: string
|
|
||||||
owner: User
|
|
||||||
permissions: Permission[]
|
|
||||||
isPublic: boolean CACHE eager
|
|
||||||
accessCount: number BEHAVES {
|
|
||||||
decaying up daily
|
|
||||||
} CACHE eager
|
|
||||||
}
|
|
||||||
|
|
||||||
// Facts with various properties and caching
|
|
||||||
fact hasRole(user: User, role: string) CACHE eager
|
|
||||||
fact isMember(user: User, group: Group) transitive CACHE lazy limit 10
|
|
||||||
fact isFriend(user: User, friend: User) symmetrical CACHE eager limit 100
|
|
||||||
fact owns(user: User, doc: Document) CACHE eager
|
|
||||||
fact isSuspended(user: User) CACHE lazy
|
|
||||||
fact hasPermission(user: User, resource: Resource, action: string) CACHE eager
|
|
||||||
fact isAdmin(user: User) CACHE eager
|
|
||||||
fact isOwner(user: User, resource: Resource) CACHE eager
|
|
||||||
fact hasAccess(user: User, resource: Resource, level: string) CACHE lazy
|
|
||||||
fact isColleague(user: User, colleague: User) symmetrical CACHE lazy limit 50
|
|
||||||
fact isParentOf(parent: User, child: User) transitive CACHE eager limit 3
|
|
||||||
|
|
||||||
// Evidence rules with complex logic
|
|
||||||
evidence canRead(user: User, doc: Document) {
|
|
||||||
owns(user, doc)
|
|
||||||
|
|
||||||
isMember(user, *group) {
|
|
||||||
canRead(group, doc)
|
|
||||||
} limit 5
|
|
||||||
|
|
||||||
parentOf(user, *parent) {
|
|
||||||
canRead(parent, doc)
|
|
||||||
} limit 3
|
|
||||||
|
|
||||||
similar(doc, *similar) |similarity| {
|
|
||||||
canRead(user, similar)
|
|
||||||
} with similarity > 0.7 limit 5
|
|
||||||
|
|
||||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
evidence canWrite(user: User, doc: Document) {
|
|
||||||
owns(user, doc)
|
|
||||||
|
|
||||||
isMember(user, *group) {
|
|
||||||
canWrite(group, doc)
|
|
||||||
} limit 3
|
|
||||||
|
|
||||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
|
||||||
|
|
||||||
REQUIRES user.isActive
|
|
||||||
}
|
|
||||||
|
|
||||||
evidence canDelete(user: User, doc: Document) {
|
|
||||||
owns(user, doc)
|
|
||||||
|
|
||||||
ALWAYS user.isActive
|
|
||||||
|
|
||||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
|
||||||
|
|
||||||
REQUIRES user.isActive
|
|
||||||
}
|
|
||||||
|
|
||||||
evidence canAccessCritical(user: User, resource: Resource) {
|
|
||||||
fusion min {
|
|
||||||
hasClearance(user, resource.level)
|
|
||||||
user.isActive
|
|
||||||
NOT user.isBlacklisted
|
|
||||||
}
|
|
||||||
|
|
||||||
fusion max {
|
|
||||||
hasRole(user, 'admin')
|
|
||||||
fusion majority {
|
|
||||||
hasClearance(user, 'secret')
|
|
||||||
user.isTrusted
|
|
||||||
user.lastActive within 1hr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
evidence canAccessSensitive(user: User, doc: Document) {
|
|
||||||
ALWAYS user.isActive
|
|
||||||
|
|
||||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
|
||||||
|
|
||||||
REQUIRES hasClearance(user, doc.level)
|
|
||||||
|
|
||||||
fusion majority {
|
|
||||||
user.isTrusted
|
|
||||||
user.hasRecentActivity
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Measures for computed values
|
|
||||||
measure userRole(user: User) {
|
|
||||||
user.role
|
|
||||||
} PROVIDES string
|
|
||||||
|
|
||||||
measure userPermissions(user: User) {
|
|
||||||
fusion max {
|
|
||||||
user.role.permissions
|
|
||||||
user.group.permissions
|
|
||||||
}
|
|
||||||
} PROVIDES Permission[]
|
|
||||||
|
|
||||||
measure effectiveClearance(user: User) {
|
|
||||||
fusion majority {
|
|
||||||
user.clearance
|
|
||||||
user.role.clearance
|
|
||||||
user.group.clearance
|
|
||||||
}
|
|
||||||
} PROVIDES string
|
|
||||||
|
|
||||||
measure userTrustScore(user: User) {
|
|
||||||
fusion average {
|
|
||||||
user.reputation
|
|
||||||
user.activityScore
|
|
||||||
user.verificationLevel
|
|
||||||
}
|
|
||||||
} PROVIDES number
|
|
||||||
|
|
||||||
measure userBalance(user: User) {
|
|
||||||
user.balance
|
|
||||||
} PROVIDES number
|
|
||||||
|
|
||||||
measure userScore(user: User) {
|
|
||||||
user.score
|
|
||||||
} PROVIDES number
|
|
||||||
`;
|
|
||||||
|
|
||||||
const result = compiler.compile(completeSystem, 'test-complete-system');
|
|
||||||
assert.ok(result.success, 'Complete authorization system should compile successfully');
|
|
||||||
assert.ok(result.program.definitions.length >= 4, 'Should have multiple definitions');
|
|
||||||
assert.ok(result.program.facts.length >= 10, 'Should have multiple facts');
|
|
||||||
assert.ok(result.program.evidence.length >= 5, 'Should have multiple evidence rules');
|
|
||||||
assert.ok(result.program.measures.length >= 6, 'Should have multiple measures');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Multi-domain system', () => {
|
|
||||||
const multiDomain = `
|
|
||||||
// Authentication domain
|
|
||||||
definition User {
|
|
||||||
role: string
|
|
||||||
isActive: boolean
|
|
||||||
lastActive: timestamp BEHAVES { decaying down hourly } CACHE lazy
|
|
||||||
session: string BEHAVES { ttl 24h } CACHE eager
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasRole(user: User, role: string) CACHE eager
|
|
||||||
fact isActive(user: User) CACHE eager
|
|
||||||
|
|
||||||
evidence canAuthenticate(user: User) {
|
|
||||||
user.isActive
|
|
||||||
user.session within 24h
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authorization domain
|
|
||||||
definition Resource {
|
|
||||||
level: string
|
|
||||||
owner: User
|
|
||||||
permissions: Permission[]
|
|
||||||
}
|
|
||||||
|
|
||||||
fact owns(user: User, resource: Resource) CACHE eager
|
|
||||||
fact hasPermission(user: User, resource: Resource, action: string) CACHE eager
|
|
||||||
|
|
||||||
evidence canAccess(user: User, resource: Resource) {
|
|
||||||
owns(user, resource)
|
|
||||||
hasPermission(user, resource, 'read')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finance domain
|
|
||||||
definition Account {
|
|
||||||
balance: number BEHAVES { decaying down hourly } CACHE eager
|
|
||||||
owner: User
|
|
||||||
isActive: boolean CACHE eager
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasAccount(user: User, account: Account) CACHE eager
|
|
||||||
fact hasBalance(user: User, amount: number) CACHE eager
|
|
||||||
|
|
||||||
evidence canWithdraw(user: User, amount: number) {
|
|
||||||
hasBalance(user, amount)
|
|
||||||
user.isActive
|
|
||||||
}
|
|
||||||
|
|
||||||
// Social domain
|
|
||||||
definition Group {
|
|
||||||
name: string
|
|
||||||
members: User[]
|
|
||||||
isPublic: boolean CACHE eager
|
|
||||||
}
|
|
||||||
|
|
||||||
fact isMember(user: User, group: Group) transitive CACHE lazy limit 10
|
|
||||||
fact isFriend(user: User, friend: User) symmetrical CACHE eager limit 100
|
|
||||||
|
|
||||||
evidence canAccessGroup(user: User, group: Group) {
|
|
||||||
isMember(user, group)
|
|
||||||
group.isPublic
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const result = compiler.compile(multiDomain, 'test-multi-domain');
|
|
||||||
assert.ok(result.success, 'Multi-domain system should compile successfully');
|
|
||||||
assert.ok(result.program.definitions.length >= 4, 'Should have multiple domain definitions');
|
|
||||||
assert.ok(result.program.facts.length >= 8, 'Should have multiple domain facts');
|
|
||||||
assert.ok(result.program.evidence.length >= 4, 'Should have multiple domain evidence rules');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Hierarchical access', () => {
|
|
||||||
const hierarchicalSystem = `
|
|
||||||
definition User {
|
|
||||||
role: string
|
|
||||||
level: string
|
|
||||||
isActive: boolean
|
|
||||||
clearance: string
|
|
||||||
}
|
|
||||||
|
|
||||||
definition Organization {
|
|
||||||
name: string
|
|
||||||
level: string
|
|
||||||
parent: Organization
|
|
||||||
}
|
|
||||||
|
|
||||||
fact isMember(user: User, org: Organization) transitive CACHE lazy limit 5
|
|
||||||
fact isParentOf(parent: Organization, child: Organization) transitive CACHE eager limit 3
|
|
||||||
fact hasRole(user: User, role: string) CACHE eager
|
|
||||||
fact hasClearance(user: User, level: string) CACHE eager
|
|
||||||
|
|
||||||
evidence canAccessOrg(user: User, org: Organization) {
|
|
||||||
isMember(user, org)
|
|
||||||
|
|
||||||
isParentOf(org, *parentOrg) {
|
|
||||||
canAccessOrg(user, parentOrg)
|
|
||||||
} limit 3
|
|
||||||
|
|
||||||
WHEN hasRole(user, 'admin') UNLESS user.isSuspended
|
|
||||||
}
|
|
||||||
|
|
||||||
evidence canAccessResource(user: User, resource: Resource) {
|
|
||||||
isMember(user, *org) {
|
|
||||||
canAccessResource(org, resource)
|
|
||||||
} limit 5
|
|
||||||
|
|
||||||
parentOf(user, *parent) {
|
|
||||||
canAccessResource(parent, resource)
|
|
||||||
} limit 2
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const result = compiler.compile(hierarchicalSystem, 'test-hierarchical');
|
|
||||||
assert.ok(result.success, 'Hierarchical access system should compile successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Similarity-based access', () => {
|
|
||||||
const similaritySystem = `
|
|
||||||
definition User {
|
|
||||||
profile: string
|
|
||||||
interests: string[]
|
|
||||||
isActive: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
definition Document {
|
|
||||||
content: string
|
|
||||||
tags: string[]
|
|
||||||
isPublic: boolean
|
|
||||||
owner: User
|
|
||||||
}
|
|
||||||
|
|
||||||
fact isFriend(user: User, friend: User) symmetrical CACHE eager limit 100
|
|
||||||
fact hasInterest(user: User, interest: string) CACHE lazy
|
|
||||||
fact hasTag(doc: Document, tag: string) CACHE lazy
|
|
||||||
|
|
||||||
evidence canRead(user: User, doc: Document) {
|
|
||||||
owns(user, doc)
|
|
||||||
|
|
||||||
similar(doc, *similar) |similarity| {
|
|
||||||
canRead(user, similar)
|
|
||||||
similar.isPublic
|
|
||||||
} with similarity > 0.7 limit 10
|
|
||||||
|
|
||||||
isFriend(user, *friend) {
|
|
||||||
canRead(friend, doc)
|
|
||||||
} limit 5
|
|
||||||
|
|
||||||
fusion majority {
|
|
||||||
user.interests
|
|
||||||
doc.tags
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
evidence canRecommend(user: User, doc: Document) {
|
|
||||||
similar(user, *similarUser) |similarity| {
|
|
||||||
canRead(similarUser, doc)
|
|
||||||
} with similarity > 0.8 limit 20
|
|
||||||
|
|
||||||
fusion average {
|
|
||||||
user.profile
|
|
||||||
doc.content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const result = compiler.compile(similaritySystem, 'test-similarity');
|
|
||||||
assert.ok(result.success, 'Similarity-based access system should compile successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Temporal access', () => {
|
|
||||||
const temporalSystem = `
|
|
||||||
definition User {
|
|
||||||
lastActive: timestamp BEHAVES { decaying down hourly } CACHE lazy
|
|
||||||
session: string BEHAVES { ttl 24h } CACHE eager
|
|
||||||
isActive: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
definition Event {
|
|
||||||
startTime: timestamp
|
|
||||||
endTime: timestamp
|
|
||||||
isPublic: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasAccess(user: User, event: Event) CACHE lazy
|
|
||||||
fact isParticipant(user: User, event: Event) CACHE eager
|
|
||||||
|
|
||||||
evidence canAccessEvent(user: User, event: Event) {
|
|
||||||
user.lastActive within 1h
|
|
||||||
|
|
||||||
isParticipant(user, event)
|
|
||||||
|
|
||||||
WHEN event.isPublic UNLESS user.isSuspended
|
|
||||||
|
|
||||||
fusion min {
|
|
||||||
user.session within 24h
|
|
||||||
user.isActive
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
evidence canAccessHistorical(user: User, event: Event) {
|
|
||||||
user.lastActive within 24h
|
|
||||||
|
|
||||||
fusion majority {
|
|
||||||
user.isActive
|
|
||||||
user.session within 24h
|
|
||||||
event.isPublic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const result = compiler.compile(temporalSystem, 'test-temporal');
|
|
||||||
assert.ok(result.success, 'Temporal access system should compile successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Complex behaviors', () => {
|
|
||||||
const behaviorSystem = `
|
|
||||||
definition User {
|
|
||||||
balance: number BEHAVES { decaying down hourly } CACHE eager
|
|
||||||
score: number BEHAVES { blurring adaptive confidence_95 } CACHE lazy
|
|
||||||
session: string BEHAVES { ttl 24h } CACHE eager
|
|
||||||
reputation: number BEHAVES { decaying up daily } CACHE lazy
|
|
||||||
clearance: string BEHAVES { blurring fixed } CACHE eager
|
|
||||||
lastActive: timestamp BEHAVES { decaying down hourly } CACHE lazy
|
|
||||||
}
|
|
||||||
|
|
||||||
definition Document {
|
|
||||||
content: string BEHAVES { blurring fixed } CACHE lazy
|
|
||||||
accessCount: number BEHAVES { decaying up daily } CACHE eager
|
|
||||||
expiresAt: timestamp BEHAVES { ttl 30d } CACHE eager
|
|
||||||
isPublic: boolean CACHE eager
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasBalance(user: User, amount: number) CACHE eager
|
|
||||||
fact hasScore(user: User, score: number) CACHE lazy
|
|
||||||
fact hasReputation(user: User, reputation: number) CACHE lazy
|
|
||||||
|
|
||||||
evidence canAccessDocument(user: User, doc: Document) {
|
|
||||||
user.balance > 0
|
|
||||||
|
|
||||||
user.score > 0.5
|
|
||||||
|
|
||||||
user.reputation > 0.3
|
|
||||||
|
|
||||||
doc.accessCount < 1000
|
|
||||||
|
|
||||||
fusion majority {
|
|
||||||
user.isActive
|
|
||||||
user.lastActive within 1h
|
|
||||||
doc.isPublic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
measure userEffectiveScore(user: User) {
|
|
||||||
fusion average {
|
|
||||||
user.score
|
|
||||||
user.reputation
|
|
||||||
user.balance
|
|
||||||
}
|
|
||||||
} PROVIDES number
|
|
||||||
|
|
||||||
measure documentPopularity(doc: Document) {
|
|
||||||
doc.accessCount
|
|
||||||
} PROVIDES number
|
|
||||||
`;
|
|
||||||
|
|
||||||
const result = compiler.compile(behaviorSystem, 'test-behaviors');
|
|
||||||
assert.ok(result.success, 'Complex behaviors system should compile successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Performance scenarios', () => {
|
|
||||||
const performanceSystem = `
|
|
||||||
definition User {
|
|
||||||
role: string
|
|
||||||
isActive: boolean
|
|
||||||
permissions: Permission[] CACHE eager
|
|
||||||
}
|
|
||||||
|
|
||||||
definition Resource {
|
|
||||||
level: string
|
|
||||||
owner: User
|
|
||||||
permissions: Permission[] CACHE eager
|
|
||||||
}
|
|
||||||
|
|
||||||
// High-frequency facts with limits
|
|
||||||
fact isMember(user: User, group: Group) transitive CACHE lazy limit 5
|
|
||||||
fact isFriend(user: User, friend: User) symmetrical CACHE eager limit 50
|
|
||||||
fact hasPermission(user: User, resource: Resource, action: string) CACHE eager
|
|
||||||
fact owns(user: User, resource: Resource) CACHE eager
|
|
||||||
|
|
||||||
// Optimized evidence rules
|
|
||||||
evidence canAccess(user: User, resource: Resource) {
|
|
||||||
owns(user, resource)
|
|
||||||
|
|
||||||
isMember(user, *group) {
|
|
||||||
canAccess(group, resource)
|
|
||||||
} limit 3
|
|
||||||
|
|
||||||
WHEN hasPermission(user, resource, 'read')
|
|
||||||
}
|
|
||||||
|
|
||||||
evidence canModify(user: User, resource: Resource) {
|
|
||||||
owns(user, resource)
|
|
||||||
|
|
||||||
isMember(user, *group) {
|
|
||||||
canModify(group, resource)
|
|
||||||
} limit 2
|
|
||||||
|
|
||||||
WHEN hasPermission(user, resource, 'write')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Efficient measures
|
|
||||||
measure userEffectivePermissions(user: User) {
|
|
||||||
user.permissions
|
|
||||||
} PROVIDES Permission[]
|
|
||||||
|
|
||||||
measure resourceAccessLevel(resource: Resource) {
|
|
||||||
resource.level
|
|
||||||
} PROVIDES string
|
|
||||||
`;
|
|
||||||
|
|
||||||
const result = compiler.compile(performanceSystem, 'test-performance');
|
|
||||||
assert.ok(result.success, 'Performance scenarios should compile successfully');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,306 +0,0 @@
|
|||||||
import { describe, test } from 'node:test';
|
|
||||||
import assert from 'node:assert/strict';
|
|
||||||
import { DSLCompiler } from '../../src/ast/DSLCompiler.js';
|
|
||||||
|
|
||||||
function createMockArbiter() {
|
|
||||||
const relationConfigs = new Map();
|
|
||||||
return {
|
|
||||||
relationConfigs,
|
|
||||||
setRelationConfig(relation, config) {
|
|
||||||
relationConfigs.set(relation, config);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Measure Definitions', () => {
|
|
||||||
const arbiter = createMockArbiter();
|
|
||||||
const compiler = new DSLCompiler(arbiter);
|
|
||||||
|
|
||||||
test('Basic measures', () => {
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `measure userRole(user: User) {
|
|
||||||
user.role
|
|
||||||
} PROVIDES string`,
|
|
||||||
description: 'Simple measure with attribute access'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userBalance(user: User) {
|
|
||||||
user.balance
|
|
||||||
} PROVIDES number`,
|
|
||||||
description: 'Measure accessing numeric attribute'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure isUserActive(user: User) {
|
|
||||||
user.isActive
|
|
||||||
} PROVIDES boolean`,
|
|
||||||
description: 'Measure accessing boolean attribute'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userPermissions(user: User) {
|
|
||||||
user.permissions
|
|
||||||
} PROVIDES Permission[]`,
|
|
||||||
description: 'Measure accessing array attribute'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userScore(user: User) {
|
|
||||||
user.score
|
|
||||||
} PROVIDES number`,
|
|
||||||
description: 'Measure with behavior-inherited attribute'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
const result = compiler.compile(input, `test-basic-measure-${Date.now()}`);
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
assert.ok(result.program.measures.length > 0, 'Should have measures');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Measure return types', () => {
|
|
||||||
const testCases = [
|
|
||||||
{ type: 'string', description: 'String return type' },
|
|
||||||
{ type: 'number', description: 'Number return type' },
|
|
||||||
{ type: 'boolean', description: 'Boolean return type' },
|
|
||||||
{ type: 'timestamp', description: 'Timestamp return type' },
|
|
||||||
{ type: 'Permission[]', description: 'Array return type' },
|
|
||||||
{ type: 'User', description: 'Custom type return' },
|
|
||||||
{ type: 'Group[]', description: 'Custom array return type' }
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ type, description }) => {
|
|
||||||
const dsl = `measure test() { true } PROVIDES ${type}`;
|
|
||||||
const result = compiler.compile(dsl, `test-measure-return-${Date.now()}`);
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Measure aggregation', () => {
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `measure userPermissions(user: User) {
|
|
||||||
aggregate {
|
|
||||||
user.role.permissions
|
|
||||||
user.group.permissions
|
|
||||||
} USING majority
|
|
||||||
} PROVIDES Permission[]`,
|
|
||||||
description: 'Aggregation with majority strategy'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userClearance(user: User) {
|
|
||||||
aggregate {
|
|
||||||
user.clearance
|
|
||||||
user.role.clearance
|
|
||||||
user.group.clearance
|
|
||||||
} USING max
|
|
||||||
} PROVIDES string`,
|
|
||||||
description: 'Aggregation with max strategy'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userScore(user: User) {
|
|
||||||
aggregate {
|
|
||||||
user.reputation
|
|
||||||
user.activityScore
|
|
||||||
user.verificationLevel
|
|
||||||
} USING average
|
|
||||||
} PROVIDES number`,
|
|
||||||
description: 'Aggregation with average strategy'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userTrust(user: User) {
|
|
||||||
aggregate {
|
|
||||||
user.reputation
|
|
||||||
user.activityScore
|
|
||||||
user.verificationLevel
|
|
||||||
user.socialProof
|
|
||||||
} USING min
|
|
||||||
} PROVIDES number`,
|
|
||||||
description: 'Aggregation with min strategy'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
const result = compiler.compile(input, `test-measure-aggregation-${Date.now()}`);
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Measure fusion', () => {
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `measure effectiveClearance(user: User) {
|
|
||||||
fusion max {
|
|
||||||
user.clearance
|
|
||||||
user.role.clearance
|
|
||||||
user.group.clearance
|
|
||||||
}
|
|
||||||
} PROVIDES string`,
|
|
||||||
description: 'Fusion with max strategy'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userPermissions(user: User) {
|
|
||||||
fusion min {
|
|
||||||
user.role.permissions
|
|
||||||
user.group.permissions
|
|
||||||
}
|
|
||||||
} PROVIDES Permission[]`,
|
|
||||||
description: 'Fusion with min strategy'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userScore(user: User) {
|
|
||||||
fusion majority {
|
|
||||||
user.reputation
|
|
||||||
user.activityScore
|
|
||||||
user.verificationLevel
|
|
||||||
}
|
|
||||||
} PROVIDES number`,
|
|
||||||
description: 'Fusion with majority strategy'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userTrust(user: User) {
|
|
||||||
fusion average {
|
|
||||||
user.reputation
|
|
||||||
user.activityScore
|
|
||||||
user.verificationLevel
|
|
||||||
user.socialProof
|
|
||||||
}
|
|
||||||
} PROVIDES number`,
|
|
||||||
description: 'Fusion with average strategy'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
const result = compiler.compile(input, `test-measure-fusion-${Date.now()}`);
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Complex measures', () => {
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `measure userEffectivePermissions(user: User) {
|
|
||||||
aggregate {
|
|
||||||
user.role.permissions
|
|
||||||
user.group.permissions
|
|
||||||
user.directPermissions
|
|
||||||
} USING majority
|
|
||||||
} PROVIDES Permission[]`,
|
|
||||||
description: 'Complex aggregation with multiple sources'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userTrustScore(user: User) {
|
|
||||||
fusion average {
|
|
||||||
user.reputation
|
|
||||||
user.activityScore
|
|
||||||
user.verificationLevel
|
|
||||||
user.socialProof
|
|
||||||
user.peerRatings
|
|
||||||
}
|
|
||||||
} PROVIDES number`,
|
|
||||||
description: 'Complex fusion with multiple metrics'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userAccessLevel(user: User) {
|
|
||||||
fusion max {
|
|
||||||
user.clearance
|
|
||||||
user.role.clearance
|
|
||||||
user.group.clearance
|
|
||||||
user.temporaryClearance
|
|
||||||
}
|
|
||||||
} PROVIDES string`,
|
|
||||||
description: 'Complex clearance calculation'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userSimilarity(user1: User, user2: User) {
|
|
||||||
similar(user1, user2) |similarity| {
|
|
||||||
similarity
|
|
||||||
} with similarity > 0.5
|
|
||||||
} PROVIDES number`,
|
|
||||||
description: 'Similarity measure with pattern matching'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userEffectiveRole(user: User) {
|
|
||||||
fusion majority {
|
|
||||||
user.role
|
|
||||||
user.temporaryRole
|
|
||||||
user.actingRole
|
|
||||||
}
|
|
||||||
} PROVIDES string`,
|
|
||||||
description: 'Role determination with multiple sources'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description }) => {
|
|
||||||
const result = compiler.compile(input, `test-complex-measure-${Date.now()}`);
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Measure error handling', () => {
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
input: `measure userRole(user: User) {
|
|
||||||
user.role
|
|
||||||
}`,
|
|
||||||
description: 'Missing PROVIDES clause should fail',
|
|
||||||
expectSuccess: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userRole(user: User) {
|
|
||||||
user.role
|
|
||||||
} PROVIDES`,
|
|
||||||
description: 'Incomplete PROVIDES clause should fail',
|
|
||||||
expectSuccess: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userRole(user: User) {
|
|
||||||
user.role
|
|
||||||
} PROVIDES string`,
|
|
||||||
description: 'Valid measure should succeed',
|
|
||||||
expectSuccess: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userPermissions(user: User) {
|
|
||||||
aggregate {
|
|
||||||
user.role.permissions
|
|
||||||
user.group.permissions
|
|
||||||
} USING
|
|
||||||
} PROVIDES Permission[]`,
|
|
||||||
description: 'Incomplete USING clause should fail',
|
|
||||||
expectSuccess: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userScore(user: User) {
|
|
||||||
fusion {
|
|
||||||
user.reputation
|
|
||||||
user.activityScore
|
|
||||||
}
|
|
||||||
} PROVIDES number`,
|
|
||||||
description: 'Missing fusion strategy should fail',
|
|
||||||
expectSuccess: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `measure userRole(user: User) {
|
|
||||||
invalid syntax here
|
|
||||||
} PROVIDES string`,
|
|
||||||
description: 'Invalid syntax should fail',
|
|
||||||
expectSuccess: false
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ input, description, expectSuccess }) => {
|
|
||||||
try {
|
|
||||||
const result = compiler.compile(input, `test-measure-error-${Date.now()}`);
|
|
||||||
if (expectSuccess) {
|
|
||||||
assert.ok(result.success, `${description} should parse successfully`);
|
|
||||||
} else {
|
|
||||||
assert.ok(!result.success, `${description} should fail to parse`);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
if (!expectSuccess) {
|
|
||||||
// Expected to fail
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
import { describe, test } from 'node:test';
|
|
||||||
import assert from 'node:assert/strict';
|
|
||||||
import { PeggyDSLParser } from '../../src/ast/parser/PeggyDSLParser.js';
|
|
||||||
|
|
||||||
describe('Peggy DSL Parser', () => {
|
|
||||||
const parser = new PeggyDSLParser();
|
|
||||||
|
|
||||||
test('Basic parsing', () => {
|
|
||||||
const dsl = `
|
|
||||||
definition User {
|
|
||||||
role: string
|
|
||||||
isActive: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasRole(user: User, role: string)
|
|
||||||
|
|
||||||
evidence canRead(user: User, doc: Document) {
|
|
||||||
hasRole(user, 'admin')
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const program = parser.parse(dsl);
|
|
||||||
assert.ok(program !== null, 'Program should be created');
|
|
||||||
assert.ok(program.definitions.length === 1, 'Should have 1 definition');
|
|
||||||
assert.ok(program.facts.length === 1, 'Should have 1 fact');
|
|
||||||
assert.ok(program.evidence.length === 1, 'Should have 1 evidence');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Complex DSL parsing', () => {
|
|
||||||
const dsl = `
|
|
||||||
definition User {
|
|
||||||
role: string
|
|
||||||
isActive: boolean
|
|
||||||
clearance: string BEHAVES {
|
|
||||||
blurring adaptive confidence_95
|
|
||||||
} CACHE eager
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasRole(user: User, role: string) CACHE eager
|
|
||||||
fact isMember(user: User, group: Group) transitive CACHE lazy
|
|
||||||
|
|
||||||
evidence canRead(user: User, doc: Document) {
|
|
||||||
hasRole(user, 'admin')
|
|
||||||
|
|
||||||
isMember(user, *group) {
|
|
||||||
canRead(group, doc)
|
|
||||||
} limit 5
|
|
||||||
|
|
||||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const program = parser.parse(dsl);
|
|
||||||
assert.ok(program !== null, 'Program should be created');
|
|
||||||
assert.ok(program.definitions.length === 1, 'Should have 1 definition');
|
|
||||||
assert.ok(program.facts.length === 2, 'Should have 2 facts');
|
|
||||||
assert.ok(program.evidence.length === 1, 'Should have 1 evidence');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Error handling', () => {
|
|
||||||
const invalidDSL = `
|
|
||||||
definition User {
|
|
||||||
role: string
|
|
||||||
// Missing closing brace
|
|
||||||
|
|
||||||
fact hasRole(user: User, role: string)
|
|
||||||
// Missing semicolon
|
|
||||||
`;
|
|
||||||
|
|
||||||
assert.throws(
|
|
||||||
() => parser.parse(invalidDSL),
|
|
||||||
/Parsing failed/,
|
|
||||||
'Should have parsing error message'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Validation', () => {
|
|
||||||
const validDSL = `
|
|
||||||
definition User {
|
|
||||||
role: string
|
|
||||||
isActive: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
fact hasRole(user: User, role: string)
|
|
||||||
`;
|
|
||||||
|
|
||||||
const invalidDSL = `
|
|
||||||
definition User {
|
|
||||||
role: string
|
|
||||||
// Missing closing brace
|
|
||||||
`;
|
|
||||||
|
|
||||||
const validResult = parser.validate(validDSL);
|
|
||||||
assert.ok(validResult.success, 'Valid DSL should pass validation');
|
|
||||||
assert.ok(validResult.program !== null, 'Valid DSL should return program');
|
|
||||||
|
|
||||||
const invalidResult = parser.validate(invalidDSL);
|
|
||||||
assert.ok(!invalidResult.success, 'Invalid DSL should fail validation');
|
|
||||||
assert.ok(invalidResult.errors.length > 0, 'Should have validation errors');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,249 +0,0 @@
|
|||||||
# Evidence DSL Test Suite
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This comprehensive test suite follows a **structural linguistic approach** to validate the Evidence DSL (Domain Specific Language) for authorization policies. The tests are organized incrementally from basic language primitives to complex integration scenarios.
|
|
||||||
|
|
||||||
## Test Structure
|
|
||||||
|
|
||||||
### 1. Structural Linguistic Tests (`StructuralLinguisticTests.js`)
|
|
||||||
**Level: Comprehensive**
|
|
||||||
- **Lexical Primitives**: Identifiers, literals, keywords, whitespace
|
|
||||||
- **Basic Expressions**: Arithmetic, logical, comparison, temporal
|
|
||||||
- **Type System**: Definitions, fields, behaviors, caching
|
|
||||||
- **Fact System**: Declarations, properties, caching, limits
|
|
||||||
- **Evidence System**: Rules, defeasible logic, pattern matching
|
|
||||||
- **Measure System**: Aggregation, fusion, return types
|
|
||||||
- **Complex Integration**: Multi-feature combinations
|
|
||||||
|
|
||||||
### 2. Expression Tests (`ExpressionTests.js`)
|
|
||||||
**Level: Focused**
|
|
||||||
- Arithmetic operator precedence
|
|
||||||
- Logical operator precedence
|
|
||||||
- Comparison operators
|
|
||||||
- Temporal expressions
|
|
||||||
- Unary operators
|
|
||||||
- Attribute access
|
|
||||||
- Function calls
|
|
||||||
- Complex expressions
|
|
||||||
- Error handling
|
|
||||||
|
|
||||||
### 3. Definition Tests (`DefinitionTests.js`)
|
|
||||||
**Level: Focused**
|
|
||||||
- Basic type definitions
|
|
||||||
- Field types (string, number, boolean, timestamp, custom)
|
|
||||||
- Array types
|
|
||||||
- Behaviors (decay, blur, TTL)
|
|
||||||
- Caching (eager, lazy)
|
|
||||||
- Complex definitions
|
|
||||||
- Error handling
|
|
||||||
|
|
||||||
### 4. Fact Tests (`FactTests.js`)
|
|
||||||
**Level: Focused**
|
|
||||||
- Basic fact declarations
|
|
||||||
- Fact properties (transitive, symmetrical)
|
|
||||||
- Fact caching
|
|
||||||
- Fact limits
|
|
||||||
- Parameter types
|
|
||||||
- Complex facts
|
|
||||||
- Error handling
|
|
||||||
|
|
||||||
### 5. Evidence Tests (`EvidenceTests.js`)
|
|
||||||
**Level: Focused**
|
|
||||||
- Basic evidence rules
|
|
||||||
- Defeasible logic (ALWAYS, WHEN/UNLESS, REQUIRES)
|
|
||||||
- Pattern matching with wildcards
|
|
||||||
- Fusion strategies (min, max, majority, average)
|
|
||||||
- Complex evidence composition
|
|
||||||
- Error handling
|
|
||||||
|
|
||||||
### 6. Measure Tests (`MeasureTests.js`)
|
|
||||||
**Level: Focused**
|
|
||||||
- Basic measure definitions
|
|
||||||
- Return types
|
|
||||||
- Aggregation with different strategies
|
|
||||||
- Fusion with different strategies
|
|
||||||
- Complex measures
|
|
||||||
- Error handling
|
|
||||||
|
|
||||||
### 7. Integration Tests (`IntegrationTests.js`)
|
|
||||||
**Level: Integration**
|
|
||||||
- Complete authorization systems
|
|
||||||
- Multi-domain systems
|
|
||||||
- Hierarchical access patterns
|
|
||||||
- Similarity-based access
|
|
||||||
- Temporal access patterns
|
|
||||||
- Complex behaviors
|
|
||||||
- Performance scenarios
|
|
||||||
|
|
||||||
## Test Runner (`TestRunner.js`)
|
|
||||||
|
|
||||||
The test runner orchestrates all test suites and provides:
|
|
||||||
- **Comprehensive Testing**: Run all test suites
|
|
||||||
- **Selective Testing**: Run specific test suites
|
|
||||||
- **Level-based Testing**: Run tests by complexity level
|
|
||||||
- **Detailed Reporting**: Summary and detailed results
|
|
||||||
- **Coverage Analysis**: Language feature coverage
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Run All Tests
|
|
||||||
```javascript
|
|
||||||
import { runAllTests } from '../../../../../lib/src/ast/tests/tests/TestRunner.js';
|
|
||||||
|
|
||||||
const results = runAllTests(arbiter);
|
|
||||||
console.log(`Tests: ${results.passed}/${results.total} passed`);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run Specific Test Suites
|
|
||||||
```javascript
|
|
||||||
import { runSpecificTests } from '../../../../../lib/src/ast/tests/tests/TestRunner.js';
|
|
||||||
|
|
||||||
const results = runSpecificTests(arbiter, [
|
|
||||||
'Expression Tests',
|
|
||||||
'Definition Tests'
|
|
||||||
]);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run Tests by Level
|
|
||||||
```javascript
|
|
||||||
import { runTestsByLevel } from '../../../../../lib/src/ast/tests/tests/TestRunner.js';
|
|
||||||
|
|
||||||
// Run only focused tests
|
|
||||||
const results = runTestsByLevel(arbiter, 'focused');
|
|
||||||
|
|
||||||
// Run only integration tests
|
|
||||||
const results = runTestsByLevel(arbiter, 'integration');
|
|
||||||
```
|
|
||||||
|
|
||||||
## Language Feature Coverage
|
|
||||||
|
|
||||||
### ✅ Lexical Primitives
|
|
||||||
- Identifiers (simple, with underscores, with numbers)
|
|
||||||
- Literals (string, number, boolean, duration)
|
|
||||||
- Keywords (reserved words)
|
|
||||||
- Whitespace and comments
|
|
||||||
|
|
||||||
### ✅ Expression System
|
|
||||||
- Arithmetic operators (+, -, *, /) with precedence
|
|
||||||
- Logical operators (&&, ||, NOT) with precedence
|
|
||||||
- Comparison operators (==, !=, >, <, >=, <=)
|
|
||||||
- Temporal expressions (within)
|
|
||||||
- Unary operators (NOT, !)
|
|
||||||
- Attribute access (object.attribute)
|
|
||||||
- Function calls (predicate(args))
|
|
||||||
|
|
||||||
### ✅ Type System
|
|
||||||
- Type definitions with fields
|
|
||||||
- Field types (string, number, boolean, timestamp, custom)
|
|
||||||
- Array types (Type[])
|
|
||||||
- Behaviors (decay, blur, TTL)
|
|
||||||
- Caching directives (eager, lazy)
|
|
||||||
|
|
||||||
### ✅ Fact System
|
|
||||||
- Fact declarations with parameters
|
|
||||||
- Fact properties (transitive, symmetrical)
|
|
||||||
- Caching directives
|
|
||||||
- Limits for performance
|
|
||||||
- Parameter types
|
|
||||||
|
|
||||||
### ✅ Evidence System
|
|
||||||
- Basic evidence rules
|
|
||||||
- Defeasible logic (ALWAYS, WHEN/UNLESS, REQUIRES)
|
|
||||||
- Pattern matching with wildcards (*)
|
|
||||||
- Binding clauses (|variable|)
|
|
||||||
- With clauses (with condition)
|
|
||||||
- Limits for pattern matching
|
|
||||||
- Fusion strategies (min, max, majority, average)
|
|
||||||
|
|
||||||
### ✅ Measure System
|
|
||||||
- Measure definitions
|
|
||||||
- Return type specifications (PROVIDES)
|
|
||||||
- Aggregation with strategies (USING)
|
|
||||||
- Fusion with strategies
|
|
||||||
- Complex value computation
|
|
||||||
|
|
||||||
### ✅ Integration Features
|
|
||||||
- Multi-domain systems
|
|
||||||
- Hierarchical access patterns
|
|
||||||
- Similarity-based access
|
|
||||||
- Temporal access patterns
|
|
||||||
- Complex behavior combinations
|
|
||||||
- Performance optimization scenarios
|
|
||||||
|
|
||||||
## Test Philosophy
|
|
||||||
|
|
||||||
### Structural Linguistic Approach
|
|
||||||
The tests follow a structural linguistic methodology:
|
|
||||||
|
|
||||||
1. **Phonological Level**: Basic lexical elements (identifiers, literals)
|
|
||||||
2. **Morphological Level**: Word formation (operators, keywords)
|
|
||||||
3. **Syntactic Level**: Grammar rules (expressions, statements)
|
|
||||||
4. **Semantic Level**: Meaning (types, behaviors, logic)
|
|
||||||
5. **Pragmatic Level**: Usage (integration, real-world scenarios)
|
|
||||||
|
|
||||||
### Incremental Complexity
|
|
||||||
Tests progress from simple to complex:
|
|
||||||
- **Level 1**: Lexical primitives
|
|
||||||
- **Level 2**: Basic expressions
|
|
||||||
- **Level 3**: Type system
|
|
||||||
- **Level 4**: Fact system
|
|
||||||
- **Level 5**: Evidence system
|
|
||||||
- **Level 6**: Measure system
|
|
||||||
- **Level 7**: Complex integration
|
|
||||||
|
|
||||||
### Comprehensive Coverage
|
|
||||||
Each language feature is tested for:
|
|
||||||
- **Valid cases**: Correct syntax and semantics
|
|
||||||
- **Invalid cases**: Error handling and recovery
|
|
||||||
- **Edge cases**: Boundary conditions
|
|
||||||
- **Integration**: Multi-feature combinations
|
|
||||||
|
|
||||||
## Running Tests
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
- Node.js environment
|
|
||||||
- Arbiter instance for testing
|
|
||||||
- All dependencies installed
|
|
||||||
|
|
||||||
### Basic Usage
|
|
||||||
```bash
|
|
||||||
# Run all tests
|
|
||||||
npm test
|
|
||||||
|
|
||||||
# Run specific test file
|
|
||||||
node src/ast/tests/StructuralLinguisticTests.js
|
|
||||||
|
|
||||||
# Run with specific arbiter
|
|
||||||
node -e "
|
|
||||||
import { runAllTests } from '../../../../../lib/src/ast/tests/src/ast/tests/TestRunner.js';
|
|
||||||
const results = runAllTests(arbiter);
|
|
||||||
console.log(results);
|
|
||||||
"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test Output
|
|
||||||
The test runner provides:
|
|
||||||
- **Progress indicators**: Real-time test execution
|
|
||||||
- **Detailed results**: Pass/fail status for each test
|
|
||||||
- **Error reporting**: Specific error messages for failures
|
|
||||||
- **Performance metrics**: Execution time for each suite
|
|
||||||
- **Coverage analysis**: Language feature coverage
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
When adding new tests:
|
|
||||||
1. Follow the structural linguistic approach
|
|
||||||
2. Test both valid and invalid cases
|
|
||||||
3. Include error handling tests
|
|
||||||
4. Document test purpose and expected behavior
|
|
||||||
5. Maintain incremental complexity
|
|
||||||
6. Update coverage documentation
|
|
||||||
|
|
||||||
## Test Maintenance
|
|
||||||
|
|
||||||
- **Regular Updates**: Keep tests current with language changes
|
|
||||||
- **Performance Monitoring**: Track test execution time
|
|
||||||
- **Coverage Analysis**: Ensure comprehensive feature coverage
|
|
||||||
- **Error Handling**: Validate error messages and recovery
|
|
||||||
- **Integration Testing**: Test real-world scenarios
|
|
||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
evaluateBuiltIn,
|
evaluateBuiltIn,
|
||||||
getFunctionSignature,
|
getFunctionSignature,
|
||||||
BUILT_IN_FUNCTIONS
|
BUILT_IN_FUNCTIONS
|
||||||
} from '../../src/ast/interpreter/BuiltInFunctions.js';
|
} from '@arbiter/evidence-dsl/interpreter/BuiltInFunctions';
|
||||||
|
|
||||||
describe('Built-in Functions Registry', () => {
|
describe('Built-in Functions Registry', () => {
|
||||||
it('should identify built-in functions', () => {
|
it('should identify built-in functions', () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, test } from 'node:test';
|
import { describe, test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { validateDslText } from '../../src/ast/validation/DSLValidation.js';
|
import { validateDslText } from '@arbiter/evidence-dsl/validation/DSLValidation';
|
||||||
|
|
||||||
describe('DSL injectable predicates (* prefix)', () => {
|
describe('DSL injectable predicates (* prefix)', () => {
|
||||||
test('allows injectable predicates with * prefix in evidence bodies', () => {
|
test('allows injectable predicates with * prefix in evidence bodies', () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, test } from 'node:test';
|
import { describe, test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { validateDslText } from '../../src/ast/validation/DSLValidation.js';
|
import { validateDslText } from '@arbiter/evidence-dsl/validation/DSLValidation';
|
||||||
|
|
||||||
describe('DSL type guards', () => {
|
describe('DSL type guards', () => {
|
||||||
test('infix is guard narrows for attribute access', () => {
|
test('infix is guard narrows for attribute access', () => {
|
||||||
|
|||||||
@@ -151,9 +151,14 @@ describe('HyperbolicLRUCache Invalidation', () => {
|
|||||||
const bobTouchingAlice = arbiter.keyManager.createCompositeKey(bobId, 'can_read', aliceId);
|
const bobTouchingAlice = arbiter.keyManager.createCompositeKey(bobId, 'can_read', aliceId);
|
||||||
const carolKey = arbiter.keyManager.createCompositeKey(carolId, 'can_read', bobId);
|
const carolKey = arbiter.keyManager.createCompositeKey(carolId, 'can_read', bobId);
|
||||||
|
|
||||||
|
// Direct-check keys are rolling hashes; invalidation-by-node uses the
|
||||||
|
// per-node key index, so every injected key must be tracked.
|
||||||
arbiter.directCheckCache.set(aliceKey, { result: { possibility: 0.8 }, timestamp: Date.now() });
|
arbiter.directCheckCache.set(aliceKey, { result: { possibility: 0.8 }, timestamp: Date.now() });
|
||||||
|
arbiter._trackDirectCheckKey('can_read', aliceId, bobId, aliceKey);
|
||||||
arbiter.directCheckCache.set(bobTouchingAlice, { result: { possibility: 0.9 }, timestamp: Date.now() });
|
arbiter.directCheckCache.set(bobTouchingAlice, { result: { possibility: 0.9 }, timestamp: Date.now() });
|
||||||
|
arbiter._trackDirectCheckKey('can_read', bobId, aliceId, bobTouchingAlice);
|
||||||
arbiter.directCheckCache.set(carolKey, { result: { possibility: 0.7 }, timestamp: Date.now() });
|
arbiter.directCheckCache.set(carolKey, { result: { possibility: 0.7 }, timestamp: Date.now() });
|
||||||
|
arbiter._trackDirectCheckKey('can_read', carolId, bobId, carolKey);
|
||||||
|
|
||||||
assert.ok(arbiter.directCheckCache.has(aliceKey));
|
assert.ok(arbiter.directCheckCache.has(aliceKey));
|
||||||
assert.ok(arbiter.directCheckCache.has(bobTouchingAlice));
|
assert.ok(arbiter.directCheckCache.has(bobTouchingAlice));
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ describe('Authorization config consistency (rigor)', () => {
|
|||||||
))
|
))
|
||||||
],
|
],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('path-parity', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('path-parity', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ effort: 300, seed: 'authz-config-parity' , artifacts: { dir: '', persist: 'never' }});
|
).run({ effort: 300, seed: 'authz-config-parity' , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ describe('Authorization config consistency (rigor)', () => {
|
|||||||
))
|
))
|
||||||
],
|
],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('override-honored', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('override-honored', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ effort: 300, seed: 'authz-config-override' , artifacts: { dir: '', persist: 'never' }});
|
).run({ effort: 300, seed: 'authz-config-override' , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
@@ -168,7 +168,7 @@ describe('Authorization config consistency (rigor)', () => {
|
|||||||
))
|
))
|
||||||
],
|
],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('remediation-contract', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('remediation-contract', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ effort: 300, seed: 'authz-config-remediation' , artifacts: { dir: '', persist: 'never' }});
|
).run({ effort: 300, seed: 'authz-config-remediation' , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ describe('Authorization graph semantics (rigor)', () => {
|
|||||||
))
|
))
|
||||||
],
|
],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('direct-exact', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('direct-exact', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ effort: 400, seed: 'authz-graph-direct' , artifacts: { dir: '', persist: 'never' }});
|
).run({ effort: 400, seed: 'authz-graph-direct' , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ describe('Authorization graph semantics (rigor)', () => {
|
|||||||
rigor.fn('check', check, rigor.args(rigor.gen.int(2, 6)))
|
rigor.fn('check', check, rigor.args(rigor.gen.int(2, 6)))
|
||||||
],
|
],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('absent-denies', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('absent-denies', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ effort: 300, seed: 'authz-graph-absent' , artifacts: { dir: '', persist: 'never' }});
|
).run({ effort: 300, seed: 'authz-graph-absent' , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
@@ -144,7 +144,7 @@ describe('Authorization graph semantics (rigor)', () => {
|
|||||||
))
|
))
|
||||||
],
|
],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('weakest-link', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('weakest-link', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ effort: 400, seed: 'authz-graph-chain' , artifacts: { dir: '', persist: 'never' }});
|
).run({ effort: 400, seed: 'authz-graph-chain' , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@ describe('Authorization graph semantics (rigor)', () => {
|
|||||||
))
|
))
|
||||||
],
|
],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('disjunctive-max', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('disjunctive-max', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ effort: 500, seed: 'authz-graph-multipath' , artifacts: { dir: '', persist: 'never' }});
|
).run({ effort: 500, seed: 'authz-graph-multipath' , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
@@ -246,7 +246,7 @@ describe('Authorization graph semantics (rigor)', () => {
|
|||||||
))
|
))
|
||||||
],
|
],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('tus-weakest-link', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('tus-weakest-link', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ effort: 400, seed: 'authz-graph-tus' , artifacts: { dir: '', persist: 'never' }});
|
).run({ effort: 400, seed: 'authz-graph-tus' , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
@@ -285,7 +285,7 @@ describe('Authorization graph semantics (rigor)', () => {
|
|||||||
))
|
))
|
||||||
],
|
],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('revoke-invalidates', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('revoke-invalidates', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ effort: 300, seed: 'authz-graph-mutation' , artifacts: { dir: '', persist: 'never' }});
|
).run({ effort: 300, seed: 'authz-graph-mutation' , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ describe('Batch loading consistency (rigor)', () => {
|
|||||||
))
|
))
|
||||||
],
|
],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('batch-parity', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('batch-parity', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ effort: 400, seed: 'batch-parity' , artifacts: { dir: '', persist: 'never' }});
|
).run({ effort: 400, seed: 'batch-parity' , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
@@ -177,7 +177,7 @@ describe('Batch loading consistency (rigor)', () => {
|
|||||||
))
|
))
|
||||||
],
|
],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('batch-mutation-parity', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('batch-mutation-parity', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ effort: 400, seed: 'batch-mutation-parity' , artifacts: { dir: '', persist: 'never' }});
|
).run({ effort: 400, seed: 'batch-mutation-parity' , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
|
|||||||
@@ -290,7 +290,7 @@ describe('Binary (threshold) mode parity (rigor)', () => {
|
|||||||
))
|
))
|
||||||
],
|
],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('binary-normal-agreement', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('binary-normal-agreement', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ effort: 1200, seed: 'binary-mode-config-matrix' , artifacts: { dir: '', persist: 'never' }});
|
).run({ effort: 1200, seed: 'binary-mode-config-matrix' , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
@@ -349,7 +349,7 @@ describe('Binary (threshold) mode parity (rigor)', () => {
|
|||||||
))
|
))
|
||||||
],
|
],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('mutation-freshness', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('mutation-freshness', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ effort: 800, seed: 'binary-mode-mutation-parity' , artifacts: { dir: '', persist: 'never' }});
|
).run({ effort: 800, seed: 'binary-mode-mutation-parity' , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ describe('Cache correctness under mutation (rigor)', () => {
|
|||||||
rigor.fn('check', check, rigor.args(opGen))
|
rigor.fn('check', check, rigor.args(opGen))
|
||||||
],
|
],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('cache-parity', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('cache-parity', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ effort: 600, seed: 'cache-onoff-parity' , artifacts: { dir: '', persist: 'never' }});
|
).run({ effort: 600, seed: 'cache-onoff-parity' , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ describe('Cache correctness under mutation (rigor)', () => {
|
|||||||
))
|
))
|
||||||
],
|
],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('override-cache-fresh', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('override-cache-fresh', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ effort: 400, seed: 'cache-override-freshness' , artifacts: { dir: '', persist: 'never' }});
|
).run({ effort: 400, seed: 'cache-override-freshness' , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
@@ -205,7 +205,7 @@ describe('Cache correctness under mutation (rigor)', () => {
|
|||||||
))
|
))
|
||||||
],
|
],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('ttl-contract', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('ttl-contract', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ effort: 300, seed: 'cache-ttl-contract' , artifacts: { dir: '', persist: 'never' }});
|
).run({ effort: 300, seed: 'cache-ttl-contract' , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ describe('ChainRule evaluation (rigor)', () => {
|
|||||||
const report = await rigor.campaign(
|
const report = await rigor.campaign(
|
||||||
[rigor.fn('check', check, rigor.args())],
|
[rigor.fn('check', check, rigor.args())],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('empty-steps', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('empty-steps', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ seed: 'chain-rule-empty-steps', effort: 200 , artifacts: { dir: '', persist: 'never' }});
|
).run({ seed: 'chain-rule-empty-steps', effort: 200 , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ describe('ChainRule evaluation (rigor)', () => {
|
|||||||
rigor.args(rigor.gen.float({ min: 0, max: 1 }))
|
rigor.args(rigor.gen.float({ min: 0, max: 1 }))
|
||||||
)],
|
)],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('possibility-bounded', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('possibility-bounded', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ seed: 'chain-rule-possibility-bounded', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
).run({ seed: 'chain-rule-possibility-bounded', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ describe('ChainRule evaluation (rigor)', () => {
|
|||||||
const report = await rigor.campaign(
|
const report = await rigor.campaign(
|
||||||
[rigor.fn('check', check, rigor.args())],
|
[rigor.fn('check', check, rigor.args())],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('no-path', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('no-path', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ seed: 'chain-rule-no-path', effort: 500 , artifacts: { dir: '', persist: 'never' }});
|
).run({ seed: 'chain-rule-no-path', effort: 500 , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
@@ -154,7 +154,7 @@ describe('ChainRule evaluation (rigor)', () => {
|
|||||||
rigor.args(rigor.gen.float({ min: 0.01, max: 1 }))
|
rigor.args(rigor.gen.float({ min: 0.01, max: 1 }))
|
||||||
)],
|
)],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('one-step-pos', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('one-step-pos', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ seed: 'chain-rule-one-step', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
).run({ seed: 'chain-rule-one-step', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
@@ -204,7 +204,7 @@ describe('ChainRule evaluation (rigor)', () => {
|
|||||||
)
|
)
|
||||||
)],
|
)],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('two-step-chain', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('two-step-chain', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ seed: 'chain-rule-two-step', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
).run({ seed: 'chain-rule-two-step', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ describe('PartialGraphContext.getChallengeProof (rigor)', () => {
|
|||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant(
|
rigor.invariant(
|
||||||
'oracle-matches-brute-force',
|
'oracle-matches-brute-force',
|
||||||
({ error, errorMessage }) => !error && !errorMessage
|
({ actual }) => actual !== undefined
|
||||||
)
|
)
|
||||||
])
|
])
|
||||||
).run({ seed: 'challenge-proof-oracle', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
|
).run({ seed: 'challenge-proof-oracle', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
|
||||||
@@ -150,7 +150,7 @@ describe('PartialGraphContext.getChallengeProof (rigor)', () => {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('no-expired', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('no-expired', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ seed: 'challenge-proof-expired', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
).run({ seed: 'challenge-proof-expired', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
@@ -225,7 +225,7 @@ describe('PartialGraphContext.getChallengeProof (rigor)', () => {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('most-recent', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('most-recent', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ seed: 'challenge-proof-most-recent', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
).run({ seed: 'challenge-proof-most-recent', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
@@ -281,7 +281,7 @@ describe('PartialGraphContext.getChallengeProof (rigor)', () => {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('within-window', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('within-window', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ seed: 'challenge-proof-within-ms', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
|
).run({ seed: 'challenge-proof-within-ms', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ describe('ChallengeRule._resolveSubjectKey (rigor)', () => {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('subjectKey-wins', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('subjectKey-wins', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ seed: 'challenge-rule-subject-key-wins', effort: 1000 , artifacts: { dir: '', persist: 'never' }});
|
).run({ seed: 'challenge-rule-subject-key-wins', effort: 1000 , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ describe('ChallengeRule._resolveSubjectKey (rigor)', () => {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('subject-mapping', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('subject-mapping', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ seed: 'challenge-rule-subject-mapping', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
|
).run({ seed: 'challenge-rule-subject-mapping', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
@@ -197,7 +197,7 @@ describe('ChallengeRule._resolveWithinMs (rigor)', () => {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('within-units', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('within-units', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ seed: 'challenge-rule-within-units', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
).run({ seed: 'challenge-rule-within-units', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
@@ -260,7 +260,7 @@ describe('ChallengeRule._resolveWithinMs (rigor)', () => {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('within-priority', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('within-priority', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ seed: 'challenge-rule-within-priority', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
).run({ seed: 'challenge-rule-within-priority', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
@@ -293,7 +293,7 @@ describe('ChallengeRule._resolveWithinMs (rigor)', () => {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('null-when-absent', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('null-when-absent', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ seed: 'challenge-rule-null-when-absent', effort: 500 , artifacts: { dir: '', persist: 'never' }});
|
).run({ seed: 'challenge-rule-null-when-absent', effort: 500 , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
@@ -333,7 +333,7 @@ describe('ChallengeRule._buildRequirement (rigor)', () => {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('buildRequirement', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('buildRequirement', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ seed: 'challenge-rule-build-requirement', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
).run({ seed: 'challenge-rule-build-requirement', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ describe('check/explain agreement (rigor)', () => {
|
|||||||
))
|
))
|
||||||
],
|
],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('check-explain-agree', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('check-explain-agree', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ effort: 500, seed: 'explain-agreement' , artifacts: { dir: '', persist: 'never' }});
|
).run({ effort: 500, seed: 'explain-agreement' , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ describe('check/explain agreement (rigor)', () => {
|
|||||||
))
|
))
|
||||||
],
|
],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('used-facts-consistent', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('used-facts-consistent', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ effort: 600, seed: 'explain-used-facts' , artifacts: { dir: '', persist: 'never' }});
|
).run({ effort: 600, seed: 'explain-used-facts' , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
@@ -196,7 +196,7 @@ describe('check/explain agreement (rigor)', () => {
|
|||||||
))
|
))
|
||||||
],
|
],
|
||||||
rigor.crucible([
|
rigor.crucible([
|
||||||
rigor.invariant('remediation-consistent', ({ error, errorMessage }) => !error && !errorMessage)
|
rigor.invariant('remediation-consistent', ({ actual }) => actual !== undefined)
|
||||||
])
|
])
|
||||||
).run({ effort: 400, seed: 'explain-remediation' , artifacts: { dir: '', persist: 'never' }});
|
).run({ effort: 400, seed: 'explain-remediation' , artifacts: { dir: '', persist: 'never' }});
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user