@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 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 { compileAgentDsl, validateAgentDsl } from "./index.js";
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 = compileAgentDsl(source);
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
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
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
- compileAgentDsl(source);
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)(?:\s+REQUIRED)?(?:\s+MAX\s+(\d+))?$/i);
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.0",
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.0"
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"