evidence-dsl: extract Evidence DSL v2 compiler from @arbiter/core
The Evidence DSL (ADR-000) is a thin declarative layer that compiles to engine rule types. It has zero runtime coupling to the core engine (DSLCompiler takes an arbiter as a duck-typed argument; the only shared code was the ip-utils helpers, now local). Extracting it into its own package keeps the core artifact free of the DSL surface. - @arbiter/evidence-dsl depends on @arbiter/core (config formats are the compilation target) - deep-path exports for the compiler, parser, generator, validation, and built-in functions (the surface the core's DSL tests consume) - tests moved alongside; generate-parser script + peggy devDep local - CI: test on push, publish on v* tags
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
import { describe, test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { DSLCompiler } from '../src/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
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user