@synapsor/dsl 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -4
- package/dist/cli.js +19 -6
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +152 -10
- package/examples/billing-late-fee.synapsor +7 -3
- package/package.json +11 -2
package/README.md
CHANGED
|
@@ -5,20 +5,83 @@
|
|
|
5
5
|
The DSL is not the source of truth. It compiles to `@synapsor/spec` JSON, and
|
|
6
6
|
the generated JSON is validated by `@synapsor/spec`.
|
|
7
7
|
|
|
8
|
+
Part of the Synapsor OSS toolchain:
|
|
9
|
+
|
|
10
|
+
- [`@synapsor/runner`](https://www.npmjs.com/package/@synapsor/runner): local MCP runtime that serves compiled contracts.
|
|
11
|
+
- [`@synapsor/spec`](https://www.npmjs.com/package/@synapsor/spec): canonical contract schemas, types, and validation.
|
|
12
|
+
- [Source and issues on GitHub](https://github.com/Synapsor/Synapsor-Runner).
|
|
13
|
+
|
|
14
|
+
## Example
|
|
15
|
+
|
|
16
|
+
```sql
|
|
17
|
+
CREATE AGENT CONTEXT local_operator
|
|
18
|
+
BIND tenant_id FROM ENVIRONMENT SYNAPSOR_TENANT_ID REQUIRED
|
|
19
|
+
BIND principal FROM ENVIRONMENT SYNAPSOR_PRINCIPAL REQUIRED
|
|
20
|
+
TENANT BINDING tenant_id
|
|
21
|
+
PRINCIPAL BINDING principal
|
|
22
|
+
END
|
|
23
|
+
|
|
24
|
+
CREATE CAPABILITY billing.inspect_invoice
|
|
25
|
+
DESCRIPTION 'Inspect one invoice in the trusted tenant before proposing a waiver.'
|
|
26
|
+
RETURNS HINT 'Returns reviewed invoice fields plus evidence/query-audit handles.'
|
|
27
|
+
USING CONTEXT local_operator
|
|
28
|
+
SOURCE local_postgres
|
|
29
|
+
ON public.invoices
|
|
30
|
+
PRIMARY KEY id
|
|
31
|
+
TENANT KEY tenant_id
|
|
32
|
+
CONFLICT GUARD updated_at
|
|
33
|
+
LOOKUP invoice_id BY id
|
|
34
|
+
ARG invoice_id STRING REQUIRED MAX LENGTH 128 DESCRIPTION 'Invoice id such as INV-3001.'
|
|
35
|
+
ALLOW READ id, tenant_id, status, late_fee_cents, updated_at
|
|
36
|
+
REQUIRE EVIDENCE
|
|
37
|
+
MAX ROWS 1
|
|
38
|
+
END
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
A longer worked contract lives in
|
|
42
|
+
[`examples/billing-late-fee.synapsor`](https://github.com/Synapsor/Synapsor-Runner/blob/main/packages/dsl/examples/billing-late-fee.synapsor),
|
|
43
|
+
and the runner README walks the full compile → validate → bundle → serve flow.
|
|
44
|
+
|
|
8
45
|
## CLI
|
|
9
46
|
|
|
10
47
|
```bash
|
|
11
|
-
synapsor-dsl validate ./contract.synapsor
|
|
12
|
-
synapsor-dsl compile ./contract.synapsor --out ./synapsor.contract.json
|
|
48
|
+
synapsor-dsl validate ./contract.synapsor [--strict]
|
|
49
|
+
synapsor-dsl compile ./contract.synapsor --out ./synapsor.contract.json [--strict]
|
|
13
50
|
```
|
|
14
51
|
|
|
15
52
|
Runner also exposes:
|
|
16
53
|
|
|
17
54
|
```bash
|
|
18
|
-
synapsor-runner dsl validate ./contract.synapsor
|
|
19
|
-
synapsor-runner dsl compile ./contract.synapsor --out ./synapsor.contract.json
|
|
55
|
+
synapsor-runner dsl validate ./contract.synapsor [--strict]
|
|
56
|
+
synapsor-runner dsl compile ./contract.synapsor --out ./synapsor.contract.json [--strict]
|
|
20
57
|
```
|
|
21
58
|
|
|
59
|
+
`--strict` treats safety warnings as errors. Use it in CI for reviewed proposal
|
|
60
|
+
contracts.
|
|
61
|
+
|
|
62
|
+
## Programmatic API
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { compileAgentDsl, parseAgentDsl, validateAgentDsl, formatAgentDsl, AgentDslError } from "@synapsor/dsl";
|
|
66
|
+
import { assertValidContract, normalizeContract } from "@synapsor/spec";
|
|
67
|
+
|
|
68
|
+
const source = `CREATE AGENT CONTEXT ...`;
|
|
69
|
+
|
|
70
|
+
const result = validateAgentDsl(source); // { ok, errors, warnings } with line/column entries
|
|
71
|
+
const ast = parseAgentDsl(source); // AST with line/column spans
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const contract = compileAgentDsl(source); // @synapsor/spec contract JSON
|
|
75
|
+
assertValidContract(normalizeContract(contract));
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error instanceof AgentDslError) {
|
|
78
|
+
console.error(`${error.message} at ${error.line}:${error.column}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`formatAgentDsl(source)` returns a canonically formatted copy of the DSL text.
|
|
84
|
+
|
|
22
85
|
## Supported Preview Constructs
|
|
23
86
|
|
|
24
87
|
- `CREATE AGENT CONTEXT`
|
|
@@ -26,11 +89,16 @@ synapsor-runner dsl compile ./contract.synapsor --out ./synapsor.contract.json
|
|
|
26
89
|
- `CREATE AGENT WORKFLOW`
|
|
27
90
|
- `BIND ... FROM SESSION|ENVIRONMENT|CLOUD_SESSION|STATIC_DEV|HTTP_CLAIM`
|
|
28
91
|
- `USING CONTEXT`
|
|
92
|
+
- `DESCRIPTION`
|
|
93
|
+
- `RETURNS HINT`
|
|
29
94
|
- `ON schema.table`
|
|
30
95
|
- `PRIMARY KEY`
|
|
31
96
|
- `TENANT KEY`
|
|
32
97
|
- `CONFLICT GUARD`
|
|
33
98
|
- `ARG`
|
|
99
|
+
- `ARG ... DESCRIPTION`
|
|
100
|
+
- `ARG ... MIN ... MAX ...` for `NUMBER`
|
|
101
|
+
- `ARG ... MAX LENGTH ...` for `STRING`/`TEXT`
|
|
34
102
|
- `LOOKUP`
|
|
35
103
|
- `ALLOW READ`
|
|
36
104
|
- `KEEP OUT`
|
|
@@ -38,6 +106,8 @@ synapsor-runner dsl compile ./contract.synapsor --out ./synapsor.contract.json
|
|
|
38
106
|
- `PROPOSE ACTION`
|
|
39
107
|
- `ALLOW WRITE`
|
|
40
108
|
- `PATCH`
|
|
109
|
+
- `BOUND`
|
|
110
|
+
- `TRANSITION`
|
|
41
111
|
- `APPROVAL ROLE`
|
|
42
112
|
- `WRITEBACK DIRECT SQL|APP HANDLER|CLOUD WORKER|NONE`
|
|
43
113
|
- workflow `ALLOW CAPABILITY`
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
|
-
import {
|
|
3
|
+
import { compileAgentDslWithWarnings, validateAgentDsl } from "./index.js";
|
|
4
4
|
async function main(argv) {
|
|
5
5
|
const [command, target, ...rest] = argv;
|
|
6
6
|
if (!command || command === "--help" || command === "-h") {
|
|
@@ -17,20 +17,31 @@ async function main(argv) {
|
|
|
17
17
|
return 2;
|
|
18
18
|
}
|
|
19
19
|
const source = await fs.readFile(target, "utf8");
|
|
20
|
+
const strict = rest.includes("--strict");
|
|
20
21
|
if (command === "validate") {
|
|
21
22
|
const result = validateAgentDsl(source);
|
|
22
23
|
if (rest.includes("--json"))
|
|
23
24
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
24
|
-
else if (result.ok)
|
|
25
|
+
else if (result.ok) {
|
|
25
26
|
process.stdout.write(`dsl valid: ${target}\n`);
|
|
27
|
+
for (const warning of result.warnings)
|
|
28
|
+
process.stdout.write(`warning ${warning.line}:${warning.column} ${warning.code}: ${warning.message}\n`);
|
|
29
|
+
}
|
|
26
30
|
else {
|
|
27
31
|
process.stdout.write(`dsl invalid: ${target}\n`);
|
|
28
32
|
for (const error of result.errors)
|
|
29
33
|
process.stdout.write(`error ${error.line}:${error.column} ${error.code}: ${error.message}\n`);
|
|
30
34
|
}
|
|
31
|
-
return result.ok ? 0 : 1;
|
|
35
|
+
return result.ok && (!strict || result.warnings.length === 0) ? 0 : 1;
|
|
36
|
+
}
|
|
37
|
+
const result = compileAgentDslWithWarnings(source);
|
|
38
|
+
if (strict && result.warnings.length > 0) {
|
|
39
|
+
process.stdout.write(`dsl warnings treated as errors: ${target}\n`);
|
|
40
|
+
for (const warning of result.warnings)
|
|
41
|
+
process.stdout.write(`warning ${warning.line}:${warning.column} ${warning.code}: ${warning.message}\n`);
|
|
42
|
+
return 1;
|
|
32
43
|
}
|
|
33
|
-
const contract =
|
|
44
|
+
const contract = result.contract;
|
|
34
45
|
const text = `${JSON.stringify(contract, null, 2)}\n`;
|
|
35
46
|
const output = option(rest, "--out") ?? option(rest, "--output");
|
|
36
47
|
if (output) {
|
|
@@ -40,6 +51,8 @@ async function main(argv) {
|
|
|
40
51
|
else {
|
|
41
52
|
process.stdout.write(text);
|
|
42
53
|
}
|
|
54
|
+
for (const warning of result.warnings)
|
|
55
|
+
process.stderr.write(`warning ${warning.line}:${warning.column} ${warning.code}: ${warning.message}\n`);
|
|
43
56
|
return 0;
|
|
44
57
|
}
|
|
45
58
|
function option(args, name) {
|
|
@@ -50,8 +63,8 @@ function usage() {
|
|
|
50
63
|
process.stdout.write(`Synapsor DSL
|
|
51
64
|
|
|
52
65
|
Usage:
|
|
53
|
-
synapsor-dsl validate ./contract.synapsor
|
|
54
|
-
synapsor-dsl compile ./contract.synapsor --out ./synapsor.contract.json
|
|
66
|
+
synapsor-dsl validate ./contract.synapsor [--strict]
|
|
67
|
+
synapsor-dsl compile ./contract.synapsor --out ./synapsor.contract.json [--strict]
|
|
55
68
|
`);
|
|
56
69
|
}
|
|
57
70
|
main(process.argv.slice(2)).then((code) => {
|
package/dist/index.d.ts
CHANGED
|
@@ -17,7 +17,10 @@ export type AgentDslContextAst = {
|
|
|
17
17
|
};
|
|
18
18
|
export type AgentDslCapabilityAst = {
|
|
19
19
|
name: string;
|
|
20
|
+
line?: number;
|
|
20
21
|
kind: "read" | "proposal";
|
|
22
|
+
description?: string;
|
|
23
|
+
returnsHint?: string;
|
|
21
24
|
context: string;
|
|
22
25
|
source?: string;
|
|
23
26
|
schema: string;
|
|
@@ -33,6 +36,10 @@ export type AgentDslCapabilityAst = {
|
|
|
33
36
|
type: "string" | "number" | "boolean";
|
|
34
37
|
required?: boolean;
|
|
35
38
|
max_length?: number;
|
|
39
|
+
minimum?: number;
|
|
40
|
+
maximum?: number;
|
|
41
|
+
description?: string;
|
|
42
|
+
line?: number;
|
|
36
43
|
}>;
|
|
37
44
|
visibleFields: string[];
|
|
38
45
|
keptOutFields: string[];
|
|
@@ -45,6 +52,14 @@ export type AgentDslCapabilityAst = {
|
|
|
45
52
|
fixed?: string | number | boolean | null;
|
|
46
53
|
from_arg?: string;
|
|
47
54
|
}>;
|
|
55
|
+
numericBounds?: Record<string, {
|
|
56
|
+
minimum?: number;
|
|
57
|
+
maximum?: number;
|
|
58
|
+
}>;
|
|
59
|
+
transitionGuards?: Record<string, {
|
|
60
|
+
from_column?: string;
|
|
61
|
+
allowed: Record<string, string[]>;
|
|
62
|
+
}>;
|
|
48
63
|
approvalRole?: string;
|
|
49
64
|
writebackMode?: "direct_sql" | "app_handler" | "cloud_worker" | "none";
|
|
50
65
|
executor?: string;
|
|
@@ -73,6 +88,10 @@ export type ValidationResult = {
|
|
|
73
88
|
message: string;
|
|
74
89
|
}>;
|
|
75
90
|
};
|
|
91
|
+
export type AgentDslCompileResult = {
|
|
92
|
+
contract: SynapsorContract;
|
|
93
|
+
warnings: ValidationResult["warnings"];
|
|
94
|
+
};
|
|
76
95
|
export declare class AgentDslError extends Error {
|
|
77
96
|
readonly line: number;
|
|
78
97
|
readonly column: number;
|
|
@@ -81,6 +100,7 @@ export declare class AgentDslError extends Error {
|
|
|
81
100
|
}
|
|
82
101
|
export declare function parseAgentDsl(source: string): AgentDslAst;
|
|
83
102
|
export declare function compileAgentDsl(source: string): SynapsorContract;
|
|
103
|
+
export declare function compileAgentDslWithWarnings(source: string): AgentDslCompileResult;
|
|
84
104
|
export declare function validateAgentDsl(source: string): ValidationResult;
|
|
85
105
|
export declare function formatAgentDsl(source: string): string;
|
|
86
106
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -27,6 +27,9 @@ export function parseAgentDsl(source) {
|
|
|
27
27
|
return { contexts, capabilities, workflows };
|
|
28
28
|
}
|
|
29
29
|
export function compileAgentDsl(source) {
|
|
30
|
+
return compileAgentDslWithWarnings(source).contract;
|
|
31
|
+
}
|
|
32
|
+
export function compileAgentDslWithWarnings(source) {
|
|
30
33
|
const ast = parseAgentDsl(source);
|
|
31
34
|
const contexts = ast.contexts.map((context) => ({
|
|
32
35
|
name: context.name,
|
|
@@ -38,6 +41,8 @@ export function compileAgentDsl(source) {
|
|
|
38
41
|
const spec = {
|
|
39
42
|
name: capability.name,
|
|
40
43
|
kind: capability.kind,
|
|
44
|
+
...(capability.description ? { description: capability.description } : {}),
|
|
45
|
+
...(capability.returnsHint ? { returns_hint: capability.returnsHint } : {}),
|
|
41
46
|
context: capability.context,
|
|
42
47
|
...(capability.source ? { source: capability.source } : {}),
|
|
43
48
|
subject: {
|
|
@@ -47,7 +52,7 @@ export function compileAgentDsl(source) {
|
|
|
47
52
|
...(capability.tenantKey ? { tenant_key: capability.tenantKey } : {}),
|
|
48
53
|
...(capability.conflictKey ? { conflict_key: capability.conflictKey } : {}),
|
|
49
54
|
},
|
|
50
|
-
args: capability.args,
|
|
55
|
+
args: specArgsFromDsl(capability.args),
|
|
51
56
|
...(capability.lookup ? { lookup: { id_from_arg: capability.lookup.arg } } : {}),
|
|
52
57
|
visible_fields: capability.visibleFields,
|
|
53
58
|
...(capability.keptOutFields.length ? { kept_out_fields: capability.keptOutFields } : {}),
|
|
@@ -59,6 +64,8 @@ export function compileAgentDsl(source) {
|
|
|
59
64
|
action: capability.proposal.action,
|
|
60
65
|
allowed_fields: capability.proposal.allowedFields,
|
|
61
66
|
patch: capability.proposal.patch,
|
|
67
|
+
...(capability.proposal.numericBounds ? { numeric_bounds: capability.proposal.numericBounds } : {}),
|
|
68
|
+
...(capability.proposal.transitionGuards ? { transition_guards: capability.proposal.transitionGuards } : {}),
|
|
62
69
|
conflict_guard: capability.conflictKey ? { column: capability.conflictKey } : { weak_guard_ack: true },
|
|
63
70
|
approval: { mode: "human", required_role: capability.proposal.approvalRole ?? "local_reviewer" },
|
|
64
71
|
writeback: {
|
|
@@ -85,12 +92,12 @@ export function compileAgentDsl(source) {
|
|
|
85
92
|
...(workflows.length ? { workflows } : {}),
|
|
86
93
|
};
|
|
87
94
|
assertValidContract(contract);
|
|
88
|
-
return normalizeContract(contract);
|
|
95
|
+
return { contract: normalizeContract(contract), warnings: collectDslWarnings(ast) };
|
|
89
96
|
}
|
|
90
97
|
export function validateAgentDsl(source) {
|
|
91
98
|
try {
|
|
92
|
-
|
|
93
|
-
return { ok: true, errors: [], warnings:
|
|
99
|
+
const result = compileAgentDslWithWarnings(source);
|
|
100
|
+
return { ok: true, errors: [], warnings: result.warnings };
|
|
94
101
|
}
|
|
95
102
|
catch (error) {
|
|
96
103
|
if (error instanceof AgentDslError) {
|
|
@@ -179,6 +186,7 @@ function parseContextBlock(block) {
|
|
|
179
186
|
function parseCapabilityBlock(block) {
|
|
180
187
|
const capability = {
|
|
181
188
|
name: block.name,
|
|
189
|
+
line: block.line,
|
|
182
190
|
kind: "read",
|
|
183
191
|
context: "",
|
|
184
192
|
schema: "",
|
|
@@ -189,6 +197,16 @@ function parseCapabilityBlock(block) {
|
|
|
189
197
|
keptOutFields: [],
|
|
190
198
|
};
|
|
191
199
|
for (const item of block.body) {
|
|
200
|
+
const description = item.text.match(/^DESCRIPTION\s+'(.*)'$/i);
|
|
201
|
+
if (description) {
|
|
202
|
+
capability.description = description[1] ?? "";
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const returnsHint = item.text.match(/^RETURNS\s+HINT\s+'(.*)'$/i);
|
|
206
|
+
if (returnsHint) {
|
|
207
|
+
capability.returnsHint = returnsHint[1] ?? "";
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
192
210
|
const context = item.text.match(/^USING\s+CONTEXT\s+([A-Za-z_][A-Za-z0-9_.]*)$/i);
|
|
193
211
|
if (context?.[1]) {
|
|
194
212
|
capability.context = context[1];
|
|
@@ -220,13 +238,9 @@ function parseCapabilityBlock(block) {
|
|
|
220
238
|
capability.conflictKey = conflict[1];
|
|
221
239
|
continue;
|
|
222
240
|
}
|
|
223
|
-
const arg = item.text.match(/^ARG\s+([A-Za-z_][A-Za-z0-9_]*)\s+(STRING|TEXT|NUMBER|BOOLEAN|BOOL)
|
|
241
|
+
const arg = item.text.match(/^ARG\s+([A-Za-z_][A-Za-z0-9_]*)\s+(STRING|TEXT|NUMBER|BOOLEAN|BOOL)\b(.*)$/i);
|
|
224
242
|
if (arg?.[1] && arg[2]) {
|
|
225
|
-
capability.args[arg[1]] =
|
|
226
|
-
type: normalizeArgType(arg[2]),
|
|
227
|
-
...(item.text.match(/\sREQUIRED(?:\s|$)/i) ? { required: true } : {}),
|
|
228
|
-
...(arg[3] ? { max_length: Number(arg[3]) } : {}),
|
|
229
|
-
};
|
|
243
|
+
capability.args[arg[1]] = parseArgSpec(arg[1], arg[2], arg[3] ?? "", item.line);
|
|
230
244
|
continue;
|
|
231
245
|
}
|
|
232
246
|
const lookup = item.text.match(/^LOOKUP\s+([A-Za-z_][A-Za-z0-9_]*)\s+BY\s+([A-Za-z_][A-Za-z0-9_]*)$/i);
|
|
@@ -276,6 +290,30 @@ function parseCapabilityBlock(block) {
|
|
|
276
290
|
capability.proposal.allowedFields.push(patch[1]);
|
|
277
291
|
continue;
|
|
278
292
|
}
|
|
293
|
+
const bound = item.text.match(/^BOUND\s+([A-Za-z_][A-Za-z0-9_]*)\s+(-?\d+(?:\.\d+)?)?\s*\.\.\s*(-?\d+(?:\.\d+)?)?$/i);
|
|
294
|
+
if (bound?.[1]) {
|
|
295
|
+
ensureProposal(capability, item);
|
|
296
|
+
const minimum = bound[2] !== undefined ? Number(bound[2]) : undefined;
|
|
297
|
+
const maximum = bound[3] !== undefined ? Number(bound[3]) : undefined;
|
|
298
|
+
if (minimum === undefined && maximum === undefined)
|
|
299
|
+
throw dslError(item.line, 1, "BOUND_RANGE_REQUIRED", "BOUND requires minimum, maximum, or both, such as 1..2500");
|
|
300
|
+
capability.proposal.numericBounds ??= {};
|
|
301
|
+
capability.proposal.numericBounds[bound[1]] = {
|
|
302
|
+
...(minimum !== undefined ? { minimum } : {}),
|
|
303
|
+
...(maximum !== undefined ? { maximum } : {}),
|
|
304
|
+
};
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
const transition = item.text.match(/^TRANSITION\s+([A-Za-z_][A-Za-z0-9_]*)(?:\s+FROM\s+([A-Za-z_][A-Za-z0-9_]*))?\s+ALLOW\s+(.+)$/i);
|
|
308
|
+
if (transition?.[1] && transition[3]) {
|
|
309
|
+
ensureProposal(capability, item);
|
|
310
|
+
capability.proposal.transitionGuards ??= {};
|
|
311
|
+
capability.proposal.transitionGuards[transition[1]] = {
|
|
312
|
+
...(transition[2] ? { from_column: transition[2] } : {}),
|
|
313
|
+
allowed: parseTransitionAllowed(transition[3], item.line),
|
|
314
|
+
};
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
279
317
|
const approval = item.text.match(/^APPROVAL\s+ROLE\s+([A-Za-z_][A-Za-z0-9_.-]*)$/i);
|
|
280
318
|
if (approval?.[1]) {
|
|
281
319
|
ensureProposal(capability, item);
|
|
@@ -308,6 +346,40 @@ function parseCapabilityBlock(block) {
|
|
|
308
346
|
throw dslError(block.line, 1, "PROPOSAL_PATCH_REQUIRED", `${block.name} proposal requires at least one PATCH line`);
|
|
309
347
|
return capability;
|
|
310
348
|
}
|
|
349
|
+
function specArgsFromDsl(args) {
|
|
350
|
+
return Object.fromEntries(Object.entries(args).map(([name, arg]) => {
|
|
351
|
+
const { line: _line, ...spec } = arg;
|
|
352
|
+
return [name, spec];
|
|
353
|
+
}));
|
|
354
|
+
}
|
|
355
|
+
function collectDslWarnings(ast) {
|
|
356
|
+
const warnings = [];
|
|
357
|
+
for (const capability of ast.capabilities) {
|
|
358
|
+
if (capability.kind !== "proposal" || !capability.proposal)
|
|
359
|
+
continue;
|
|
360
|
+
const line = capability.line ?? 1;
|
|
361
|
+
if (!capability.description) {
|
|
362
|
+
warnings.push({ line, column: 1, code: "DESCRIPTION_RECOMMENDED", message: `${capability.name} is a proposal capability without DESCRIPTION.` });
|
|
363
|
+
}
|
|
364
|
+
if (!capability.returnsHint) {
|
|
365
|
+
warnings.push({ line, column: 1, code: "RETURNS_HINT_RECOMMENDED", message: `${capability.name} is a proposal capability without RETURNS HINT.` });
|
|
366
|
+
}
|
|
367
|
+
for (const [column, binding] of Object.entries(capability.proposal.patch)) {
|
|
368
|
+
if (!binding.from_arg)
|
|
369
|
+
continue;
|
|
370
|
+
const arg = capability.args[binding.from_arg];
|
|
371
|
+
if (arg?.type === "number" && arg.minimum === undefined && arg.maximum === undefined && capability.proposal.numericBounds?.[column] === undefined) {
|
|
372
|
+
warnings.push({
|
|
373
|
+
line: arg.line ?? line,
|
|
374
|
+
column: 1,
|
|
375
|
+
code: "NUMERIC_PATCH_BOUND_RECOMMENDED",
|
|
376
|
+
message: `${capability.name} patches ${column} from numeric arg ${binding.from_arg} without ARG MIN/MAX or BOUND ${column}.`,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return warnings;
|
|
382
|
+
}
|
|
311
383
|
function parseWorkflowBlock(block) {
|
|
312
384
|
const workflow = { name: block.name, context: "", allowedCapabilities: [] };
|
|
313
385
|
for (const item of block.body) {
|
|
@@ -365,6 +437,76 @@ function parsePatchBinding(raw) {
|
|
|
365
437
|
return { fixed: quoted[1] ?? "" };
|
|
366
438
|
return { fixed: trimmed };
|
|
367
439
|
}
|
|
440
|
+
function parseArgSpec(name, rawType, rawOptions, line) {
|
|
441
|
+
const type = normalizeArgType(rawType);
|
|
442
|
+
let rest = rawOptions.trim();
|
|
443
|
+
let description;
|
|
444
|
+
const descriptionMatch = rest.match(/\bDESCRIPTION\s+'([^']*)'/i);
|
|
445
|
+
if (descriptionMatch) {
|
|
446
|
+
description = descriptionMatch[1] ?? "";
|
|
447
|
+
rest = `${rest.slice(0, descriptionMatch.index)} ${rest.slice((descriptionMatch.index ?? 0) + descriptionMatch[0].length)}`.trim();
|
|
448
|
+
}
|
|
449
|
+
const required = /\bREQUIRED\b/i.test(rest);
|
|
450
|
+
rest = rest.replace(/\bREQUIRED\b/ig, " ").trim();
|
|
451
|
+
const maxLengthMatch = rest.match(/\bMAX\s+LENGTH\s+(\d+)\b/i);
|
|
452
|
+
const maxLength = maxLengthMatch?.[1] ? Number(maxLengthMatch[1]) : undefined;
|
|
453
|
+
if (maxLengthMatch)
|
|
454
|
+
rest = `${rest.slice(0, maxLengthMatch.index)} ${rest.slice((maxLengthMatch.index ?? 0) + maxLengthMatch[0].length)}`.trim();
|
|
455
|
+
const minMatch = rest.match(/\bMIN\s+(-?\d+(?:\.\d+)?)\b/i);
|
|
456
|
+
const minimum = minMatch?.[1] ? Number(minMatch[1]) : undefined;
|
|
457
|
+
if (minMatch)
|
|
458
|
+
rest = `${rest.slice(0, minMatch.index)} ${rest.slice((minMatch.index ?? 0) + minMatch[0].length)}`.trim();
|
|
459
|
+
const maxMatch = rest.match(/\bMAX\s+(-?\d+(?:\.\d+)?)\b/i);
|
|
460
|
+
const maximumOrLegacyLength = maxMatch?.[1] ? Number(maxMatch[1]) : undefined;
|
|
461
|
+
if (maxMatch)
|
|
462
|
+
rest = `${rest.slice(0, maxMatch.index)} ${rest.slice((maxMatch.index ?? 0) + maxMatch[0].length)}`.trim();
|
|
463
|
+
if (rest.trim().length > 0) {
|
|
464
|
+
throw dslError(line, 1, "ARG_OPTIONS_UNSUPPORTED", `unsupported ARG options for ${name}: ${rest.trim()}`);
|
|
465
|
+
}
|
|
466
|
+
if (type !== "number" && minimum !== undefined) {
|
|
467
|
+
throw dslError(line, 1, "ARG_MIN_REQUIRES_NUMBER", `ARG ${name} uses MIN, but MIN is only valid for NUMBER args`);
|
|
468
|
+
}
|
|
469
|
+
if (type === "boolean" && (maxLength !== undefined || maximumOrLegacyLength !== undefined)) {
|
|
470
|
+
throw dslError(line, 1, "ARG_MAX_INVALID_FOR_BOOLEAN", `ARG ${name} cannot use MAX or MAX LENGTH because BOOLEAN has no numeric or length bound`);
|
|
471
|
+
}
|
|
472
|
+
if (type === "number" && maxLength !== undefined) {
|
|
473
|
+
throw dslError(line, 1, "ARG_MAX_LENGTH_REQUIRES_TEXT", `ARG ${name} uses MAX LENGTH, but MAX LENGTH is only valid for STRING/TEXT args`);
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
type,
|
|
477
|
+
line,
|
|
478
|
+
...(required ? { required: true } : {}),
|
|
479
|
+
...(description !== undefined ? { description } : {}),
|
|
480
|
+
...(type === "number" && minimum !== undefined ? { minimum } : {}),
|
|
481
|
+
...(type === "number" && maximumOrLegacyLength !== undefined ? { maximum: maximumOrLegacyLength } : {}),
|
|
482
|
+
...(type !== "number" && maxLength !== undefined ? { max_length: maxLength } : {}),
|
|
483
|
+
...(type !== "number" && maxLength === undefined && maximumOrLegacyLength !== undefined ? { max_length: maximumOrLegacyLength } : {}),
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
function parseTransitionAllowed(raw, line) {
|
|
487
|
+
const allowed = {};
|
|
488
|
+
for (const item of raw.split(",").map((part) => part.trim()).filter(Boolean)) {
|
|
489
|
+
const match = item.match(/^(.+?)\s*->\s*(.+)$/);
|
|
490
|
+
if (!match?.[1] || !match[2]) {
|
|
491
|
+
throw dslError(line, 1, "TRANSITION_RULE_INVALID", `transition rule must use from -> to syntax: ${item}`);
|
|
492
|
+
}
|
|
493
|
+
const from = parseStateValue(match[1]);
|
|
494
|
+
const toValues = match[2].split("|").map(parseStateValue).filter(Boolean);
|
|
495
|
+
if (!from || toValues.length === 0) {
|
|
496
|
+
throw dslError(line, 1, "TRANSITION_RULE_INVALID", `transition rule must name non-empty states: ${item}`);
|
|
497
|
+
}
|
|
498
|
+
allowed[from] = toValues;
|
|
499
|
+
}
|
|
500
|
+
if (Object.keys(allowed).length === 0) {
|
|
501
|
+
throw dslError(line, 1, "TRANSITION_ALLOWED_REQUIRED", "TRANSITION requires at least one from -> to rule");
|
|
502
|
+
}
|
|
503
|
+
return allowed;
|
|
504
|
+
}
|
|
505
|
+
function parseStateValue(raw) {
|
|
506
|
+
const trimmed = raw.trim();
|
|
507
|
+
const quoted = trimmed.match(/^'(.*)'$/);
|
|
508
|
+
return (quoted?.[1] ?? trimmed).trim();
|
|
509
|
+
}
|
|
368
510
|
function normalizeBindingSource(source) {
|
|
369
511
|
const normalized = source.toUpperCase();
|
|
370
512
|
if (normalized === "ENV")
|
|
@@ -6,6 +6,8 @@ CREATE AGENT CONTEXT local_operator
|
|
|
6
6
|
END
|
|
7
7
|
|
|
8
8
|
CREATE CAPABILITY billing.inspect_invoice
|
|
9
|
+
DESCRIPTION 'Inspect one invoice in the trusted tenant before proposing a waiver.'
|
|
10
|
+
RETURNS HINT 'Returns reviewed invoice fields plus evidence/query-audit handles.'
|
|
9
11
|
USING CONTEXT local_operator
|
|
10
12
|
SOURCE local_postgres
|
|
11
13
|
ON public.invoices
|
|
@@ -13,7 +15,7 @@ CREATE CAPABILITY billing.inspect_invoice
|
|
|
13
15
|
TENANT KEY tenant_id
|
|
14
16
|
CONFLICT GUARD updated_at
|
|
15
17
|
LOOKUP invoice_id BY id
|
|
16
|
-
ARG invoice_id STRING REQUIRED MAX 128
|
|
18
|
+
ARG invoice_id STRING REQUIRED MAX LENGTH 128 DESCRIPTION 'Invoice id such as INV-3001.'
|
|
17
19
|
ALLOW READ id, tenant_id, customer_id, status, late_fee_cents, waiver_reason, updated_at
|
|
18
20
|
KEEP OUT card_token, internal_risk_score
|
|
19
21
|
REQUIRE EVIDENCE
|
|
@@ -21,6 +23,8 @@ CREATE CAPABILITY billing.inspect_invoice
|
|
|
21
23
|
END
|
|
22
24
|
|
|
23
25
|
CREATE CAPABILITY billing.propose_late_fee_waiver
|
|
26
|
+
DESCRIPTION 'Propose waiving one invoice late fee after inspecting invoice and policy evidence.'
|
|
27
|
+
RETURNS HINT 'Returns a review-required proposal id, exact diff, evidence handle, and source_database_changed:false.'
|
|
24
28
|
USING CONTEXT local_operator
|
|
25
29
|
SOURCE local_postgres
|
|
26
30
|
ON public.invoices
|
|
@@ -28,8 +32,8 @@ CREATE CAPABILITY billing.propose_late_fee_waiver
|
|
|
28
32
|
TENANT KEY tenant_id
|
|
29
33
|
CONFLICT GUARD updated_at
|
|
30
34
|
LOOKUP invoice_id BY id
|
|
31
|
-
ARG invoice_id STRING REQUIRED MAX 128
|
|
32
|
-
ARG waiver_reason TEXT REQUIRED MAX 500
|
|
35
|
+
ARG invoice_id STRING REQUIRED MAX LENGTH 128 DESCRIPTION 'Invoice id such as INV-3001.'
|
|
36
|
+
ARG waiver_reason TEXT REQUIRED MAX LENGTH 500 DESCRIPTION 'Business reason for the proposed waiver.'
|
|
33
37
|
ALLOW READ id, tenant_id, customer_id, status, late_fee_cents, waiver_reason, updated_at
|
|
34
38
|
KEEP OUT card_token, internal_risk_score
|
|
35
39
|
REQUIRE EVIDENCE
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synapsor/dsl",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Preview SQL-like Synapsor authoring DSL compiler for canonical @synapsor/spec contracts.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"package.json"
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@synapsor/spec": "^0.1.
|
|
19
|
+
"@synapsor/spec": "^0.1.2"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"typescript": "^5.7.0",
|
|
@@ -35,6 +35,15 @@
|
|
|
35
35
|
"publishConfig": {
|
|
36
36
|
"access": "public"
|
|
37
37
|
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/Synapsor/Synapsor-Runner.git",
|
|
41
|
+
"directory": "packages/dsl"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/Synapsor/Synapsor-Runner/tree/main/packages/dsl#readme",
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/Synapsor/Synapsor-Runner/issues"
|
|
46
|
+
},
|
|
38
47
|
"scripts": {
|
|
39
48
|
"build": "tsc -b",
|
|
40
49
|
"test": "vitest run"
|