10 Commits

Author SHA1 Message Date
John Dvorak bd9c74fb0e feat: tuple_to_userset intermediate reachability; ratio-based benchmark gate
CI / publish (push) Successful in 15s
CI / test (push) Successful in 5m7s
CI / benchmark (push) Successful in 53s
- ChainRule._expandRuleFromSrc now expands tuple_to_userset configs: from a
  source node the reachable set is the objects sharing an intermediate with
  the source (src ->computed-> intermediate ->tupleset-> object, direction
  aware, weakest-link combined). Lets a TTU evidence serve as an intermediate
  condition step in a chain.
- scripts/benchmark.js: ratio-based self-calibration. Comparing each action's
  RATIO to a cheap reference action (default check[direct-hit]) cancels
  machine-load swings that scale all actions proportionally, so the gate only
  fails on code regressions that shift a single action's ratio. The reference
  is still checked absolutely with a loose bound. Verified: stable across
  runs, and a simulated union-ttu slowdown is caught (+76.8% ratio).

Tests: chain-condition-step intermediate TTU expansion.
2026-08-03 15:43:48 -07:00
John Dvorak ab0e569552 bench: stabilize sub-ms measurements — more samples, no per-sample GC, warmup, batching, regression threshold
CI / test (push) Successful in 5m53s
CI / benchmark (push) Successful in 49s
CI / publish (push) Has been skipped
The CI benchmark gate was flagging wild run-to-run 'regressions'/'improvements'
(-23%..-83% on unchanged code) because sub-ms checks were measured from ~5
samples with a global.gc() injected between every iteration:

- uncertaintyThreshold 0.99 -> 0.1: the loop now keeps sampling until variance
  actually tightens instead of 'passing' at the first check.
- minSamples 0 -> 200, maxSamples 200 -> 2000: real sample floor + headroom.
- gcBetweenSamples true -> false: per-sample GC dominated sub-ms timings.
- warmup phase: each hot path runs to steady state (JIT, lazy index, caches)
  before sampling, eliminating the bimodal ~4us vs ~20us distribution.
- BATCH=100 for sub-ms checks: jitter amortizes across a batch per sample; the
  relative comparison stays exact because the baseline uses the same batch.
- minimum-change threshold (MIN_HIGH_REGRESSION_PERCENT, default 10): a
  high-severity flag only fails CI when the change exceeds run-to-run noise.

Result: within-run p95 spread is now ~5% instead of ~100x. Residual cross-run
variance on loaded shared runners (this machine: load ~19) is environmental —
the baseline alphaCuts capture per-run spread, not machine-load swings.
2026-08-03 14:02:31 -07:00
John Dvorak ed34df4474 feat: intermediate chain condition steps, graph-version cache invalidation, rolling-hash chain keys; fix vacuous rigor invariants
CI / benchmark (push) Successful in 48s
CI / test (push) Successful in 5m26s
CI / publish (push) Has been skipped
Chain intermediates (rule-based reachability):
- ChainRule: a condition step ({ rule, conditionStep }) at an INTERMEDIATE
  position is now EXPANDED from the current node — the rule's base edges'
  destinations, filtered by its defeaters/requirements — and traversal
  continues from each discovered node. Adds _expandRuleFromSrc / direct /
  logical(union/intersection) / defeasible / nested-chain expansion.
- RuleEvaluator: _subjectIsObject flag for unary predicate calls whose subject
  entity IS the object parameter (trusted(other) inside peer_trusted(user,
  other)); previously only subject-var unary calls (_subjectAsObject) were
  handled, so object-var unary defeaters never fired.

Graph-version cache invalidation:
- Arbiter gains a monotonic _graphVersion, incremented on every relation
  mutation. ChainRule result cache, RuleEvaluator rule-result cache, and
  DecisionCache rule cache now stamp entries with the graph version and treat
  any mismatch as a miss — graph mutations can no longer serve stale
  chain/authorization results.

Rolling-hash cache keys:
- UnifiedKeyManager.createChainKey now builds a 53-bit rolling hash (dual
  FNV-1a lanes, exact for ints/floats/strings/nested configs) instead of
  JSON.stringify — no string allocation or serialization on the chain-cache
  hot path. Composite keys stay structured strings because the direct-check
  cache pattern-invalidates by relation ID.

Rigor invariant migration (correctness):
- All 43 rigor test files' throw-based invariants ({ error, errorMessage } =>
  !error && !errorMessage) never saw fn throws — vacuous. Migrated to
  ({ actual }) => actual !== undefined, which fails on any thrown violation
  while passing legitimate null-skips. The migration immediately surfaced
  two latent bugs, now fixed:
    * node-manager/graph-indices skip paths returned bare undefined (falsy
      sentinel) — return { skipped: true }.
    * complex-graph-values-crucible expiry section rewrote values equal to the
      mutation loop's last write; the engine (by design) keeps the old
      timestamp on same-value rewrites so the pre-expiry grant never
      materialized. Now writes guaranteed-different values.
2026-08-03 13:26:42 -07:00
John Dvorak 4da3158c63 feat: ChainRule condition steps — rule-based final hops for chains
CI / publish (push) Has been skipped
CI / test (push) Successful in 5m43s
CI / benchmark (push) Successful in 35s
A chain step of the form { rule: <config>, conditionStep: true } is a
condition-gated hop instead of an edge traversal. It is valid only as the
FINAL step: the object is known, so the engine verifies the referenced rule
at (intermediate, object) for each current path. Enables a chain's object-side
hop to reference a defeasible/logical evidence (e.g.
member_of(user,*g){ gated(g,doc) } where gated is WHEN/UNLESS).

- RuleEvaluator wires the ChainRule with itself so condition steps can
  evaluate sub-rules recursively.
- ChainRule constructor accepts the ruleEvaluator; the traversal loop handles
  a final condition step (min-combined possibility, reliability product,
  threshold respect). Non-final condition steps return condition_step_not_final.
- The rule config is part of the chain cache key (JSON.stringify of steps).

Tests: chain-condition-step (grant/deny/missing-edge/non-final/parallel-max).
2026-08-03 12:00:16 -07:00
John Dvorak aa4ceff30c fix: defeasible reason codes, _subjectAsObject unary semantics, checker reason whitelist
CI / test (push) Successful in 6m16s
CI / benchmark (push) Successful in 37s
CI / publish (push) Successful in 10s
- LogicalOperators normal mode now emits top-level reason codes
  (never_rule_triggered / requirements_not_met / defeated_by_unless) when a
  defeasible rule resolves to 0, instead of degrading to no_matching_rule.
- RuleEvaluator honors rule._subjectAsObject: unary DSL predicate calls inside
  binary evidence (banned(user) within can_open(user, doc)) check the relation
  on the subject itself, matching the unary fact's self-edge.
- AuthorizationChecker reason whitelist now preserves the defeasible reason
  codes so the checker reports why a defeasible rule denied.
2026-08-03 10:49:20 -07:00
John Dvorak 41d2547bc8 docs: point the Evidence DSL reference to @arbiter/evidence-dsl
CI / test (push) Successful in 5m24s
CI / benchmark (push) Successful in 42s
CI / publish (push) Has been skipped
2026-08-03 09:18:13 -07:00
John Dvorak f446750ff3 refactor: extract Evidence DSL to @arbiter/evidence-dsl package
CI / test (push) Successful in 5m23s
CI / benchmark (push) Successful in 47s
CI / publish (push) Has been skipped
The Evidence DSL (ADR-000) compiles a natural DSL into core rule
configurations — it is a separate concern from the engine. The AST had
zero runtime coupling to the core (DSLCompiler takes the arbiter as a
duck-typed argument; ip-utils were the only shared code, now local to
the DSL package). This extraction removes the DSL surface from the core
artifact entirely:

- src/ast/ (748K, ~60 files) moved to @arbiter/evidence-dsl@1.0.0
- ip-utils moved with it (only the DSL consumed them)
- generate-parser script + peggy devDep moved to the DSL package
- the 8 DSL-consuming tests now import from @arbiter/evidence-dsl
  (deep-path exports: DSLCompiler, parser/*, generator/*, validation/*,
  interpreter/*)
- package.json gains the devDependency, drops build:ast/generate:parser

Tarball: AST-free. Rigor 251/251, full suite 838/776/0.
2026-08-03 09:17:33 -07:00
John Dvorak f8f6c5cb1b cleanup: remove dead code, stale shipped scaffolding, internal docs
CI / test (push) Successful in 5m45s
CI / benchmark (push) Successful in 43s
CI / publish (push) Has been skipped
Dead code with zero callers (deprecation notes promised removal):
- RelationCSR index: always-off option (useRelationCsrIndex), never
  enabled in production, wired through RelationManager/RelationUpdates/
  RelationLookup. Removed the module and all wiring.
- getAggregatedBlurredValue (RelationManager) and aggregateBlurredValues
  (ValueManager): @deprecated shims, zero callers.
- QualitativeRelationalComparatorRule._aggregateBlurredValues:
  @deprecated shim, zero callers.

Kept compareRelationValues: non-deprecated public API, coherent and
clock-threaded, just currently callerless.

Stale scaffolding shipping in the published artifact (files: src/):
- src/ast/tests/* and src/ast/examples/*: orphaned duplicates of
  tests/ast/, zero references anywhere, 11 files in the tarball.
  Removed; the live copies live in tests/ast/.

Internal docs moved out of the shipped surface (1266 lines) to
docs/internal/: VALUE_OPTIMIZATION_SUMMARY, rules API_SPECIFICATION,
ast README, qualitative README — repo-kept, not packaged.

Tarball .md count: 11 -> 1. Rigor 251/251, full suite 853/791/0.
2026-08-02 21:03:37 -07:00
John Dvorak 33dd15bba5 docs: full API reference + capabilities; export PartialGraphContext
CI / test (push) Successful in 5m32s
CI / benchmark (push) Successful in 40s
CI / publish (push) Has been skipped
docs/API.md is the authoritative interface reference: exports, Arbiter
methods, check options/result shape with contract invariants, all ten
relation configuration formats, valueManager, partial graphs (raw spec
+ pre-built context, precedence contract), snapshots (format, frozen
restore, trust boundary), reachability (null-defer contract), OWAFusion,
audit hook, errors, reason codes, and the validity taxonomy.

README gains a capabilities section (what the engine does and does not
do) and links the reference, per the map-vs-manual split.

The reference pass surfaced one interface gap: PartialGraphContext —
a first-class public type used in the pre-built overlay form — was not
exported. It is now part of the public surface.

Verified claims against source (overlay spec keys: challenges/
challengeProofs not proofs; qualitative routing via qualitative: true /
scaleName; simpleBinary shape; reason enum; config formats). Rigor
251/251, full suite 853/791/0.
2026-08-02 20:50:19 -07:00
John Dvorak 817601a0f3 ci: run complex-query cold-traffic benchmarks in the benchmark job
CI / test (push) Successful in 5m42s
CI / benchmark (push) Successful in 42s
CI / publish (push) Has been skipped
The prod-gating measurement (complex-query-bench: tuple-to-userset,
chains, defeasible, comparators, OWA, nested fusion, mixed unions over
cold traffic with binary parity) was committed but never ran in CI. The
benchmark job now runs it alongside the possibilistic baseline compare,
uploads the results artifact, and surfaces both in the run summary.

Also: refreshed the committed .rigor-baseline.json (engine unchanged
since the tag; the runner's canonical baseline lives in its artifact
store), and added npm run benchmark:complex-query for local runs.
2026-08-02 20:26:36 -07:00
134 changed files with 1567 additions and 30539 deletions
+13 -1
View File
@@ -70,6 +70,10 @@ jobs:
run: node --expose-gc scripts/benchmark.js --json > bench-results.json
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)
if: startsWith(github.ref, 'refs/tags/v')
run: node --expose-gc scripts/benchmark.js --save
@@ -87,7 +91,9 @@ jobs:
uses: actions/upload-artifact@v3
with:
name: bench-results
path: bench-results.json
path: |
bench-results.json
complex-query-results.json
retention-days: 30
- name: Benchmark summary
@@ -99,6 +105,12 @@ jobs:
cat bench-results.json >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
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:
runs-on: ubuntu-latest
+57 -57
View File
@@ -1,140 +1,140 @@
{
"version": 1,
"generated": "2026-08-02T19:51:19.208Z",
"generated": "2026-08-03T22:37:59.060Z",
"actions": {
"check[direct-hit]": {
"mostPlausible": 0.25810395981996936,
"mostPlausible": 0.10672237227527051,
"alphaCuts": {
"p50": {
"lower": 0.16434574601481775,
"upper": 0.40535024830520205
"lower": 0.105663874043709,
"upper": 0.10779178510155751
},
"p95": {
"lower": 0.10394999999999016,
"upper": 1.2158777106838974
"lower": 0.10367558838055704,
"upper": 0.10985914641251807
},
"p99": {
"lower": 0.10394999999999016,
"upper": 2.040950000000002
"lower": 0.10273583797909441,
"upper": 0.11086333384764993
}
}
},
"check[union-ttu]": {
"mostPlausible": 0.3581109933665416,
"mostPlausible": 0.11249228961043199,
"alphaCuts": {
"p50": {
"lower": 0.07645000000001517,
"upper": 0.35811139703768785
"lower": 0.111604019277894,
"upper": 0.11338800600185026
},
"p95": {
"lower": 0.07645000000001517,
"upper": 0.35811139703768785
"lower": 0.10993048219120201,
"upper": 0.11511402952231622
},
"p99": {
"lower": 0.07645000000001517,
"upper": 0.35811139703768785
"lower": 0.1091378967617418,
"upper": 0.11594970606842417
}
}
},
"check[denied-miss]": {
"mostPlausible": 0.21718442918988012,
"mostPlausible": 0.08747524001151907,
"alphaCuts": {
"p50": {
"lower": 0.18384889559816292,
"upper": 0.2565642603295502
"lower": 0.08668417849217541,
"upper": 0.08827353325683636
},
"p95": {
"lower": 0.13274999999997988,
"upper": 0.3601500000000823
"lower": 0.085196295997943,
"upper": 0.08981478022606769
},
"p99": {
"lower": 0.13274999999997988,
"upper": 0.3601500000000823
"lower": 0.08449268650925873,
"upper": 0.09056310270952082
}
}
},
"check[include-meta]": {
"mostPlausible": 0.9937660048895368,
"mostPlausible": 0.17368093893555905,
"alphaCuts": {
"p50": {
"lower": 0.9937660048895368,
"upper": 0.9937663566453343
"lower": 0.17235808249234505,
"upper": 0.17501399072522753
},
"p95": {
"lower": 0.9937660048895368,
"upper": 0.9937663566453343
"lower": 0.1698647154489402,
"upper": 0.17758260042495247
},
"p99": {
"lower": 0.9937660048895368,
"upper": 0.9937663566453343
"lower": 0.16868343694884977,
"upper": 0.178826872871734
}
}
},
"check[overlay-on-top]": {
"mostPlausible": 0.3485105914857449,
"mostPlausible": 0.34658090606556824,
"alphaCuts": {
"p50": {
"lower": 0.26044999999998425,
"upper": 0.3881187132654935
"lower": 0.343563635087355,
"upper": 0.3496248120197711
},
"p95": {
"lower": 0.26044999999998425,
"upper": 0.5286391152207875
"lower": 0.33788494447103923,
"upper": 0.3555006559358586
},
"p99": {
"lower": 0.26044999999998425,
"upper": 0.5374790233000701
"lower": 0.33519739588838326,
"upper": 0.35835082770353416
}
}
},
"check[binary-direct]": {
"mostPlausible": 0.2660225524868295,
"mostPlausible": 0.10136432040214981,
"alphaCuts": {
"p50": {
"lower": 0.21220996174732032,
"upper": 0.3334807973509898
"lower": 0.10038730449515523,
"upper": 0.10235072528492808
},
"p95": {
"lower": 0.16554999999993186,
"upper": 0.5780022226626057
"lower": 0.0985518364498951,
"upper": 0.10425698760463886
},
"p99": {
"lower": 0.16554999999993186,
"upper": 0.8219500000000078
"lower": 0.09768455509480933,
"upper": 0.10518292733500886
}
}
},
"snapshot[build-binary]": {
"mostPlausible": 4.119453383803492,
"mostPlausible": 2.848037533444555,
"alphaCuts": {
"p50": {
"lower": 3.638457178151043,
"upper": 4.664037328289517
"lower": 2.827413825511722,
"upper": 2.868811889350609
},
"p95": {
"lower": 3.1707499999999906,
"upper": 6.309350037482752
"lower": 2.788520370277795,
"upper": 2.9088252291455836
},
"p99": {
"lower": 3.1707499999999906,
"upper": 7.547749999999944
"lower": 2.770077133158435,
"upper": 2.9281936091667653
}
}
},
"snapshot[restore-binary]": {
"mostPlausible": 6.906283652168588,
"mostPlausible": 2.9488926124434385,
"alphaCuts": {
"p50": {
"lower": 3.2581500000001067,
"upper": 6.906283919208314
"lower": 2.9247809483999716,
"upper": 2.9732031186198484
},
"p95": {
"lower": 3.2581500000001067,
"upper": 6.906283919208314
"lower": 2.8793716893148757,
"upper": 3.020092288118393
},
"p99": {
"lower": 3.2581500000001067,
"upper": 6.906283919208314
"lower": 2.8578657882531426,
"upper": 3.0428172805315117
}
}
}
+17 -11
View File
@@ -1,6 +1,8 @@
# @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
@@ -72,23 +74,27 @@ Denied decisions never leak a source's reliability. `includeMeta: true` adds `me
## 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`
- **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)
- **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 |
|--------|------|---------|---------|
| `includeMeta` | `boolean` | `false` | Attach full provenance in `meta` |
| `explain` | `boolean` | `false` | Enrich `meta` with the evaluation trace |
| `binary` | `boolean` | `false` | Binary fast path; results marked `binary: true` |
| `now` | `number` | engine clock | Pinned temporal context for TTL gates |
| `partialGraphContext` | `PartialGraphContext` | none | Overlay taking precedence over the base graph |
The engine derives authorization decisions from a relation graph. What it does, in one pass:
- **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.
- **Possibilistic semantics**: decisions are maxitive rankings in `[0,1]`, not booleans — with reliability, validity provenance, and reason codes on every result.
- **Caller-owned time**: every TTL gate, decay, and proof expiry honors the caller's pinned `{ now }`; a rerun reproduces the decision.
- **Overlays**: evaluate "what if this evidence existed" without mutating the graph; caller-supplied facts ride the policy's direct relations.
- **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
+559
View File
@@ -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).
+22 -54
View File
@@ -1,12 +1,12 @@
{
"name": "@arbiter/core",
"version": "1.0.0",
"version": "1.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@arbiter/core",
"version": "1.0.0",
"version": "1.0.1",
"license": "ISC",
"dependencies": {
"@tenere/pltc-core": "^0.6.3",
@@ -14,21 +14,32 @@
"uuidv7": "^1.0.2"
},
"devDependencies": {
"@arbiter/evidence-dsl": "^1.0.0",
"@rigor/core": "*",
"@tenere/benchmark-lib": "^2.0.1",
"fast-check": "^4.5.3",
"peggy": "^5.0.6"
"fast-check": "^4.5.3"
}
},
"node_modules/@peggyjs/from-mem": {
"version": "3.1.3",
"node_modules/@arbiter/core": {
"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,
"license": "MIT",
"license": "ISC",
"dependencies": {
"semver": "7.7.4"
},
"engines": {
"node": ">=20.8"
"@tenere/pltc-core": "^0.6.3",
"heapify": "^1.0.2",
"uuidv7": "^1.0.2"
}
},
"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": {
@@ -332,14 +343,6 @@
"@tenere/graph-core": "^1.0.1"
}
},
"node_modules/commander": {
"version": "14.0.3",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
}
},
"node_modules/fast-check": {
"version": "4.9.0",
"dev": true,
@@ -365,22 +368,6 @@
"version": "1.0.2",
"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": {
"version": "8.4.2",
"dev": true,
@@ -396,25 +383,6 @@
],
"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": {
"version": "1.2.1",
"license": "Apache-2.0",
+4 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@arbiter/core",
"version": "1.0.1",
"version": "1.0.5",
"description": "Arbiter core engine: graph indices, relation/reachability, authorization rule evaluator, DSL/AST, condensed & sharded snapshots, and evidence fusion.",
"license": "ISC",
"author": "",
@@ -32,8 +32,6 @@
"test:property": "node --test --test-force-exit \"tests/property-based/**/*.test.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",
"generate:parser": "node scripts/generate-parser.js",
"build:ast": "npm run generate:parser",
"benchmark": "node --expose-gc scripts/benchmark.js",
"benchmark:core": "node benchmarks/core-performance-benchmark.js",
"benchmark:batch": "node benchmarks/batch-size-analysis.js",
@@ -49,7 +47,8 @@
"benchmark:memory": "node benchmarks/memory-breakdown.js",
"benchmark:multi-hop": "node benchmarks/multi-hop-rule-bench.js",
"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": [
"zanzibar",
@@ -68,6 +67,6 @@
"@rigor/core": "*",
"@tenere/benchmark-lib": "^2.0.1",
"fast-check": "^4.5.3",
"peggy": "^5.0.6"
"@arbiter/evidence-dsl": "^1.0.0"
}
}
+100 -16
View File
@@ -28,11 +28,24 @@ const BASELINE_PATH = (() => {
benchmark.config({
measurements: ['timing'],
uncertaintyThreshold: 0.99,
minSamples: 0,
maxSamples: 200,
// Stop collecting once 90% confident (was 0.99 ≈ "confident" immediately,
// so the loop stopped at the first check and the sub-ms checks were
// 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,
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
@@ -63,34 +76,64 @@ function 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 420µ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]', () => {
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]', () => {
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]', () => {
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]', () => {
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 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 })
for (let i = 0; i < BATCH; i++) {
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 })
}
})
const binaryBench = (() => {
const snap = buildEngine()
snap.enableCondensedSnapshot()
for (let i = 0; i < 5000; i++) snap.check('user:1', 'owner', 'doc:1', { binary: true })
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 regResult = detectRegressions(current, baseline)
if (!AS_JSON) console.log(formatRegressions(regResult, 'pretty'))
const critical = regResult.regressions.filter(r => r.severity === 'high')
if (critical.length > 0) {
console.error(`${critical.length} critical regression(s): ${critical.map(r => r.name).join(', ')}`)
// ── Ratio-based self-calibration ─────────────────────────────────────
// 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)
}
} else if (!SAVE) {
-51
View File
@@ -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);
}
-356
View File
@@ -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;
}
}
-352
View File
@@ -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();
-53
View File
@@ -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
-420
View File
@@ -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]*
/ "/*" (!"*/" .)* "*/"
-167
View File
@@ -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
= "/*" (!"*/" .)* "*/"
-22
View File
@@ -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';
-182
View File
@@ -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;
}
-167
View File
@@ -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();
}
}
-162
View File
@@ -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})`;
}
}
-157
View File
@@ -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;
}
}
-151
View File
@@ -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})`;
}
}
-150
View File
@@ -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})`;
}
}
-117
View File
@@ -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)`;
}
}
-78
View File
@@ -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})`;
}
}
-82
View File
@@ -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)`;
}
}
-117
View File
@@ -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})`;
}
}
-260
View File
@@ -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})`;
}
}
}
-166
View File
@@ -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})`;
}
}
-141
View File
@@ -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()})`;
}
}
-170
View File
@@ -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})`;
}
}
-130
View File
@@ -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(', ')})`;
}
}
-117
View File
@@ -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})`;
}
}
-79
View File
@@ -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()})`;
}
}
-142
View File
@@ -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})`;
}
}
-106
View File
@@ -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()})`;
}
}
-158
View File
@@ -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)`;
}
}
-87
View File
@@ -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})`;
}
}
-154
View File
@@ -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()})`;
}
}
-24
View File
@@ -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
-114
View File
@@ -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'
};
}
}
-45
View File
@@ -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;
}
}
}
-381
View File
@@ -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();
}
-464
View File
@@ -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();
}
-347
View File
@@ -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();
}
-330
View File
@@ -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();
}
-635
View File
@@ -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();
}
-393
View File
@@ -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();
}
-249
View File
@@ -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
-413
View File
@@ -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);
}
-37
View File
@@ -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
@@ -507,6 +507,12 @@ export class AuthorizationChecker {
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_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) {
maxAllow = resAllowPossibility;
+7 -1
View File
@@ -107,6 +107,11 @@ export class DecisionCache {
if (!this.ruleEnabled) return undefined;
const entry = this.arbiter.ruleResultCache.get(ruleCacheKey);
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) {
return undefined;
}
@@ -117,7 +122,8 @@ export class DecisionCache {
if (!this.ruleEnabled) return;
this.arbiter.ruleResultCache.set(ruleCacheKey, {
result,
timestamp: this.clock()
timestamp: this.clock(),
graphVersion: this.arbiter._graphVersion ?? 0
});
}
+27 -5
View File
@@ -20,7 +20,7 @@ export class RuleEvaluator {
tuple_to_userset: new TupleToUsersetRule(arbiter),
multi_hop: new MultiHopRule(arbiter),
relational_comparator: new RelationalComparatorRouter(arbiter, this),
chain: new ChainRule(arbiter),
chain: new ChainRule(arbiter, this),
challenge: new ChallengeRule(arbiter)
};
@@ -33,8 +33,25 @@ export class RuleEvaluator {
// Convert string keys to numeric IDs if needed
const numericUserId = typeof userId === 'string' ? this.arbiter.resolveNodeId(userId, options) : userId;
const numericObjectId = typeof objectId === 'string' ? this.arbiter.resolveNodeId(objectId, options) : objectId;
let numericUserId = typeof userId === 'string' ? this.arbiter.resolveNodeId(userId, options) : userId;
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
? true
@@ -56,7 +73,11 @@ export class RuleEvaluator {
if (canCacheRuleResult) {
const cached = this.arbiter.ruleResultCache.get(ruleCacheKey);
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++;
if (collectValues && finalValueContext && cached.result?.collectedValues?.length) {
const ruleType = rule?.type || (rule?.union ? 'union' : rule?.intersection ? 'intersection' : rule?.exclusion ? 'exclusion' : 'rule');
@@ -163,7 +184,8 @@ export class RuleEvaluator {
const cacheNow = this.arbiter.clock ? this.arbiter.clock() : Date.now();
this.arbiter.ruleResultCache.set(cacheKey, {
result,
timestamp: cacheNow
timestamp: cacheNow,
graphVersion: this.arbiter._graphVersion ?? 0
});
this.arbiter._cacheRuleResult(relation, cacheKey);
return result;
+291 -10
View File
@@ -44,8 +44,9 @@ import { QualitativeScale } from '../../qualitative/QualitativeScale.js';
* }
*/
export class ChainRule extends BaseRule {
constructor(arbiter) {
constructor(arbiter, ruleEvaluator = null) {
super(arbiter);
this.ruleEvaluator = ruleEvaluator;
// Chain-specific caching with HyperbolicLRUCache for better memory management
this.maxCacheSize = 2000;
@@ -200,6 +201,80 @@ export class ChainRule extends BaseRule {
: rawStep;
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) {
currentPaths = [];
break;
@@ -408,15 +483,214 @@ export class ChainRule extends BaseRule {
* Get relations for a step based on direction
* @private
*/
_getRelationsForStep(entityId, relation, direction, options = null) {
if (direction === 'out') {
return this.arbiter.relationManager.getRelationsFromSrc(entityId, relation, options);
} else if (direction === 'in') {
return this.arbiter.relationManager.getRelationsToDst(entityId, relation, options);
} else {
// Default to 'out' for backward compatibility
return this.arbiter.relationManager.getRelationsFromSrc(entityId, relation, options);
_getRelationsForStep(entityId, relation, direction, options = null) {
if (direction === 'out') {
return this.arbiter.relationManager.getRelationsFromSrc(entityId, relation, options);
} else if (direction === 'in') {
return this.arbiter.relationManager.getRelationsToDst(entityId, relation, options);
} else {
// Default to 'out' for backward compatibility
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 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();
if (entry && cacheNow - entry.timestamp < this.cacheTTL) {
return entry.result;
@@ -455,7 +735,8 @@ export class ChainRule extends BaseRule {
const cacheNow = this.arbiter.clock ? this.arbiter.clock() : Date.now();
this.chainResultCache.set(key, {
result,
timestamp: cacheNow
timestamp: cacheNow,
graphVersion: this.arbiter._graphVersion ?? 0
});
}
+21 -2
View File
@@ -805,6 +805,7 @@ export class LogicalOperators extends BaseRule {
return {
possibility: 0,
reliability,
reason: 'never_rule_triggered',
collectedValues: allCollectedValues,
meta: {
...resultMeta,
@@ -857,10 +858,28 @@ export class LogicalOperators extends BaseRule {
}
}
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 {
possibility: Math.max(0, Math.min(1, possibility)),
possibility: finalPossibility,
reliability,
validity: buildValidity('product', [], [], 2, Math.max(0, Math.min(1, possibility))),
reason,
validity: buildValidity('product', [], [], 2, finalPossibility),
collectedValues: allCollectedValues,
meta: {
...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
* @private
+7
View File
@@ -25,6 +25,13 @@ export class Arbiter {
constructor(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
// per check with a minimal decision record. The engine does NOT store
// audit state — the caller owns persistence and retention. The record
+6 -39
View File
@@ -1,4 +1,3 @@
import { RelationCSR } from './relation/RelationCSR.js';
import { RelationSnapshotAccess } from './relation/RelationSnapshotAccess.js';
import { RelationCaches } from './relation/RelationCaches.js';
import { RelationLookup } from './relation/RelationLookup.js';
@@ -22,16 +21,10 @@ export class RelationManager {
this._relationKeyToIndex = new Map();
this._relationNameToId = new Map();
this._relationCsr = new RelationCSR(this);
this._snapshotAccess = new RelationSnapshotAccess(this);
this._lookup = new RelationLookup(this);
this._updates = new RelationUpdates(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._cacheMisses = 0;
@@ -60,26 +53,6 @@ export class RelationManager {
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) {
this._ensureIndicesBuilt();
const indices = this.arbiter.indices;
@@ -525,17 +498,6 @@ export class RelationManager {
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
* @param {Object} leftRelation - Left relation object or { srcId, relation, dstId }
@@ -734,6 +696,12 @@ export class RelationManager {
* Cache management methods
*/
_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);
}
@@ -747,7 +715,6 @@ export class RelationManager {
_clearAllCaches() {
this._caches.clearAll();
this._relationCsr.clear();
this._relationGraph.clear();
}
+74 -2
View File
@@ -129,7 +129,7 @@ export class UnifiedKeyManager {
* @param {number} userId - User ID
* @param {number} objectId - Object ID
* @param {Array} steps - Chain steps
* @returns {number} Composite key
* @returns {number} Rolling-hash composite key
*/
createChainKey(userId, objectId, steps) {
if (userId > this.options.maxSrcId) {
@@ -139,7 +139,79 @@ export class UnifiedKeyManager {
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);
}
/**
-11
View File
@@ -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
* @param {Object} leftInterval - { min, max }
-154
View File
@@ -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();
}
}
-8
View File
@@ -47,10 +47,6 @@ export class RelationLookup {
const persistent = this.manager.arbiter.indices.getRelationsFromSrc(srcId, relation);
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
const cacheKey = this.manager._makeSrcRelCacheKey(srcId, relation);
const cached = this.manager._caches.relationLookupCache.get(cacheKey);
@@ -84,10 +80,6 @@ export class RelationLookup {
const persistent = this.manager.arbiter.indices.getRelationsToDst(dstId, relation);
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
const cacheKey = this.manager._makeDstRelCacheKey(dstId, relation);
const cached = this.manager._caches.relationLookupCache.get(cacheKey);
-3
View File
@@ -71,7 +71,6 @@ export class RelationUpdates {
this.manager.arbiter.indicesBuilt = true;
this.manager._addRelationToGraph(srcId, relation, dstId);
this.manager._recordRelationCsrAdd(relationObj);
// Update PLTC indices incrementally (if initialized and not in batch mode)
if (!this.manager.arbiter.batchUpdateInProgress) {
@@ -124,7 +123,6 @@ export class RelationUpdates {
this.manager._invalidateValueRelationCaches(srcId, dstId, relationName);
this.manager._removeRelationFromGraph(srcId, relationName, dstId);
this.manager._recordRelationCsrRemove(srcId, relationName, dstId);
// Notify ValueManager about the removed relation's last state
if (relationObjectToRemove.value !== undefined && relationObjectToRemove.stateId) {
@@ -368,7 +366,6 @@ export class RelationUpdates {
this.manager.arbiter.indicesBuilt = true;
this.manager._addRelationToGraph(srcId, relation, dstId);
this.manager._recordRelationCsrAdd(relationObj);
// 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)
+1
View File
@@ -1,4 +1,5 @@
export { Arbiter } from './core/Arbiter.js';
export { PartialGraphContext } from './core/PartialGraphContext.js';
export { GraphIndices } from './core/GraphIndices.js';
export { NodeManager } from './core/NodeManager.js';
export { RelationManager } from './core/RelationManager.js';
-165
View File
@@ -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';
-123
View File
@@ -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('.');
}
-265
View File
@@ -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');
});
});
-283
View File
@@ -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
}
});
});
});
-374
View File
@@ -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
}
});
});
});
-233
View File
@@ -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
}
});
});
});
-232
View File
@@ -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
}
});
});
});
-534
View File
@@ -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');
});
});
-306
View File
@@ -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
}
}
});
});
});
-101
View File
@@ -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');
});
});
-249
View File
@@ -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
+1 -1
View File
@@ -11,7 +11,7 @@ import {
evaluateBuiltIn,
getFunctionSignature,
BUILT_IN_FUNCTIONS
} from '../../src/ast/interpreter/BuiltInFunctions.js';
} from '@arbiter/evidence-dsl/interpreter/BuiltInFunctions';
describe('Built-in Functions Registry', () => {
it('should identify built-in functions', () => {
+1 -1
View File
@@ -1,6 +1,6 @@
import { describe, test } from 'node:test';
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)', () => {
test('allows injectable predicates with * prefix in evidence bodies', () => {
+1 -1
View File
@@ -1,6 +1,6 @@
import { describe, test } from 'node:test';
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', () => {
test('infix is guard narrows for attribute access', () => {
@@ -75,7 +75,7 @@ describe('Authorization config consistency (rigor)', () => {
))
],
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' }});
@@ -120,7 +120,7 @@ describe('Authorization config consistency (rigor)', () => {
))
],
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' }});
@@ -168,7 +168,7 @@ describe('Authorization config consistency (rigor)', () => {
))
],
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' }});
+6 -6
View File
@@ -66,7 +66,7 @@ describe('Authorization graph semantics (rigor)', () => {
))
],
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' }});
@@ -101,7 +101,7 @@ describe('Authorization graph semantics (rigor)', () => {
rigor.fn('check', check, rigor.args(rigor.gen.int(2, 6)))
],
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' }});
@@ -144,7 +144,7 @@ describe('Authorization graph semantics (rigor)', () => {
))
],
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' }});
@@ -194,7 +194,7 @@ describe('Authorization graph semantics (rigor)', () => {
))
],
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' }});
@@ -246,7 +246,7 @@ describe('Authorization graph semantics (rigor)', () => {
))
],
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' }});
@@ -285,7 +285,7 @@ describe('Authorization graph semantics (rigor)', () => {
))
],
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' }});
+2 -2
View File
@@ -129,7 +129,7 @@ describe('Batch loading consistency (rigor)', () => {
))
],
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' }});
@@ -177,7 +177,7 @@ describe('Batch loading consistency (rigor)', () => {
))
],
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' }});
+2 -2
View File
@@ -290,7 +290,7 @@ describe('Binary (threshold) mode parity (rigor)', () => {
))
],
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' }});
@@ -349,7 +349,7 @@ describe('Binary (threshold) mode parity (rigor)', () => {
))
],
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' }});
+3 -3
View File
@@ -113,7 +113,7 @@ describe('Cache correctness under mutation (rigor)', () => {
rigor.fn('check', check, rigor.args(opGen))
],
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' }});
@@ -159,7 +159,7 @@ describe('Cache correctness under mutation (rigor)', () => {
))
],
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' }});
@@ -205,7 +205,7 @@ describe('Cache correctness under mutation (rigor)', () => {
))
],
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' }});
+5 -5
View File
@@ -45,7 +45,7 @@ describe('ChainRule evaluation (rigor)', () => {
const report = await rigor.campaign(
[rigor.fn('check', check, rigor.args())],
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' }});
@@ -82,7 +82,7 @@ describe('ChainRule evaluation (rigor)', () => {
rigor.args(rigor.gen.float({ min: 0, max: 1 }))
)],
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' }});
@@ -117,7 +117,7 @@ describe('ChainRule evaluation (rigor)', () => {
const report = await rigor.campaign(
[rigor.fn('check', check, rigor.args())],
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' }});
@@ -154,7 +154,7 @@ describe('ChainRule evaluation (rigor)', () => {
rigor.args(rigor.gen.float({ min: 0.01, max: 1 }))
)],
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' }});
@@ -204,7 +204,7 @@ describe('ChainRule evaluation (rigor)', () => {
)
)],
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' }});
+4 -4
View File
@@ -108,7 +108,7 @@ describe('PartialGraphContext.getChallengeProof (rigor)', () => {
rigor.crucible([
rigor.invariant(
'oracle-matches-brute-force',
({ error, errorMessage }) => !error && !errorMessage
({ actual }) => actual !== undefined
)
])
).run({ seed: 'challenge-proof-oracle', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
@@ -150,7 +150,7 @@ describe('PartialGraphContext.getChallengeProof (rigor)', () => {
)
],
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' }});
@@ -225,7 +225,7 @@ describe('PartialGraphContext.getChallengeProof (rigor)', () => {
)
],
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' }});
@@ -281,7 +281,7 @@ describe('PartialGraphContext.getChallengeProof (rigor)', () => {
)
],
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' }});
+6 -6
View File
@@ -67,7 +67,7 @@ describe('ChallengeRule._resolveSubjectKey (rigor)', () => {
)
],
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' }});
@@ -116,7 +116,7 @@ describe('ChallengeRule._resolveSubjectKey (rigor)', () => {
)
],
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' }});
@@ -197,7 +197,7 @@ describe('ChallengeRule._resolveWithinMs (rigor)', () => {
)
],
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' }});
@@ -260,7 +260,7 @@ describe('ChallengeRule._resolveWithinMs (rigor)', () => {
)
],
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' }});
@@ -293,7 +293,7 @@ describe('ChallengeRule._resolveWithinMs (rigor)', () => {
)
],
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' }});
@@ -333,7 +333,7 @@ describe('ChallengeRule._buildRequirement (rigor)', () => {
)
],
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' }});
+3 -3
View File
@@ -78,7 +78,7 @@ describe('check/explain agreement (rigor)', () => {
))
],
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' }});
@@ -148,7 +148,7 @@ describe('check/explain agreement (rigor)', () => {
))
],
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' }});
@@ -196,7 +196,7 @@ describe('check/explain agreement (rigor)', () => {
))
],
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' }});
+2 -2
View File
@@ -113,7 +113,7 @@ describe('Relational comparator full-path parity (rigor)', () => {
))
],
rigor.crucible([
rigor.invariant('comparator-full-path', ({ error, errorMessage }) => !error && !errorMessage)
rigor.invariant('comparator-full-path', ({ actual }) => actual !== undefined)
])
).run({ effort: 1200, seed: 'comparator-full-path-parity' , artifacts: { dir: '', persist: 'never' }});
@@ -162,7 +162,7 @@ describe('Relational comparator full-path parity (rigor)', () => {
))
],
rigor.crucible([
rigor.invariant('comparator-aggregation', ({ error, errorMessage }) => !error && !errorMessage)
rigor.invariant('comparator-aggregation', ({ actual }) => actual !== undefined)
])
).run({ effort: 500, seed: 'comparator-aggregation' , artifacts: { dir: '', persist: 'never' }});
@@ -154,10 +154,10 @@ describe('Complex-graph batch/cache crucibles (rigor)', () => {
))
],
rigor.crucible([
rigor.invariant('batch-sequential-parity', ({ error, errorMessage }) => !error || !String(errorMessage).startsWith('[parity]')),
rigor.invariant('batch-mutation', ({ error, errorMessage }) => !error || !String(errorMessage).startsWith('[mutation]')),
rigor.invariant('cache-interaction', ({ error, errorMessage }) => !error || !String(errorMessage).startsWith('[cache]')),
rigor.invariant('fixture-size', ({ error, errorMessage }) => !error || !String(errorMessage).startsWith('[fixture]'))
rigor.invariant('batch-sequential-parity', ({ actual }) => actual !== undefined),
rigor.invariant('batch-mutation', ({ actual }) => actual !== undefined),
rigor.invariant('cache-interaction', ({ actual }) => actual !== undefined),
rigor.invariant('fixture-size', ({ actual }) => actual !== undefined)
])
).run({ effort: 150, seed: 'complex-graph-batch-crucible', artifacts: { dir: '', persist: 'never' } });
+1 -1
View File
@@ -127,7 +127,7 @@ describe('Complex-graph crucibles (rigor)', () => {
))
],
rigor.crucible([
rigor.invariant('parity', ({ error, errorMessage }) => !error && !errorMessage)
rigor.invariant('parity', ({ actual }) => actual !== undefined)
])
).run({ effort: 300, seed: 'complex-graph-parity', artifacts: { dir: '', persist: 'never' } });

Some files were not shown because too many files have changed in this diff Show More