@synapsor/runner 0.1.7 → 0.1.9
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/CHANGELOG.md +45 -1
- package/README.md +39 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/runner.mjs +413 -26
- package/docs/capability-authoring.md +39 -4
- package/docs/conformance.md +11 -0
- package/docs/local-mode.md +22 -0
- package/docs/migrating-to-synapsor-spec.md +1 -1
- package/docs/release-notes.md +39 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,51 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## 0.1.9
|
|
6
|
+
|
|
7
|
+
### CLI Hygiene
|
|
8
|
+
|
|
9
|
+
- Adds top-level `synapsor-runner --version`, `synapsor-runner -v`, and
|
|
10
|
+
`synapsor-runner version` output so published package checks do not look like
|
|
11
|
+
an unknown command.
|
|
12
|
+
|
|
13
|
+
## 0.1.8
|
|
14
|
+
|
|
15
|
+
### DSL / JSON Contract Parity
|
|
16
|
+
|
|
17
|
+
- Adds portable spec fields for capability `returns_hint`, proposal
|
|
18
|
+
`numeric_bounds`, and proposal `transition_guards` so reviewed safety
|
|
19
|
+
metadata can live in canonical contracts instead of runner-private config.
|
|
20
|
+
- Extends the DSL with `DESCRIPTION`, `RETURNS HINT`, arg descriptions,
|
|
21
|
+
numeric arg min/max, text `MAX LENGTH`, patch `BOUND`, and `TRANSITION`
|
|
22
|
+
clauses.
|
|
23
|
+
- Adds DSL warnings and `--strict` mode so weak proposal contracts fail CI
|
|
24
|
+
instead of silently compiling.
|
|
25
|
+
- Preserves compiled bounds through `contracts: []` into Runner propose-time
|
|
26
|
+
enforcement and accepts pure-contract configs with `capabilities: []`.
|
|
27
|
+
- Adds `docs/dsl-json-parity.md` so developers can see which fields are
|
|
28
|
+
authored in DSL, validated in JSON, enforced by Runner, and accepted by
|
|
29
|
+
C++/Cloud.
|
|
30
|
+
|
|
31
|
+
### Cloud Registry Push
|
|
32
|
+
|
|
33
|
+
- Wires non-dry-run `synapsor-runner cloud push` to the Synapsor Cloud control
|
|
34
|
+
API. The CLI validates locally, posts normalized `@synapsor/spec` JSON, and
|
|
35
|
+
reports Cloud contract/version/digest details only after the server confirms
|
|
36
|
+
storage.
|
|
37
|
+
- Keeps `--dry-run` network-free and updates error handling for invalid tokens,
|
|
38
|
+
missing workspace permissions, validation errors, conflicts, and network
|
|
39
|
+
failures without printing bearer tokens.
|
|
40
|
+
- Documents the project-scoped Cloud registry path and backend runner-bundle
|
|
41
|
+
export foundation.
|
|
42
|
+
|
|
43
|
+
### Release Verification
|
|
44
|
+
|
|
45
|
+
- Adds `corepack pnpm test:live-apply` as the documented Docker-backed live
|
|
46
|
+
apply smoke. It aliases the existing MCP local examples proof and verifies
|
|
47
|
+
proposal diffs, approval outside MCP, guarded writeback, idempotent retry,
|
|
48
|
+
stale-row conflict, receipts, and replay against disposable Postgres/MySQL
|
|
49
|
+
databases.
|
|
6
50
|
|
|
7
51
|
## 0.1.7
|
|
8
52
|
|
package/README.md
CHANGED
|
@@ -235,6 +235,8 @@ CREATE AGENT CONTEXT local_operator
|
|
|
235
235
|
END
|
|
236
236
|
|
|
237
237
|
CREATE CAPABILITY billing.inspect_invoice
|
|
238
|
+
DESCRIPTION 'Inspect one invoice in the trusted tenant before proposing a waiver.'
|
|
239
|
+
RETURNS HINT 'Returns reviewed invoice fields plus evidence/query-audit handles.'
|
|
238
240
|
USING CONTEXT local_operator
|
|
239
241
|
SOURCE local_postgres
|
|
240
242
|
ON public.invoices
|
|
@@ -242,13 +244,15 @@ CREATE CAPABILITY billing.inspect_invoice
|
|
|
242
244
|
TENANT KEY tenant_id
|
|
243
245
|
CONFLICT GUARD updated_at
|
|
244
246
|
LOOKUP invoice_id BY id
|
|
245
|
-
ARG invoice_id STRING REQUIRED MAX 128
|
|
247
|
+
ARG invoice_id STRING REQUIRED MAX LENGTH 128 DESCRIPTION 'Invoice id such as INV-3001.'
|
|
246
248
|
ALLOW READ id, tenant_id, status, late_fee_cents, updated_at
|
|
247
249
|
REQUIRE EVIDENCE
|
|
248
250
|
MAX ROWS 1
|
|
249
251
|
END
|
|
250
252
|
|
|
251
253
|
CREATE CAPABILITY billing.propose_late_fee_waiver
|
|
254
|
+
DESCRIPTION 'Propose waiving one invoice late fee after inspecting invoice and policy evidence.'
|
|
255
|
+
RETURNS HINT 'Returns a review-required proposal id, exact diff, evidence handle, and source_database_changed:false.'
|
|
252
256
|
USING CONTEXT local_operator
|
|
253
257
|
SOURCE local_postgres
|
|
254
258
|
ON public.invoices
|
|
@@ -256,8 +260,8 @@ CREATE CAPABILITY billing.propose_late_fee_waiver
|
|
|
256
260
|
TENANT KEY tenant_id
|
|
257
261
|
CONFLICT GUARD updated_at
|
|
258
262
|
LOOKUP invoice_id BY id
|
|
259
|
-
ARG invoice_id STRING REQUIRED MAX 128
|
|
260
|
-
ARG reason TEXT REQUIRED MAX 500
|
|
263
|
+
ARG invoice_id STRING REQUIRED MAX LENGTH 128 DESCRIPTION 'Invoice id such as INV-3001.'
|
|
264
|
+
ARG reason TEXT REQUIRED MAX LENGTH 500 DESCRIPTION 'Business reason for the proposed waiver.'
|
|
261
265
|
ALLOW READ id, tenant_id, status, late_fee_cents, waiver_reason, updated_at
|
|
262
266
|
REQUIRE EVIDENCE
|
|
263
267
|
MAX ROWS 1
|
|
@@ -265,6 +269,7 @@ CREATE CAPABILITY billing.propose_late_fee_waiver
|
|
|
265
269
|
ALLOW WRITE late_fee_cents, waiver_reason
|
|
266
270
|
PATCH late_fee_cents = 0
|
|
267
271
|
PATCH waiver_reason = ARG reason
|
|
272
|
+
BOUND late_fee_cents 0..10000
|
|
268
273
|
APPROVAL ROLE billing_lead
|
|
269
274
|
WRITEBACK DIRECT SQL
|
|
270
275
|
END
|
|
@@ -273,20 +278,33 @@ END
|
|
|
273
278
|
Compile, validate, and serve it:
|
|
274
279
|
|
|
275
280
|
```bash
|
|
276
|
-
synapsor-runner dsl compile ./contract.synapsor --out ./synapsor.contract.json
|
|
281
|
+
synapsor-runner dsl compile ./contract.synapsor --out ./synapsor.contract.json --strict
|
|
277
282
|
synapsor-runner contract validate ./synapsor.contract.json
|
|
278
283
|
synapsor-runner contract bundle ./synapsor.contract.json --out ./synapsor-runner-bundle
|
|
279
284
|
synapsor-runner cloud push ./synapsor.contract.json --dry-run
|
|
285
|
+
synapsor-runner cloud push ./synapsor.contract.json \
|
|
286
|
+
--api-url "$SYNAPSOR_CLOUD_BASE_URL" \
|
|
287
|
+
--token "$SYNAPSOR_CLOUD_TOKEN" \
|
|
288
|
+
--workspace "$SYNAPSOR_PROJECT_ID" \
|
|
289
|
+
--name billing-late-fee
|
|
280
290
|
cd ./synapsor-runner-bundle
|
|
281
291
|
cp .env.example .env # fill in and export your read-only database values
|
|
282
292
|
synapsor-runner mcp serve --config ./synapsor.runner.json --store ./.synapsor/local.db
|
|
283
293
|
```
|
|
284
294
|
|
|
295
|
+
`--strict` treats DSL safety warnings as errors, so CI catches proposal
|
|
296
|
+
capabilities that are missing descriptions, returns hints, or numeric patch
|
|
297
|
+
bounds.
|
|
298
|
+
|
|
285
299
|
The `contract bundle` step generates `synapsor.runner.json` (with env-var
|
|
286
300
|
placeholders) inside the bundle directory, which is why `mcp serve` runs from
|
|
287
301
|
there. The server keeps stdout clean for MCP protocol frames and prints its
|
|
288
302
|
ready line on stderr.
|
|
289
303
|
|
|
304
|
+
See [`docs/dsl-json-parity.md`](docs/dsl-json-parity.md) for the current
|
|
305
|
+
field-by-field support matrix across JSON spec, DSL, runner enforcement,
|
|
306
|
+
C++/Cloud compatibility, and Cloud push.
|
|
307
|
+
|
|
290
308
|
Your `synapsor.runner.json` supplies local wiring: database env var names,
|
|
291
309
|
SQLite store path, MCP transport settings, and local debug options. The
|
|
292
310
|
contract supplies the portable meaning: contexts, capabilities, evidence,
|
|
@@ -406,6 +424,18 @@ and proves the proposal-first write path:
|
|
|
406
424
|
npx -y -p @synapsor/runner synapsor-runner demo
|
|
407
425
|
```
|
|
408
426
|
|
|
427
|
+
For contributor/release verification from a checkout, the live apply smoke uses
|
|
428
|
+
disposable Postgres/MySQL containers and the official MCP stdio client transport:
|
|
429
|
+
|
|
430
|
+
```bash
|
|
431
|
+
corepack pnpm test:live-apply
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
It verifies semantic tool listing, proposal diffs, source rows unchanged before
|
|
435
|
+
approval, guarded writeback, idempotent retry, stale-row conflict, receipts, and
|
|
436
|
+
replay. See [`docs/local-mode.md`](docs/local-mode.md#local-mcp-smoke) for
|
|
437
|
+
prerequisites and expected output.
|
|
438
|
+
|
|
409
439
|
After the demo prints its generated config and store path, run the happy path it
|
|
410
440
|
prints. The shape is:
|
|
411
441
|
|
|
@@ -1205,6 +1235,11 @@ Portable contracts can be checked locally before Cloud import:
|
|
|
1205
1235
|
synapsor-runner contract validate ./synapsor.contract.json
|
|
1206
1236
|
synapsor-runner contract bundle ./synapsor.contract.json --out ./synapsor-runner-bundle
|
|
1207
1237
|
synapsor-runner cloud push ./synapsor.contract.json --dry-run
|
|
1238
|
+
synapsor-runner cloud push ./synapsor.contract.json \
|
|
1239
|
+
--api-url "$SYNAPSOR_CLOUD_BASE_URL" \
|
|
1240
|
+
--token "$SYNAPSOR_CLOUD_TOKEN" \
|
|
1241
|
+
--workspace "$SYNAPSOR_PROJECT_ID" \
|
|
1242
|
+
--name billing-late-fee
|
|
1208
1243
|
```
|
|
1209
1244
|
|
|
1210
1245
|
## Current Limitations
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAYA,OAAO,EAAoN,KAAK,WAAW,EAA2F,MAAM,6BAA6B,CAAC;AAoB1W,OAAO,EAA6G,KAAK,YAAY,EAAwB,MAAM,2BAA2B,CAAC;AAE/L,OAAO,EAOL,KAAK,gBAAgB,EAEtB,MAAM,mCAAmC,CAAC;AA6U3C,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAYA,OAAO,EAAoN,KAAK,WAAW,EAA2F,MAAM,6BAA6B,CAAC;AAoB1W,OAAO,EAA6G,KAAK,YAAY,EAAwB,MAAM,2BAA2B,CAAC;AAE/L,OAAO,EAOL,KAAK,gBAAgB,EAEtB,MAAM,mCAAmC,CAAC;AA6U3C,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAyD1D;AA+DD,KAAK,SAAS,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAE9E,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,GAAE;IACP,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;CACvC,GACL,OAAO,CAAC,MAAM,CAAC,CA2TjB;AAmsED,wBAAsB,0BAA0B,CAAC,GAAG,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAS/H"}
|
package/dist/runner.mjs
CHANGED
|
@@ -890,6 +890,7 @@ function validateExecutorAuth(value, path4, strict, errors) {
|
|
|
890
890
|
function validateCapabilities(value, sources, contexts, executors, mode, strict, errors, warnings, hasContracts = false) {
|
|
891
891
|
if (mode === "cloud" && value === void 0) return;
|
|
892
892
|
if (hasContracts && value === void 0) return;
|
|
893
|
+
if (hasContracts && Array.isArray(value) && value.length === 0) return;
|
|
893
894
|
if (!Array.isArray(value) || value.length === 0) {
|
|
894
895
|
errors.push({ path: "$.capabilities", code: "CAPABILITIES_REQUIRED", message: "At least one capability is required." });
|
|
895
896
|
return;
|
|
@@ -2812,13 +2813,15 @@ var METADATA_KEYS = /* @__PURE__ */ new Set(["name", "description", "version", "
|
|
|
2812
2813
|
var RESOURCE_KEYS = /* @__PURE__ */ new Set(["name", "engine", "schema", "table", "type", "primary_key", "tenant_key", "conflict_key", "single_tenant_dev"]);
|
|
2813
2814
|
var CONTEXT_KEYS2 = /* @__PURE__ */ new Set(["name", "description", "bindings", "tenant_binding", "principal_binding"]);
|
|
2814
2815
|
var BINDING_KEYS = /* @__PURE__ */ new Set(["name", "source", "key", "required"]);
|
|
2815
|
-
var CAPABILITY_KEYS2 = /* @__PURE__ */ new Set(["name", "description", "kind", "context", "source", "subject", "args", "lookup", "visible_fields", "kept_out_fields", "evidence", "max_rows", "proposal"]);
|
|
2816
|
+
var CAPABILITY_KEYS2 = /* @__PURE__ */ new Set(["name", "description", "returns_hint", "kind", "context", "source", "subject", "args", "lookup", "visible_fields", "kept_out_fields", "evidence", "max_rows", "proposal"]);
|
|
2816
2817
|
var SUBJECT_KEYS = /* @__PURE__ */ new Set(["resource", "schema", "table", "primary_key", "tenant_key", "conflict_key", "single_tenant_dev"]);
|
|
2817
2818
|
var ARG_KEYS2 = /* @__PURE__ */ new Set(["type", "description", "required", "max_length", "minimum", "maximum", "enum"]);
|
|
2818
2819
|
var LOOKUP_KEYS2 = /* @__PURE__ */ new Set(["id_from_arg"]);
|
|
2819
2820
|
var EVIDENCE_KEYS = /* @__PURE__ */ new Set(["required", "sources", "query_audit", "handle_prefix"]);
|
|
2820
|
-
var PROPOSAL_KEYS = /* @__PURE__ */ new Set(["action", "allowed_fields", "patch", "conflict_guard", "approval", "writeback"]);
|
|
2821
|
+
var PROPOSAL_KEYS = /* @__PURE__ */ new Set(["action", "allowed_fields", "patch", "numeric_bounds", "transition_guards", "conflict_guard", "approval", "writeback"]);
|
|
2821
2822
|
var PATCH_KEYS = /* @__PURE__ */ new Set(["fixed", "from_arg"]);
|
|
2823
|
+
var NUMERIC_BOUND_KEYS2 = /* @__PURE__ */ new Set(["minimum", "maximum"]);
|
|
2824
|
+
var TRANSITION_GUARD_KEYS2 = /* @__PURE__ */ new Set(["from_column", "allowed"]);
|
|
2822
2825
|
var CONFLICT_GUARD_KEYS2 = /* @__PURE__ */ new Set(["column", "weak_guard_ack"]);
|
|
2823
2826
|
var APPROVAL_KEYS2 = /* @__PURE__ */ new Set(["mode", "required_role"]);
|
|
2824
2827
|
var WRITEBACK_KEYS2 = /* @__PURE__ */ new Set(["mode", "executor", "idempotency_key"]);
|
|
@@ -2997,6 +3000,8 @@ function validateCapabilities2(value, contextNames, resourceNames, errors, warni
|
|
|
2997
3000
|
errors.push({ path: `${path4}.name`, code: "INVALID_CAPABILITY_NAME", message: "capability name must be namespace.name." });
|
|
2998
3001
|
else
|
|
2999
3002
|
addUnique(names, capability.name, `${path4}.name`, "DUPLICATE_CAPABILITY_NAME", errors);
|
|
3003
|
+
if (capability.returns_hint !== void 0 && !isNonEmptyString2(capability.returns_hint))
|
|
3004
|
+
errors.push({ path: `${path4}.returns_hint`, code: "INVALID_RETURNS_HINT", message: "returns_hint must be a non-empty string." });
|
|
3000
3005
|
if (!["read", "proposal", "external_action", "answer_with_evidence"].includes(String(capability.kind)))
|
|
3001
3006
|
errors.push({ path: `${path4}.kind`, code: "INVALID_CAPABILITY_KIND", message: "kind must be read, proposal, external_action, or answer_with_evidence." });
|
|
3002
3007
|
if (!isNonEmptyString2(capability.context) || !contextNames.has(capability.context))
|
|
@@ -3059,6 +3064,20 @@ function validateArgs2(value, path4, errors) {
|
|
|
3059
3064
|
checkUnknownKeys2(arg, ARG_KEYS2, argPath, errors);
|
|
3060
3065
|
if (!["string", "number", "boolean"].includes(String(arg.type)))
|
|
3061
3066
|
errors.push({ path: `${argPath}.type`, code: "INVALID_ARG_TYPE", message: "arg type must be string, number, or boolean." });
|
|
3067
|
+
if (arg.description !== void 0 && !isNonEmptyString2(arg.description))
|
|
3068
|
+
errors.push({ path: `${argPath}.description`, code: "INVALID_ARG_DESCRIPTION", message: "arg description must be a non-empty string." });
|
|
3069
|
+
if (arg.max_length !== void 0 && (!Number.isInteger(arg.max_length) || Number(arg.max_length) <= 0))
|
|
3070
|
+
errors.push({ path: `${argPath}.max_length`, code: "INVALID_MAX_LENGTH", message: "max_length must be a positive integer." });
|
|
3071
|
+
if ((arg.minimum !== void 0 || arg.maximum !== void 0) && arg.type !== "number")
|
|
3072
|
+
errors.push({ path: argPath, code: "NUMERIC_BOUNDS_REQUIRE_NUMBER", message: "minimum/maximum can only be used with number args." });
|
|
3073
|
+
if (arg.minimum !== void 0 && !isFiniteNumber2(arg.minimum))
|
|
3074
|
+
errors.push({ path: `${argPath}.minimum`, code: "INVALID_MINIMUM", message: "minimum must be a finite number." });
|
|
3075
|
+
if (arg.maximum !== void 0 && !isFiniteNumber2(arg.maximum))
|
|
3076
|
+
errors.push({ path: `${argPath}.maximum`, code: "INVALID_MAXIMUM", message: "maximum must be a finite number." });
|
|
3077
|
+
if (isFiniteNumber2(arg.minimum) && isFiniteNumber2(arg.maximum) && Number(arg.minimum) > Number(arg.maximum))
|
|
3078
|
+
errors.push({ path: argPath, code: "INVALID_ARG_NUMERIC_RANGE", message: "minimum must be less than or equal to maximum." });
|
|
3079
|
+
if (arg.enum !== void 0 && (!Array.isArray(arg.enum) || arg.enum.length === 0))
|
|
3080
|
+
errors.push({ path: `${argPath}.enum`, code: "INVALID_ARG_ENUM", message: "enum must be a non-empty array when provided." });
|
|
3062
3081
|
}
|
|
3063
3082
|
}
|
|
3064
3083
|
function validateLookup2(value, path4, args, errors) {
|
|
@@ -3109,6 +3128,8 @@ function validateProposalAction(value, path4, errors) {
|
|
|
3109
3128
|
errors.push({ path: patchPath, code: "PATCH_BINDING_REQUIRED", message: "patch binding must include fixed or from_arg." });
|
|
3110
3129
|
}
|
|
3111
3130
|
}
|
|
3131
|
+
validateNumericBounds2(value.numeric_bounds, value.patch, `${path4}.numeric_bounds`, errors);
|
|
3132
|
+
validateTransitionGuards2(value.transition_guards, value.patch, `${path4}.transition_guards`, errors);
|
|
3112
3133
|
if (value.conflict_guard !== void 0) {
|
|
3113
3134
|
if (!isRecord3(value.conflict_guard))
|
|
3114
3135
|
errors.push({ path: `${path4}.conflict_guard`, code: "CONFLICT_GUARD_NOT_OBJECT", message: "conflict_guard must be an object." });
|
|
@@ -3128,6 +3149,74 @@ function validateProposalAction(value, path4, errors) {
|
|
|
3128
3149
|
errors.push({ path: `${path4}.writeback.mode`, code: "INVALID_WRITEBACK_MODE", message: "writeback.mode must be direct_sql, app_handler, cloud_worker, or none." });
|
|
3129
3150
|
}
|
|
3130
3151
|
}
|
|
3152
|
+
function validateNumericBounds2(value, patch, path4, errors) {
|
|
3153
|
+
if (value === void 0)
|
|
3154
|
+
return;
|
|
3155
|
+
if (!isRecord3(value)) {
|
|
3156
|
+
errors.push({ path: path4, code: "NUMERIC_BOUNDS_NOT_OBJECT", message: "numeric_bounds must map patch fields to numeric bounds." });
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
3159
|
+
const patchFields = isRecord3(patch) ? new Set(Object.keys(patch)) : /* @__PURE__ */ new Set();
|
|
3160
|
+
for (const [field, bounds] of Object.entries(value)) {
|
|
3161
|
+
const boundPath = `${path4}.${field}`;
|
|
3162
|
+
if (!isSafeIdentifier2(field))
|
|
3163
|
+
errors.push({ path: boundPath, code: "INVALID_NUMERIC_BOUND_FIELD", message: "numeric_bounds keys must be safe patch fields." });
|
|
3164
|
+
if (!patchFields.has(field))
|
|
3165
|
+
errors.push({ path: boundPath, code: "NUMERIC_BOUND_PATCH_FIELD_REQUIRED", message: "numeric_bounds can only constrain fields in proposal.patch." });
|
|
3166
|
+
if (!isRecord3(bounds)) {
|
|
3167
|
+
errors.push({ path: boundPath, code: "NUMERIC_BOUND_NOT_OBJECT", message: "numeric bound must be an object." });
|
|
3168
|
+
continue;
|
|
3169
|
+
}
|
|
3170
|
+
checkUnknownKeys2(bounds, NUMERIC_BOUND_KEYS2, boundPath, errors);
|
|
3171
|
+
const hasMinimum = bounds.minimum !== void 0;
|
|
3172
|
+
const hasMaximum = bounds.maximum !== void 0;
|
|
3173
|
+
if (!hasMinimum && !hasMaximum)
|
|
3174
|
+
errors.push({ path: boundPath, code: "NUMERIC_BOUND_EMPTY", message: "numeric bound must define minimum, maximum, or both." });
|
|
3175
|
+
if (hasMinimum && !isFiniteNumber2(bounds.minimum))
|
|
3176
|
+
errors.push({ path: `${boundPath}.minimum`, code: "INVALID_MINIMUM", message: "minimum must be a finite number." });
|
|
3177
|
+
if (hasMaximum && !isFiniteNumber2(bounds.maximum))
|
|
3178
|
+
errors.push({ path: `${boundPath}.maximum`, code: "INVALID_MAXIMUM", message: "maximum must be a finite number." });
|
|
3179
|
+
if (isFiniteNumber2(bounds.minimum) && isFiniteNumber2(bounds.maximum) && Number(bounds.minimum) > Number(bounds.maximum)) {
|
|
3180
|
+
errors.push({ path: boundPath, code: "INVALID_NUMERIC_RANGE", message: "minimum must be less than or equal to maximum." });
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
function validateTransitionGuards2(value, patch, path4, errors) {
|
|
3185
|
+
if (value === void 0)
|
|
3186
|
+
return;
|
|
3187
|
+
if (!isRecord3(value)) {
|
|
3188
|
+
errors.push({ path: path4, code: "TRANSITION_GUARDS_NOT_OBJECT", message: "transition_guards must map patch fields to allowed state transitions." });
|
|
3189
|
+
return;
|
|
3190
|
+
}
|
|
3191
|
+
const patchFields = isRecord3(patch) ? new Set(Object.keys(patch)) : /* @__PURE__ */ new Set();
|
|
3192
|
+
for (const [field, guard] of Object.entries(value)) {
|
|
3193
|
+
const guardPath = `${path4}.${field}`;
|
|
3194
|
+
if (!isSafeIdentifier2(field))
|
|
3195
|
+
errors.push({ path: guardPath, code: "INVALID_TRANSITION_GUARD_FIELD", message: "transition_guards keys must be safe patch fields." });
|
|
3196
|
+
if (!patchFields.has(field))
|
|
3197
|
+
errors.push({ path: guardPath, code: "TRANSITION_GUARD_PATCH_FIELD_REQUIRED", message: "transition_guards can only constrain fields in proposal.patch." });
|
|
3198
|
+
if (!isRecord3(guard)) {
|
|
3199
|
+
errors.push({ path: guardPath, code: "TRANSITION_GUARD_NOT_OBJECT", message: "transition guard must be an object." });
|
|
3200
|
+
continue;
|
|
3201
|
+
}
|
|
3202
|
+
checkUnknownKeys2(guard, TRANSITION_GUARD_KEYS2, guardPath, errors);
|
|
3203
|
+
if (guard.from_column !== void 0 && !isSafeIdentifier2(guard.from_column)) {
|
|
3204
|
+
errors.push({ path: `${guardPath}.from_column`, code: "INVALID_TRANSITION_FROM_COLUMN", message: "from_column must be a safe identifier." });
|
|
3205
|
+
}
|
|
3206
|
+
if (!isRecord3(guard.allowed) || Object.keys(guard.allowed).length === 0) {
|
|
3207
|
+
errors.push({ path: `${guardPath}.allowed`, code: "TRANSITION_ALLOWED_REQUIRED", message: "transition guard must define allowed transitions." });
|
|
3208
|
+
continue;
|
|
3209
|
+
}
|
|
3210
|
+
for (const [from, toValues] of Object.entries(guard.allowed)) {
|
|
3211
|
+
const allowedPath = `${guardPath}.allowed.${from}`;
|
|
3212
|
+
if (!isNonEmptyString2(from))
|
|
3213
|
+
errors.push({ path: allowedPath, code: "TRANSITION_FROM_REQUIRED", message: "transition source state must be a non-empty string." });
|
|
3214
|
+
if (!Array.isArray(toValues) || toValues.length === 0 || toValues.some((item) => !isNonEmptyString2(item))) {
|
|
3215
|
+
errors.push({ path: allowedPath, code: "TRANSITION_TO_VALUES_REQUIRED", message: "transition target states must be non-empty strings." });
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3131
3220
|
function validateWorkflows(value, contextNames, capabilityNames, errors) {
|
|
3132
3221
|
if (value === void 0)
|
|
3133
3222
|
return;
|
|
@@ -3311,6 +3400,9 @@ function isQualifiedOrSafeName(value) {
|
|
|
3311
3400
|
function isPositiveInteger2(value) {
|
|
3312
3401
|
return Number.isInteger(value) && Number(value) > 0;
|
|
3313
3402
|
}
|
|
3403
|
+
function isFiniteNumber2(value) {
|
|
3404
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
3405
|
+
}
|
|
3314
3406
|
|
|
3315
3407
|
// packages/spec/dist/normalize.js
|
|
3316
3408
|
function normalizeContract(input) {
|
|
@@ -3428,6 +3520,7 @@ function runtimeCapabilityFromSpec(capability, resources, config) {
|
|
|
3428
3520
|
name: capability.name,
|
|
3429
3521
|
kind: capability.kind === "proposal" ? "proposal" : "read",
|
|
3430
3522
|
...capability.description ? { description: capability.description } : {},
|
|
3523
|
+
...capability.returns_hint ? { returns_hint: capability.returns_hint } : {},
|
|
3431
3524
|
source,
|
|
3432
3525
|
context: capability.context,
|
|
3433
3526
|
target: target2,
|
|
@@ -3440,6 +3533,8 @@ function runtimeCapabilityFromSpec(capability, resources, config) {
|
|
|
3440
3533
|
if (capability.kind === "proposal" && capability.proposal) {
|
|
3441
3534
|
runtime.patch = capability.proposal.patch;
|
|
3442
3535
|
runtime.allowed_columns = capability.proposal.allowed_fields;
|
|
3536
|
+
runtime.numeric_bounds = capability.proposal.numeric_bounds;
|
|
3537
|
+
runtime.transition_guards = capability.proposal.transition_guards;
|
|
3443
3538
|
runtime.conflict_guard = capability.proposal.conflict_guard;
|
|
3444
3539
|
runtime.approval = capability.proposal.approval;
|
|
3445
3540
|
runtime.writeback = {
|
|
@@ -6590,7 +6685,7 @@ function parseAgentDsl(source) {
|
|
|
6590
6685
|
}
|
|
6591
6686
|
return { contexts, capabilities, workflows };
|
|
6592
6687
|
}
|
|
6593
|
-
function
|
|
6688
|
+
function compileAgentDslWithWarnings(source) {
|
|
6594
6689
|
const ast = parseAgentDsl(source);
|
|
6595
6690
|
const contexts = ast.contexts.map((context) => ({
|
|
6596
6691
|
name: context.name,
|
|
@@ -6602,6 +6697,8 @@ function compileAgentDsl(source) {
|
|
|
6602
6697
|
const spec = {
|
|
6603
6698
|
name: capability.name,
|
|
6604
6699
|
kind: capability.kind,
|
|
6700
|
+
...capability.description ? { description: capability.description } : {},
|
|
6701
|
+
...capability.returnsHint ? { returns_hint: capability.returnsHint } : {},
|
|
6605
6702
|
context: capability.context,
|
|
6606
6703
|
...capability.source ? { source: capability.source } : {},
|
|
6607
6704
|
subject: {
|
|
@@ -6611,7 +6708,7 @@ function compileAgentDsl(source) {
|
|
|
6611
6708
|
...capability.tenantKey ? { tenant_key: capability.tenantKey } : {},
|
|
6612
6709
|
...capability.conflictKey ? { conflict_key: capability.conflictKey } : {}
|
|
6613
6710
|
},
|
|
6614
|
-
args: capability.args,
|
|
6711
|
+
args: specArgsFromDsl(capability.args),
|
|
6615
6712
|
...capability.lookup ? { lookup: { id_from_arg: capability.lookup.arg } } : {},
|
|
6616
6713
|
visible_fields: capability.visibleFields,
|
|
6617
6714
|
...capability.keptOutFields.length ? { kept_out_fields: capability.keptOutFields } : {},
|
|
@@ -6623,6 +6720,8 @@ function compileAgentDsl(source) {
|
|
|
6623
6720
|
action: capability.proposal.action,
|
|
6624
6721
|
allowed_fields: capability.proposal.allowedFields,
|
|
6625
6722
|
patch: capability.proposal.patch,
|
|
6723
|
+
...capability.proposal.numericBounds ? { numeric_bounds: capability.proposal.numericBounds } : {},
|
|
6724
|
+
...capability.proposal.transitionGuards ? { transition_guards: capability.proposal.transitionGuards } : {},
|
|
6626
6725
|
conflict_guard: capability.conflictKey ? { column: capability.conflictKey } : { weak_guard_ack: true },
|
|
6627
6726
|
approval: { mode: "human", required_role: capability.proposal.approvalRole ?? "local_reviewer" },
|
|
6628
6727
|
writeback: {
|
|
@@ -6649,12 +6748,12 @@ function compileAgentDsl(source) {
|
|
|
6649
6748
|
...workflows.length ? { workflows } : {}
|
|
6650
6749
|
};
|
|
6651
6750
|
assertValidContract(contract);
|
|
6652
|
-
return normalizeContract(contract);
|
|
6751
|
+
return { contract: normalizeContract(contract), warnings: collectDslWarnings(ast) };
|
|
6653
6752
|
}
|
|
6654
6753
|
function validateAgentDsl(source) {
|
|
6655
6754
|
try {
|
|
6656
|
-
|
|
6657
|
-
return { ok: true, errors: [], warnings:
|
|
6755
|
+
const result = compileAgentDslWithWarnings(source);
|
|
6756
|
+
return { ok: true, errors: [], warnings: result.warnings };
|
|
6658
6757
|
} catch (error) {
|
|
6659
6758
|
if (error instanceof AgentDslError) {
|
|
6660
6759
|
return {
|
|
@@ -6739,6 +6838,7 @@ function parseContextBlock(block) {
|
|
|
6739
6838
|
function parseCapabilityBlock(block) {
|
|
6740
6839
|
const capability = {
|
|
6741
6840
|
name: block.name,
|
|
6841
|
+
line: block.line,
|
|
6742
6842
|
kind: "read",
|
|
6743
6843
|
context: "",
|
|
6744
6844
|
schema: "",
|
|
@@ -6749,6 +6849,16 @@ function parseCapabilityBlock(block) {
|
|
|
6749
6849
|
keptOutFields: []
|
|
6750
6850
|
};
|
|
6751
6851
|
for (const item of block.body) {
|
|
6852
|
+
const description = item.text.match(/^DESCRIPTION\s+'(.*)'$/i);
|
|
6853
|
+
if (description) {
|
|
6854
|
+
capability.description = description[1] ?? "";
|
|
6855
|
+
continue;
|
|
6856
|
+
}
|
|
6857
|
+
const returnsHint = item.text.match(/^RETURNS\s+HINT\s+'(.*)'$/i);
|
|
6858
|
+
if (returnsHint) {
|
|
6859
|
+
capability.returnsHint = returnsHint[1] ?? "";
|
|
6860
|
+
continue;
|
|
6861
|
+
}
|
|
6752
6862
|
const context = item.text.match(/^USING\s+CONTEXT\s+([A-Za-z_][A-Za-z0-9_.]*)$/i);
|
|
6753
6863
|
if (context?.[1]) {
|
|
6754
6864
|
capability.context = context[1];
|
|
@@ -6780,13 +6890,9 @@ function parseCapabilityBlock(block) {
|
|
|
6780
6890
|
capability.conflictKey = conflict3[1];
|
|
6781
6891
|
continue;
|
|
6782
6892
|
}
|
|
6783
|
-
const arg = item.text.match(/^ARG\s+([A-Za-z_][A-Za-z0-9_]*)\s+(STRING|TEXT|NUMBER|BOOLEAN|BOOL)
|
|
6893
|
+
const arg = item.text.match(/^ARG\s+([A-Za-z_][A-Za-z0-9_]*)\s+(STRING|TEXT|NUMBER|BOOLEAN|BOOL)\b(.*)$/i);
|
|
6784
6894
|
if (arg?.[1] && arg[2]) {
|
|
6785
|
-
capability.args[arg[1]] =
|
|
6786
|
-
type: normalizeArgType(arg[2]),
|
|
6787
|
-
...item.text.match(/\sREQUIRED(?:\s|$)/i) ? { required: true } : {},
|
|
6788
|
-
...arg[3] ? { max_length: Number(arg[3]) } : {}
|
|
6789
|
-
};
|
|
6895
|
+
capability.args[arg[1]] = parseArgSpec(arg[1], arg[2], arg[3] ?? "", item.line);
|
|
6790
6896
|
continue;
|
|
6791
6897
|
}
|
|
6792
6898
|
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);
|
|
@@ -6836,6 +6942,30 @@ function parseCapabilityBlock(block) {
|
|
|
6836
6942
|
capability.proposal.allowedFields.push(patch[1]);
|
|
6837
6943
|
continue;
|
|
6838
6944
|
}
|
|
6945
|
+
const bound = item.text.match(/^BOUND\s+([A-Za-z_][A-Za-z0-9_]*)\s+(-?\d+(?:\.\d+)?)?\s*\.\.\s*(-?\d+(?:\.\d+)?)?$/i);
|
|
6946
|
+
if (bound?.[1]) {
|
|
6947
|
+
ensureProposal(capability, item);
|
|
6948
|
+
const minimum = bound[2] !== void 0 ? Number(bound[2]) : void 0;
|
|
6949
|
+
const maximum = bound[3] !== void 0 ? Number(bound[3]) : void 0;
|
|
6950
|
+
if (minimum === void 0 && maximum === void 0)
|
|
6951
|
+
throw dslError(item.line, 1, "BOUND_RANGE_REQUIRED", "BOUND requires minimum, maximum, or both, such as 1..2500");
|
|
6952
|
+
capability.proposal.numericBounds ??= {};
|
|
6953
|
+
capability.proposal.numericBounds[bound[1]] = {
|
|
6954
|
+
...minimum !== void 0 ? { minimum } : {},
|
|
6955
|
+
...maximum !== void 0 ? { maximum } : {}
|
|
6956
|
+
};
|
|
6957
|
+
continue;
|
|
6958
|
+
}
|
|
6959
|
+
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);
|
|
6960
|
+
if (transition?.[1] && transition[3]) {
|
|
6961
|
+
ensureProposal(capability, item);
|
|
6962
|
+
capability.proposal.transitionGuards ??= {};
|
|
6963
|
+
capability.proposal.transitionGuards[transition[1]] = {
|
|
6964
|
+
...transition[2] ? { from_column: transition[2] } : {},
|
|
6965
|
+
allowed: parseTransitionAllowed(transition[3], item.line)
|
|
6966
|
+
};
|
|
6967
|
+
continue;
|
|
6968
|
+
}
|
|
6839
6969
|
const approval = item.text.match(/^APPROVAL\s+ROLE\s+([A-Za-z_][A-Za-z0-9_.-]*)$/i);
|
|
6840
6970
|
if (approval?.[1]) {
|
|
6841
6971
|
ensureProposal(capability, item);
|
|
@@ -6868,6 +6998,40 @@ function parseCapabilityBlock(block) {
|
|
|
6868
6998
|
throw dslError(block.line, 1, "PROPOSAL_PATCH_REQUIRED", `${block.name} proposal requires at least one PATCH line`);
|
|
6869
6999
|
return capability;
|
|
6870
7000
|
}
|
|
7001
|
+
function specArgsFromDsl(args) {
|
|
7002
|
+
return Object.fromEntries(Object.entries(args).map(([name, arg]) => {
|
|
7003
|
+
const { line: _line, ...spec } = arg;
|
|
7004
|
+
return [name, spec];
|
|
7005
|
+
}));
|
|
7006
|
+
}
|
|
7007
|
+
function collectDslWarnings(ast) {
|
|
7008
|
+
const warnings = [];
|
|
7009
|
+
for (const capability of ast.capabilities) {
|
|
7010
|
+
if (capability.kind !== "proposal" || !capability.proposal)
|
|
7011
|
+
continue;
|
|
7012
|
+
const line = capability.line ?? 1;
|
|
7013
|
+
if (!capability.description) {
|
|
7014
|
+
warnings.push({ line, column: 1, code: "DESCRIPTION_RECOMMENDED", message: `${capability.name} is a proposal capability without DESCRIPTION.` });
|
|
7015
|
+
}
|
|
7016
|
+
if (!capability.returnsHint) {
|
|
7017
|
+
warnings.push({ line, column: 1, code: "RETURNS_HINT_RECOMMENDED", message: `${capability.name} is a proposal capability without RETURNS HINT.` });
|
|
7018
|
+
}
|
|
7019
|
+
for (const [column, binding] of Object.entries(capability.proposal.patch)) {
|
|
7020
|
+
if (!binding.from_arg)
|
|
7021
|
+
continue;
|
|
7022
|
+
const arg = capability.args[binding.from_arg];
|
|
7023
|
+
if (arg?.type === "number" && arg.minimum === void 0 && arg.maximum === void 0 && capability.proposal.numericBounds?.[column] === void 0) {
|
|
7024
|
+
warnings.push({
|
|
7025
|
+
line: arg.line ?? line,
|
|
7026
|
+
column: 1,
|
|
7027
|
+
code: "NUMERIC_PATCH_BOUND_RECOMMENDED",
|
|
7028
|
+
message: `${capability.name} patches ${column} from numeric arg ${binding.from_arg} without ARG MIN/MAX or BOUND ${column}.`
|
|
7029
|
+
});
|
|
7030
|
+
}
|
|
7031
|
+
}
|
|
7032
|
+
}
|
|
7033
|
+
return warnings;
|
|
7034
|
+
}
|
|
6871
7035
|
function parseWorkflowBlock(block) {
|
|
6872
7036
|
const workflow = { name: block.name, context: "", allowedCapabilities: [] };
|
|
6873
7037
|
for (const item of block.body) {
|
|
@@ -6925,6 +7089,76 @@ function parsePatchBinding(raw) {
|
|
|
6925
7089
|
return { fixed: quoted[1] ?? "" };
|
|
6926
7090
|
return { fixed: trimmed };
|
|
6927
7091
|
}
|
|
7092
|
+
function parseArgSpec(name, rawType, rawOptions, line) {
|
|
7093
|
+
const type = normalizeArgType(rawType);
|
|
7094
|
+
let rest = rawOptions.trim();
|
|
7095
|
+
let description;
|
|
7096
|
+
const descriptionMatch = rest.match(/\bDESCRIPTION\s+'([^']*)'/i);
|
|
7097
|
+
if (descriptionMatch) {
|
|
7098
|
+
description = descriptionMatch[1] ?? "";
|
|
7099
|
+
rest = `${rest.slice(0, descriptionMatch.index)} ${rest.slice((descriptionMatch.index ?? 0) + descriptionMatch[0].length)}`.trim();
|
|
7100
|
+
}
|
|
7101
|
+
const required = /\bREQUIRED\b/i.test(rest);
|
|
7102
|
+
rest = rest.replace(/\bREQUIRED\b/ig, " ").trim();
|
|
7103
|
+
const maxLengthMatch = rest.match(/\bMAX\s+LENGTH\s+(\d+)\b/i);
|
|
7104
|
+
const maxLength = maxLengthMatch?.[1] ? Number(maxLengthMatch[1]) : void 0;
|
|
7105
|
+
if (maxLengthMatch)
|
|
7106
|
+
rest = `${rest.slice(0, maxLengthMatch.index)} ${rest.slice((maxLengthMatch.index ?? 0) + maxLengthMatch[0].length)}`.trim();
|
|
7107
|
+
const minMatch = rest.match(/\bMIN\s+(-?\d+(?:\.\d+)?)\b/i);
|
|
7108
|
+
const minimum = minMatch?.[1] ? Number(minMatch[1]) : void 0;
|
|
7109
|
+
if (minMatch)
|
|
7110
|
+
rest = `${rest.slice(0, minMatch.index)} ${rest.slice((minMatch.index ?? 0) + minMatch[0].length)}`.trim();
|
|
7111
|
+
const maxMatch = rest.match(/\bMAX\s+(-?\d+(?:\.\d+)?)\b/i);
|
|
7112
|
+
const maximumOrLegacyLength = maxMatch?.[1] ? Number(maxMatch[1]) : void 0;
|
|
7113
|
+
if (maxMatch)
|
|
7114
|
+
rest = `${rest.slice(0, maxMatch.index)} ${rest.slice((maxMatch.index ?? 0) + maxMatch[0].length)}`.trim();
|
|
7115
|
+
if (rest.trim().length > 0) {
|
|
7116
|
+
throw dslError(line, 1, "ARG_OPTIONS_UNSUPPORTED", `unsupported ARG options for ${name}: ${rest.trim()}`);
|
|
7117
|
+
}
|
|
7118
|
+
if (type !== "number" && minimum !== void 0) {
|
|
7119
|
+
throw dslError(line, 1, "ARG_MIN_REQUIRES_NUMBER", `ARG ${name} uses MIN, but MIN is only valid for NUMBER args`);
|
|
7120
|
+
}
|
|
7121
|
+
if (type === "boolean" && (maxLength !== void 0 || maximumOrLegacyLength !== void 0)) {
|
|
7122
|
+
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`);
|
|
7123
|
+
}
|
|
7124
|
+
if (type === "number" && maxLength !== void 0) {
|
|
7125
|
+
throw dslError(line, 1, "ARG_MAX_LENGTH_REQUIRES_TEXT", `ARG ${name} uses MAX LENGTH, but MAX LENGTH is only valid for STRING/TEXT args`);
|
|
7126
|
+
}
|
|
7127
|
+
return {
|
|
7128
|
+
type,
|
|
7129
|
+
line,
|
|
7130
|
+
...required ? { required: true } : {},
|
|
7131
|
+
...description !== void 0 ? { description } : {},
|
|
7132
|
+
...type === "number" && minimum !== void 0 ? { minimum } : {},
|
|
7133
|
+
...type === "number" && maximumOrLegacyLength !== void 0 ? { maximum: maximumOrLegacyLength } : {},
|
|
7134
|
+
...type !== "number" && maxLength !== void 0 ? { max_length: maxLength } : {},
|
|
7135
|
+
...type !== "number" && maxLength === void 0 && maximumOrLegacyLength !== void 0 ? { max_length: maximumOrLegacyLength } : {}
|
|
7136
|
+
};
|
|
7137
|
+
}
|
|
7138
|
+
function parseTransitionAllowed(raw, line) {
|
|
7139
|
+
const allowed = {};
|
|
7140
|
+
for (const item of raw.split(",").map((part) => part.trim()).filter(Boolean)) {
|
|
7141
|
+
const match = item.match(/^(.+?)\s*->\s*(.+)$/);
|
|
7142
|
+
if (!match?.[1] || !match[2]) {
|
|
7143
|
+
throw dslError(line, 1, "TRANSITION_RULE_INVALID", `transition rule must use from -> to syntax: ${item}`);
|
|
7144
|
+
}
|
|
7145
|
+
const from = parseStateValue(match[1]);
|
|
7146
|
+
const toValues = match[2].split("|").map(parseStateValue).filter(Boolean);
|
|
7147
|
+
if (!from || toValues.length === 0) {
|
|
7148
|
+
throw dslError(line, 1, "TRANSITION_RULE_INVALID", `transition rule must name non-empty states: ${item}`);
|
|
7149
|
+
}
|
|
7150
|
+
allowed[from] = toValues;
|
|
7151
|
+
}
|
|
7152
|
+
if (Object.keys(allowed).length === 0) {
|
|
7153
|
+
throw dslError(line, 1, "TRANSITION_ALLOWED_REQUIRED", "TRANSITION requires at least one from -> to rule");
|
|
7154
|
+
}
|
|
7155
|
+
return allowed;
|
|
7156
|
+
}
|
|
7157
|
+
function parseStateValue(raw) {
|
|
7158
|
+
const trimmed = raw.trim();
|
|
7159
|
+
const quoted = trimmed.match(/^'(.*)'$/);
|
|
7160
|
+
return (quoted?.[1] ?? trimmed).trim();
|
|
7161
|
+
}
|
|
6928
7162
|
function normalizeBindingSource(source) {
|
|
6929
7163
|
const normalized = source.toUpperCase();
|
|
6930
7164
|
if (normalized === "ENV")
|
|
@@ -8145,6 +8379,11 @@ async function main(argv) {
|
|
|
8145
8379
|
usage([]);
|
|
8146
8380
|
return 0;
|
|
8147
8381
|
}
|
|
8382
|
+
if (command === "--version" || command === "-v" || command === "version") {
|
|
8383
|
+
process2.stdout.write(`${await runnerPackageVersion()}
|
|
8384
|
+
`);
|
|
8385
|
+
return 0;
|
|
8386
|
+
}
|
|
8148
8387
|
if (!isKnownTopLevelCommand(command)) {
|
|
8149
8388
|
process2.stderr.write(`Unknown command: ${cliCommandName()} ${command}
|
|
8150
8389
|
|
|
@@ -9331,12 +9570,15 @@ async function dslValidate(args) {
|
|
|
9331
9570
|
const target2 = firstPositional(args);
|
|
9332
9571
|
if (!target2) throw new Error("dsl validate requires <contract.synapsor>");
|
|
9333
9572
|
const source = await fs3.readFile(target2, "utf8");
|
|
9573
|
+
const strict = args.includes("--strict");
|
|
9334
9574
|
const result = validateAgentDsl(source);
|
|
9335
9575
|
if (args.includes("--json")) {
|
|
9336
9576
|
process2.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
9337
9577
|
`);
|
|
9338
9578
|
} else if (result.ok) {
|
|
9339
9579
|
process2.stdout.write(`dsl valid: ${target2}
|
|
9580
|
+
`);
|
|
9581
|
+
for (const warning of result.warnings) process2.stdout.write(`warning ${warning.line}:${warning.column} ${warning.code}: ${warning.message}
|
|
9340
9582
|
`);
|
|
9341
9583
|
} else {
|
|
9342
9584
|
process2.stdout.write(`dsl invalid: ${target2}
|
|
@@ -9344,13 +9586,22 @@ async function dslValidate(args) {
|
|
|
9344
9586
|
for (const error of result.errors) process2.stdout.write(`error ${error.line}:${error.column} ${error.code}: ${error.message}
|
|
9345
9587
|
`);
|
|
9346
9588
|
}
|
|
9347
|
-
return result.ok ? 0 : 1;
|
|
9589
|
+
return result.ok && (!strict || result.warnings.length === 0) ? 0 : 1;
|
|
9348
9590
|
}
|
|
9349
9591
|
async function dslCompile(args) {
|
|
9350
9592
|
const target2 = firstPositional(args);
|
|
9351
9593
|
if (!target2) throw new Error("dsl compile requires <contract.synapsor>");
|
|
9352
9594
|
const source = await fs3.readFile(target2, "utf8");
|
|
9353
|
-
const
|
|
9595
|
+
const strict = args.includes("--strict");
|
|
9596
|
+
const result = compileAgentDslWithWarnings(source);
|
|
9597
|
+
if (strict && result.warnings.length > 0) {
|
|
9598
|
+
process2.stdout.write(`dsl warnings treated as errors: ${target2}
|
|
9599
|
+
`);
|
|
9600
|
+
for (const warning of result.warnings) process2.stdout.write(`warning ${warning.line}:${warning.column} ${warning.code}: ${warning.message}
|
|
9601
|
+
`);
|
|
9602
|
+
return 1;
|
|
9603
|
+
}
|
|
9604
|
+
const contract = result.contract;
|
|
9354
9605
|
const output = outputArg(args);
|
|
9355
9606
|
const text = `${JSON.stringify(contract, null, 2)}
|
|
9356
9607
|
`;
|
|
@@ -9361,6 +9612,8 @@ async function dslCompile(args) {
|
|
|
9361
9612
|
} else {
|
|
9362
9613
|
process2.stdout.write(text);
|
|
9363
9614
|
}
|
|
9615
|
+
for (const warning of result.warnings) process2.stderr.write(`warning ${warning.line}:${warning.column} ${warning.code}: ${warning.message}
|
|
9616
|
+
`);
|
|
9364
9617
|
return 0;
|
|
9365
9618
|
}
|
|
9366
9619
|
async function contractValidate(args) {
|
|
@@ -11291,19 +11544,33 @@ async function cloudPush(args) {
|
|
|
11291
11544
|
if (!target2) throw new Error("cloud push requires <synapsor.contract.json>");
|
|
11292
11545
|
const parsed = JSON.parse(await fs3.readFile(target2, "utf8"));
|
|
11293
11546
|
const contract = normalizeContract(parsed);
|
|
11547
|
+
const workspace = (optionalArg(args, "--workspace") ?? optionalArg(args, "--project") ?? process2.env.SYNAPSOR_WORKSPACE_ID ?? process2.env.SYNAPSOR_PROJECT_ID ?? "").trim();
|
|
11548
|
+
const name = (optionalArg(args, "--name") ?? contract.metadata?.name ?? "").trim();
|
|
11549
|
+
const description = (optionalArg(args, "--description") ?? contract.metadata?.description ?? "").trim();
|
|
11550
|
+
const idempotencyKey = optionalArg(args, "--idempotency-key");
|
|
11294
11551
|
const payload = {
|
|
11295
11552
|
schema_version: "synapsor.cloud-contract-push.v0.1",
|
|
11296
11553
|
contract,
|
|
11297
11554
|
summary: contractSummary(contract),
|
|
11298
|
-
workspace
|
|
11299
|
-
name
|
|
11555
|
+
workspace,
|
|
11556
|
+
name,
|
|
11557
|
+
description,
|
|
11558
|
+
source: "runner",
|
|
11559
|
+
source_versions: {
|
|
11560
|
+
"@synapsor/runner": process2.env.npm_package_version ?? "unknown"
|
|
11561
|
+
},
|
|
11562
|
+
activate: args.includes("--activate"),
|
|
11563
|
+
idempotency_key: idempotencyKey,
|
|
11300
11564
|
pushed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
11301
11565
|
};
|
|
11302
11566
|
const dryRun = args.includes("--dry-run");
|
|
11303
|
-
|
|
11567
|
+
const json = args.includes("--json");
|
|
11568
|
+
if (dryRun && json) {
|
|
11304
11569
|
process2.stdout.write(`${JSON.stringify({ ok: dryRun, dry_run: dryRun, payload }, null, 2)}
|
|
11305
11570
|
`);
|
|
11306
|
-
|
|
11571
|
+
return 0;
|
|
11572
|
+
}
|
|
11573
|
+
if (dryRun) {
|
|
11307
11574
|
process2.stdout.write("Synapsor Cloud contract push preview\n");
|
|
11308
11575
|
process2.stdout.write(`Contract: ${target2}
|
|
11309
11576
|
`);
|
|
@@ -11317,17 +11584,48 @@ async function cloudPush(args) {
|
|
|
11317
11584
|
`);
|
|
11318
11585
|
process2.stdout.write(`Kept-out fields: ${payload.summary.kept_out_fields}
|
|
11319
11586
|
`);
|
|
11320
|
-
|
|
11321
|
-
if (dryRun) {
|
|
11322
|
-
if (!args.includes("--json")) process2.stdout.write("Dry run only. No Cloud upload attempted.\n");
|
|
11587
|
+
process2.stdout.write("Dry run only. No Cloud upload attempted.\n");
|
|
11323
11588
|
return 0;
|
|
11324
11589
|
}
|
|
11325
|
-
const apiUrl = optionalArg(args, "--api-url") ?? process2.env.SYNAPSOR_CLOUD_BASE_URL;
|
|
11326
|
-
const token = optionalArg(args, "--token") ?? process2.env.SYNAPSOR_CLOUD_TOKEN ?? process2.env.SYNAPSOR_RUNNER_TOKEN;
|
|
11590
|
+
const apiUrl = (optionalArg(args, "--api-url") ?? process2.env.SYNAPSOR_CLOUD_BASE_URL ?? "").trim();
|
|
11591
|
+
const token = (optionalArg(args, "--token") ?? process2.env.SYNAPSOR_CLOUD_TOKEN ?? process2.env.SYNAPSOR_RUNNER_TOKEN ?? "").trim();
|
|
11592
|
+
if (!workspace) {
|
|
11593
|
+
throw new Error("cloud push upload requires --workspace <project_id> or SYNAPSOR_WORKSPACE_ID/SYNAPSOR_PROJECT_ID.");
|
|
11594
|
+
}
|
|
11327
11595
|
if (!apiUrl || !token) {
|
|
11328
|
-
throw new Error("cloud push upload requires --
|
|
11596
|
+
throw new Error("cloud push upload requires --api-url plus --token/SYNAPSOR_CLOUD_TOKEN. Use --dry-run for local validation without a network call.");
|
|
11597
|
+
}
|
|
11598
|
+
const response = await postCloudContractPush({
|
|
11599
|
+
apiUrl,
|
|
11600
|
+
token,
|
|
11601
|
+
workspace,
|
|
11602
|
+
payload,
|
|
11603
|
+
idempotencyKey
|
|
11604
|
+
});
|
|
11605
|
+
if (json) {
|
|
11606
|
+
process2.stdout.write(`${JSON.stringify(response, null, 2)}
|
|
11607
|
+
`);
|
|
11608
|
+
return 0;
|
|
11329
11609
|
}
|
|
11330
|
-
|
|
11610
|
+
const contractId = cloudStringField(response, "contract_id") || cloudStringField(response.contract, "contract_id");
|
|
11611
|
+
const versionId = cloudStringField(response, "contract_version_id") || cloudStringField(response.version, "contract_version_id");
|
|
11612
|
+
const digest = cloudStringField(response, "digest") || cloudStringField(response.version, "digest");
|
|
11613
|
+
const status = cloudStringField(response, "status") || cloudStringField(response.version, "status") || "stored";
|
|
11614
|
+
process2.stdout.write("Synapsor Cloud contract push complete\n");
|
|
11615
|
+
process2.stdout.write(`Workspace: ${workspace}
|
|
11616
|
+
`);
|
|
11617
|
+
if (contractId) process2.stdout.write(`Contract id: ${contractId}
|
|
11618
|
+
`);
|
|
11619
|
+
if (versionId) process2.stdout.write(`Version id: ${versionId}
|
|
11620
|
+
`);
|
|
11621
|
+
if (digest) process2.stdout.write(`Digest: ${digest}
|
|
11622
|
+
`);
|
|
11623
|
+
process2.stdout.write(`Status: ${status}
|
|
11624
|
+
`);
|
|
11625
|
+
const registryUrl = cloudStringField(response, "registry_url");
|
|
11626
|
+
if (registryUrl) process2.stdout.write(`Registry: ${registryUrl}
|
|
11627
|
+
`);
|
|
11628
|
+
return 0;
|
|
11331
11629
|
}
|
|
11332
11630
|
function contractSummary(contract) {
|
|
11333
11631
|
return {
|
|
@@ -11338,6 +11636,81 @@ function contractSummary(contract) {
|
|
|
11338
11636
|
kept_out_fields: contract.capabilities.reduce((count, capability) => count + (capability.kept_out_fields?.length ?? 0), 0)
|
|
11339
11637
|
};
|
|
11340
11638
|
}
|
|
11639
|
+
async function postCloudContractPush(input) {
|
|
11640
|
+
let endpoint;
|
|
11641
|
+
try {
|
|
11642
|
+
endpoint = new URL(`/v1/control/projects/${encodeURIComponent(input.workspace)}/agent-contracts`, input.apiUrl.endsWith("/") ? input.apiUrl : `${input.apiUrl}/`);
|
|
11643
|
+
} catch {
|
|
11644
|
+
throw new Error("cloud push upload failed: --api-url/SYNAPSOR_CLOUD_BASE_URL is not a valid URL.");
|
|
11645
|
+
}
|
|
11646
|
+
const headers = {
|
|
11647
|
+
accept: "application/json",
|
|
11648
|
+
authorization: `Bearer ${input.token}`,
|
|
11649
|
+
"content-type": "application/json",
|
|
11650
|
+
"user-agent": "synapsor-runner-cloud-push"
|
|
11651
|
+
};
|
|
11652
|
+
if (input.idempotencyKey) headers["idempotency-key"] = input.idempotencyKey;
|
|
11653
|
+
const controller = new AbortController();
|
|
11654
|
+
const timeout = setTimeout(() => controller.abort(), 15e3);
|
|
11655
|
+
let response;
|
|
11656
|
+
try {
|
|
11657
|
+
response = await fetch(endpoint, {
|
|
11658
|
+
method: "POST",
|
|
11659
|
+
headers,
|
|
11660
|
+
body: JSON.stringify(input.payload),
|
|
11661
|
+
signal: controller.signal
|
|
11662
|
+
});
|
|
11663
|
+
} catch (error) {
|
|
11664
|
+
const reason = error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError") ? "request timed out" : "network request failed";
|
|
11665
|
+
throw new Error(`cloud push upload failed: ${reason}. Check --api-url/SYNAPSOR_CLOUD_BASE_URL and network connectivity.`);
|
|
11666
|
+
} finally {
|
|
11667
|
+
clearTimeout(timeout);
|
|
11668
|
+
}
|
|
11669
|
+
const text = await response.text();
|
|
11670
|
+
const body = parseCloudResponseJson(text);
|
|
11671
|
+
if (!response.ok || body.ok === false) {
|
|
11672
|
+
throw new Error(cloudPushErrorMessage(response.status, body));
|
|
11673
|
+
}
|
|
11674
|
+
return body;
|
|
11675
|
+
}
|
|
11676
|
+
function parseCloudResponseJson(text) {
|
|
11677
|
+
if (!text.trim()) return {};
|
|
11678
|
+
try {
|
|
11679
|
+
const parsed = JSON.parse(text);
|
|
11680
|
+
return isRecord7(parsed) ? parsed : { value: parsed };
|
|
11681
|
+
} catch {
|
|
11682
|
+
return { raw: text.slice(0, 500) };
|
|
11683
|
+
}
|
|
11684
|
+
}
|
|
11685
|
+
function cloudPushErrorMessage(status, body) {
|
|
11686
|
+
const code = typeof body.error === "string" && body.error ? body.error : `http_${status}`;
|
|
11687
|
+
const detail = cloudValidationDetail(body);
|
|
11688
|
+
if (status === 401) return `cloud push upload failed: token is missing, invalid, or expired (${code}).`;
|
|
11689
|
+
if (status === 403) return `cloud push upload failed: token does not have permission to write this workspace (${code}).`;
|
|
11690
|
+
if (status === 404) return `cloud push upload failed: Cloud API URL or workspace was not found (${code}).`;
|
|
11691
|
+
if (status === 409) return `cloud push upload failed: registry conflict (${code}).${detail}`;
|
|
11692
|
+
if (status === 422) return `cloud push upload failed: Cloud rejected the contract (${code}).${detail}`;
|
|
11693
|
+
if (status >= 500) return `cloud push upload failed: Cloud returned HTTP ${status} (${code}).`;
|
|
11694
|
+
return `cloud push upload failed: Cloud returned HTTP ${status} (${code}).${detail}`;
|
|
11695
|
+
}
|
|
11696
|
+
function cloudValidationDetail(body) {
|
|
11697
|
+
const errors = Array.isArray(body.errors) ? body.errors : [];
|
|
11698
|
+
if (!errors.length) {
|
|
11699
|
+
const message = typeof body.message === "string" ? body.message : "";
|
|
11700
|
+
return message ? ` ${message}` : "";
|
|
11701
|
+
}
|
|
11702
|
+
const rendered = errors.slice(0, 3).map((issue) => {
|
|
11703
|
+
if (!isRecord7(issue)) return String(issue);
|
|
11704
|
+
const path4 = typeof issue.path === "string" ? issue.path : "$";
|
|
11705
|
+
const code = typeof issue.code === "string" ? issue.code : "validation_error";
|
|
11706
|
+
const message = typeof issue.message === "string" ? issue.message : "";
|
|
11707
|
+
return `${path4} ${code}${message ? `: ${message}` : ""}`;
|
|
11708
|
+
}).join("; ");
|
|
11709
|
+
return ` ${rendered}`;
|
|
11710
|
+
}
|
|
11711
|
+
function cloudStringField(value, key) {
|
|
11712
|
+
return isRecord7(value) && typeof value[key] === "string" ? value[key] : "";
|
|
11713
|
+
}
|
|
11341
11714
|
async function cloudConnect(args) {
|
|
11342
11715
|
const configPath = optionalArg(args, "--config") ?? process2.env.SYNAPSOR_MCP_CONFIG ?? "synapsor.cloud.json";
|
|
11343
11716
|
const parsed = JSON.parse(await fs3.readFile(configPath, "utf8"));
|
|
@@ -14648,6 +15021,7 @@ function firstPositional(args) {
|
|
|
14648
15021
|
const flagsWithValues = /* @__PURE__ */ new Set([
|
|
14649
15022
|
"--allowed-columns",
|
|
14650
15023
|
"--approval-role",
|
|
15024
|
+
"--api-url",
|
|
14651
15025
|
"--actor",
|
|
14652
15026
|
"--action",
|
|
14653
15027
|
"--auth-token-env",
|
|
@@ -14658,6 +15032,7 @@ function firstPositional(args) {
|
|
|
14658
15032
|
"--conflict-column",
|
|
14659
15033
|
"--cors-origin",
|
|
14660
15034
|
"--database-url-env",
|
|
15035
|
+
"--description",
|
|
14661
15036
|
"--destination",
|
|
14662
15037
|
"--engine",
|
|
14663
15038
|
"--evidence",
|
|
@@ -14713,10 +15088,12 @@ function firstPositional(args) {
|
|
|
14713
15088
|
"--tenant-env",
|
|
14714
15089
|
"--tenant-key",
|
|
14715
15090
|
"--timeout-ms",
|
|
15091
|
+
"--token",
|
|
14716
15092
|
"--to",
|
|
14717
15093
|
"--transition-guard",
|
|
14718
15094
|
"--url",
|
|
14719
15095
|
"--visible-columns",
|
|
15096
|
+
"--workspace",
|
|
14720
15097
|
"--writeback-job",
|
|
14721
15098
|
"--write-url-env"
|
|
14722
15099
|
]);
|
|
@@ -15733,6 +16110,16 @@ function cliCommandName() {
|
|
|
15733
16110
|
if (process2.env.SYNAPSOR_RUNNER_COMMAND_NAME) return process2.env.SYNAPSOR_RUNNER_COMMAND_NAME;
|
|
15734
16111
|
return "synapsor-runner";
|
|
15735
16112
|
}
|
|
16113
|
+
async function runnerPackageVersion() {
|
|
16114
|
+
if (process2.env.npm_package_version) return process2.env.npm_package_version;
|
|
16115
|
+
const packageUrl = new URL("../package.json", import.meta.url);
|
|
16116
|
+
try {
|
|
16117
|
+
const parsed = JSON.parse(await fs3.readFile(packageUrl, "utf8"));
|
|
16118
|
+
if (typeof parsed.version === "string" && parsed.version.trim()) return parsed.version.trim();
|
|
16119
|
+
} catch {
|
|
16120
|
+
}
|
|
16121
|
+
return "unknown";
|
|
16122
|
+
}
|
|
15736
16123
|
function usage(args = []) {
|
|
15737
16124
|
const [command, subcommand] = args;
|
|
15738
16125
|
const key = (command === "mcp" || command === "handler") && subcommand ? `${command} ${subcommand}` : command ?? "";
|
|
@@ -26,6 +26,8 @@ CREATE AGENT CONTEXT local_operator
|
|
|
26
26
|
END
|
|
27
27
|
|
|
28
28
|
CREATE CAPABILITY billing.inspect_invoice
|
|
29
|
+
DESCRIPTION 'Inspect one invoice in the trusted tenant before proposing a waiver.'
|
|
30
|
+
RETURNS HINT 'Returns reviewed invoice fields plus evidence/query-audit handles.'
|
|
29
31
|
USING CONTEXT local_operator
|
|
30
32
|
SOURCE local_postgres
|
|
31
33
|
ON public.invoices
|
|
@@ -33,13 +35,15 @@ CREATE CAPABILITY billing.inspect_invoice
|
|
|
33
35
|
TENANT KEY tenant_id
|
|
34
36
|
CONFLICT GUARD updated_at
|
|
35
37
|
LOOKUP invoice_id BY id
|
|
36
|
-
ARG invoice_id STRING REQUIRED MAX 128
|
|
38
|
+
ARG invoice_id STRING REQUIRED MAX LENGTH 128 DESCRIPTION 'Invoice id such as INV-3001.'
|
|
37
39
|
ALLOW READ id, tenant_id, status, late_fee_cents, updated_at
|
|
38
40
|
REQUIRE EVIDENCE
|
|
39
41
|
MAX ROWS 1
|
|
40
42
|
END
|
|
41
43
|
|
|
42
44
|
CREATE CAPABILITY billing.propose_late_fee_waiver
|
|
45
|
+
DESCRIPTION 'Propose waiving one invoice late fee after inspecting invoice and policy evidence.'
|
|
46
|
+
RETURNS HINT 'Returns a review-required proposal id, exact diff, evidence handle, and source_database_changed:false.'
|
|
43
47
|
USING CONTEXT local_operator
|
|
44
48
|
SOURCE local_postgres
|
|
45
49
|
ON public.invoices
|
|
@@ -47,8 +51,8 @@ CREATE CAPABILITY billing.propose_late_fee_waiver
|
|
|
47
51
|
TENANT KEY tenant_id
|
|
48
52
|
CONFLICT GUARD updated_at
|
|
49
53
|
LOOKUP invoice_id BY id
|
|
50
|
-
ARG invoice_id STRING REQUIRED MAX 128
|
|
51
|
-
ARG reason TEXT REQUIRED MAX 500
|
|
54
|
+
ARG invoice_id STRING REQUIRED MAX LENGTH 128 DESCRIPTION 'Invoice id such as INV-3001.'
|
|
55
|
+
ARG reason TEXT REQUIRED MAX LENGTH 500 DESCRIPTION 'Business reason for the proposed waiver.'
|
|
52
56
|
ALLOW READ id, tenant_id, status, late_fee_cents, waiver_reason, updated_at
|
|
53
57
|
REQUIRE EVIDENCE
|
|
54
58
|
MAX ROWS 1
|
|
@@ -56,6 +60,7 @@ CREATE CAPABILITY billing.propose_late_fee_waiver
|
|
|
56
60
|
ALLOW WRITE late_fee_cents, waiver_reason
|
|
57
61
|
PATCH late_fee_cents = 0
|
|
58
62
|
PATCH waiver_reason = ARG reason
|
|
63
|
+
BOUND late_fee_cents 0..10000
|
|
59
64
|
APPROVAL ROLE billing_lead
|
|
60
65
|
WRITEBACK DIRECT SQL
|
|
61
66
|
END
|
|
@@ -64,10 +69,15 @@ END
|
|
|
64
69
|
Compile and validate:
|
|
65
70
|
|
|
66
71
|
```bash
|
|
67
|
-
synapsor-runner dsl compile ./contract.synapsor --out ./synapsor.contract.json
|
|
72
|
+
synapsor-runner dsl compile ./contract.synapsor --out ./synapsor.contract.json --strict
|
|
68
73
|
synapsor-runner contract validate ./synapsor.contract.json
|
|
69
74
|
synapsor-runner contract bundle ./synapsor.contract.json --out ./synapsor-runner-bundle
|
|
70
75
|
synapsor-runner cloud push ./synapsor.contract.json --dry-run
|
|
76
|
+
synapsor-runner cloud push ./synapsor.contract.json \
|
|
77
|
+
--api-url "$SYNAPSOR_CLOUD_BASE_URL" \
|
|
78
|
+
--token "$SYNAPSOR_CLOUD_TOKEN" \
|
|
79
|
+
--workspace "$SYNAPSOR_PROJECT_ID" \
|
|
80
|
+
--name billing-late-fee
|
|
71
81
|
```
|
|
72
82
|
|
|
73
83
|
Reference the generated contract from local runner wiring:
|
|
@@ -95,6 +105,31 @@ synapsor-runner tools preview --config ./synapsor.runner.json --store ./.synapso
|
|
|
95
105
|
synapsor-runner mcp serve --config ./synapsor.runner.json --store ./.synapsor/local.db
|
|
96
106
|
```
|
|
97
107
|
|
|
108
|
+
`--strict` treats DSL safety warnings as errors. Use it in CI so proposal
|
|
109
|
+
capabilities cannot accidentally lose descriptions, returns hints, or numeric
|
|
110
|
+
patch bounds during review.
|
|
111
|
+
|
|
112
|
+
## DSL / JSON Capability Parity
|
|
113
|
+
|
|
114
|
+
The DSL compiles to canonical `@synapsor/spec` JSON. It must not silently weaken
|
|
115
|
+
reviewed runner JSON capabilities. Current parity:
|
|
116
|
+
|
|
117
|
+
| JSON field | DSL clause | Since | Notes |
|
|
118
|
+
| --- | --- | --- | --- |
|
|
119
|
+
| capability `description` | `DESCRIPTION '...'` | 0.1.8 | Recommended for every model-facing tool; strict mode warns when proposal capabilities omit it. |
|
|
120
|
+
| capability `returns_hint` | `RETURNS HINT '...'` | 0.1.8 | Recommended for every model-facing tool; strict mode warns when proposal capabilities omit it. |
|
|
121
|
+
| arg `description` | `ARG name TYPE ... DESCRIPTION '...'` | 0.1.8 | Used in MCP tool input schemas. |
|
|
122
|
+
| arg `minimum` | `ARG amount NUMBER MIN 1` | 0.1.8 | NUMBER args only. |
|
|
123
|
+
| arg `maximum` | `ARG amount NUMBER MAX 2500` | 0.1.8 | NUMBER args only. |
|
|
124
|
+
| arg `max_length` | `ARG reason TEXT MAX LENGTH 500` | 0.1.8 | STRING/TEXT args only. Legacy `MAX 500` is still accepted for existing DSL files, but `MAX LENGTH` is the reviewed spelling. |
|
|
125
|
+
| arg `enum` | Not expressible in DSL yet | 0.1 | Use embedded JSON or generated contract JSON when enum allowlists are required. |
|
|
126
|
+
| proposal `numeric_bounds` | `BOUND column 1..2500`, `BOUND column ..2500`, or `BOUND column 1..` | 0.1.8 | Applies to patched numeric columns. Strict mode warns when a NUMBER arg is patched without arg min/max or a matching `BOUND`. |
|
|
127
|
+
| proposal `transition_guards` | `TRANSITION status ALLOW pending -> approved\|rejected` or `TRANSITION status FROM current_status ALLOW open -> closed` | 0.1.8 | Values are state strings; use `|` for multiple target states. |
|
|
128
|
+
| proposal `conflict_guard` | `CONFLICT GUARD updated_at` | 0.1 | If omitted, DSL emits an explicit weak-guard acknowledgement. Prefer a real row-version column. |
|
|
129
|
+
| proposal `approval` | `APPROVAL ROLE billing_lead` | 0.1 | Local mode records the required role; enforcement is still outside the model-facing MCP tool. |
|
|
130
|
+
| proposal `writeback` | `WRITEBACK DIRECT SQL`, `WRITEBACK APP HANDLER EXECUTOR name`, `WRITEBACK CLOUD WORKER`, `WRITEBACK NONE` | 0.1.7 | Handler URLs/tokens stay in `synapsor.runner.json`; contracts carry only the handler name. |
|
|
131
|
+
| evidence options | `REQUIRE EVIDENCE` | 0.1 | Detailed evidence sources/handle prefixes are not expressible in DSL yet; use embedded JSON or generated contract JSON for those. |
|
|
132
|
+
|
|
98
133
|
## Direct Runner Config Path
|
|
99
134
|
|
|
100
135
|
Directly embedding capabilities in `synapsor.runner.json` remains supported for
|
package/docs/conformance.md
CHANGED
|
@@ -26,6 +26,17 @@ The fixture set is intentionally small in 0.1. It covers the runner-supported
|
|
|
26
26
|
semantic surface first: trusted context, scoped reads, evidence handles,
|
|
27
27
|
proposal boundaries, kept-out fields, manual approval, and replay envelopes.
|
|
28
28
|
|
|
29
|
+
Additional 0.1 parity coverage currently lives in tests and verification
|
|
30
|
+
scripts rather than separate `cloud-push/` or `dsl-json-parity/` conformance
|
|
31
|
+
fixture directories:
|
|
32
|
+
|
|
33
|
+
- `docs/dsl-json-parity.md`, DSL/spec tests, and the
|
|
34
|
+
`numeric-bounds-transition` C++ export fixture cover richer DSL/JSON parity
|
|
35
|
+
fields such as `returns_hint`, numeric bounds, and transition guards.
|
|
36
|
+
- The main Synapsor repo script `scripts/verify_contract_cloud_push.sh`
|
|
37
|
+
verifies real Cloud push, retrieval, idempotent versioning, unauthorized
|
|
38
|
+
rejection, and runner-bundle download against a live local control-plane.
|
|
39
|
+
|
|
29
40
|
## Runner Usage
|
|
30
41
|
|
|
31
42
|
Runner tests load every conformance `contract.json` and verify that the MCP
|
package/docs/local-mode.md
CHANGED
|
@@ -351,6 +351,28 @@ It launches the official MCP stdio client transport against `synapsor-runner mcp
|
|
|
351
351
|
The business state changed after the agent saw it, so Synapsor refused to commit.
|
|
352
352
|
```
|
|
353
353
|
|
|
354
|
+
The same live writeback smoke has a clearer release-check alias:
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
corepack pnpm test:live-apply
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Prerequisites:
|
|
361
|
+
|
|
362
|
+
- Docker daemon running;
|
|
363
|
+
- local ports `55433`, `55434`, and `53307` available;
|
|
364
|
+
- no Synapsor Cloud account, API key, hosted workspace, or production database.
|
|
365
|
+
|
|
366
|
+
Expected output:
|
|
367
|
+
|
|
368
|
+
- disposable Postgres billing, Postgres support, and MySQL orders containers start;
|
|
369
|
+
- MCP `tools/list` exposes reviewed semantic tools, not raw SQL;
|
|
370
|
+
- proposal calls create exact before/after diffs while source rows remain unchanged;
|
|
371
|
+
- local approval happens outside MCP;
|
|
372
|
+
- guarded writeback applies approved jobs and records receipts/replay;
|
|
373
|
+
- idempotent retry returns the existing receipt;
|
|
374
|
+
- a stale-row write is blocked as a conflict.
|
|
375
|
+
|
|
354
376
|
## Optional MCP client configs
|
|
355
377
|
|
|
356
378
|
After the Docker demo passes, developers who want to attach an MCP client can use the checked-in stdio config shapes in:
|
|
@@ -139,7 +139,7 @@ You can also compile from the SQL-like authoring layer:
|
|
|
139
139
|
|
|
140
140
|
```bash
|
|
141
141
|
synapsor-runner dsl validate ./contract.synapsor
|
|
142
|
-
synapsor-runner dsl compile ./contract.synapsor --out ./synapsor.contract.json
|
|
142
|
+
synapsor-runner dsl compile ./contract.synapsor --out ./synapsor.contract.json --strict
|
|
143
143
|
```
|
|
144
144
|
|
|
145
145
|
## Bundle For Local Runner
|
package/docs/release-notes.md
CHANGED
|
@@ -10,6 +10,45 @@ npx -y -p @synapsor/runner synapsor-runner demo --quick
|
|
|
10
10
|
The OSS runner command is `synapsor-runner`. The `synapsor` command is reserved
|
|
11
11
|
for the Synapsor Cloud CLI.
|
|
12
12
|
|
|
13
|
+
## Unreleased
|
|
14
|
+
|
|
15
|
+
## 0.1.9
|
|
16
|
+
|
|
17
|
+
### CLI Hygiene
|
|
18
|
+
|
|
19
|
+
- Adds top-level `synapsor-runner --version`, `synapsor-runner -v`, and
|
|
20
|
+
`synapsor-runner version` output.
|
|
21
|
+
|
|
22
|
+
## 0.1.8
|
|
23
|
+
|
|
24
|
+
### DSL / JSON Contract Parity
|
|
25
|
+
|
|
26
|
+
- Adds portable spec fields for capability `returns_hint`, proposal
|
|
27
|
+
`numeric_bounds`, and proposal `transition_guards`.
|
|
28
|
+
- Extends the DSL with `DESCRIPTION`, `RETURNS HINT`, arg descriptions,
|
|
29
|
+
numeric arg min/max, text `MAX LENGTH`, patch `BOUND`, and `TRANSITION`
|
|
30
|
+
clauses.
|
|
31
|
+
- Adds DSL warnings and `--strict` mode so proposal capabilities cannot
|
|
32
|
+
silently lose reviewed safety metadata.
|
|
33
|
+
- Preserves compiled bounds through `contracts: []` into Runner propose-time
|
|
34
|
+
enforcement and accepts pure-contract configs with `capabilities: []`.
|
|
35
|
+
- Adds `docs/dsl-json-parity.md` as the field-by-field support matrix across
|
|
36
|
+
JSON spec, DSL, Runner, C++/Cloud, and Cloud push.
|
|
37
|
+
|
|
38
|
+
### Cloud Registry Push
|
|
39
|
+
|
|
40
|
+
- Wires non-dry-run `synapsor-runner cloud push` to the Cloud control API.
|
|
41
|
+
- Keeps dry-run network-free and prints server-confirmed contract, version,
|
|
42
|
+
digest, and registry details for real uploads.
|
|
43
|
+
- Adds clearer 401/403/404/409/422/network error messages without printing
|
|
44
|
+
bearer tokens.
|
|
45
|
+
|
|
46
|
+
### Release Verification
|
|
47
|
+
|
|
48
|
+
- Adds `corepack pnpm test:live-apply` as the documented Docker-backed live
|
|
49
|
+
apply smoke for disposable Postgres/MySQL MCP examples, guarded writeback,
|
|
50
|
+
idempotent retry, stale-row conflict, receipts, and replay.
|
|
51
|
+
|
|
13
52
|
## 0.1.5
|
|
14
53
|
|
|
15
54
|
### Contract Authoring Front Door
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synapsor/runner",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Stop giving AI agents execute_sql; expose reviewed Postgres/MySQL MCP actions with proposals, approval, writeback, and replay.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|