@githolon/dsl 0.14.0 → 0.16.0

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.
Files changed (42) hide show
  1. package/dart/example/nomos_webview_transport.dart +2 -2
  2. package/dart/lib/nomos_dsl.dart +4 -4
  3. package/dart/lib/src/{dispatch.dart → offer.dart} +7 -7
  4. package/dart/lib/src/provider.dart +30 -30
  5. package/dart/lib/src/subscriptions.dart +3 -3
  6. package/dart/lib/src/webview_bridge.dart +56 -21
  7. package/dart/test/webview_bridge_test.dart +35 -34
  8. package/package.json +5 -3
  9. package/src/aggregate.ts +9 -1
  10. package/src/authoring.ts +10 -1
  11. package/src/build_package.ts +1 -1
  12. package/src/capability_exports.ts +2 -2
  13. package/src/codegen_dart.ts +34 -24
  14. package/src/codegen_dart_client.ts +74 -12
  15. package/src/codegen_provider_dart.ts +67 -61
  16. package/src/codegen_ts.ts +45 -17
  17. package/src/compile_package_main.ts +103 -36
  18. package/src/ctx.ts +13 -0
  19. package/src/engine_entry.ts +22 -5
  20. package/src/fields.ts +44 -0
  21. package/src/flow_gate.ts +208 -0
  22. package/src/framework/capability.ts +29 -11
  23. package/src/framework/encryption_scope.ts +9 -1
  24. package/src/framework/genesis.ts +5 -1
  25. package/src/framework/impure_capability.ts +73 -67
  26. package/src/framework/relations.ts +23 -4
  27. package/src/framework/signing_identity.ts +1 -1
  28. package/src/framework/sync_lifecycle.ts +3 -3
  29. package/src/framework/workspace_invariant.ts +29 -8
  30. package/src/framework/workspaces.ts +7 -0
  31. package/src/index.ts +25 -1
  32. package/src/manifest.ts +11 -17
  33. package/src/ops.ts +42 -1
  34. package/src/principal_evidence.ts +112 -0
  35. package/src/relation_sugar.ts +10 -4
  36. package/src/shape_compat.ts +208 -0
  37. package/src/usd.ts +287 -0
  38. package/src/usd_layers.ts +213 -26
  39. package/src/wire.ts +8 -0
  40. package/src/wire_encode.ts +75 -2
  41. package/src/workspace_sharding.ts +197 -24
  42. package/src/workspace_type.ts +1 -0
@@ -15,13 +15,13 @@
15
15
  * emit (e.g. `{name:"HQ", createdAt:1000}`);
16
16
  * - typed READ accessors per aggregate + per declared query — reactive
17
17
  * `watch…`/one-shot `read…` over the GitHolon `query()`/`query_by_id()`
18
- * surface (the read half of the dispatch(intent) boundary);
18
+ * surface (the read half of the offer/query boundary);
19
19
  * - a Dart enum per `t.enum([...])` field;
20
20
  * - a Dart intent class per domain action — a pure typed DTO. Each class exposes its
21
21
  * `domain` / `intentType` (so the call site never hand-types a string) and a
22
22
  * `toPayloadJson()` that serialises only its typed fields to data. It builds no
23
23
  * `Event` and no `Hlc`: the GitHolon executes the installed domain plan.
24
- * - a single generic write verb [`dispatch`] that ships {domain, intentType,
24
+ * - a single generic effectful write verb [`offer`] that ships {domain, intentType,
25
25
  * toPayloadJson()} to the GitHolon `author(domain, intentType, payloadJson, actor)`.
26
26
  *
27
27
  * It does NOT hand-write the fixed wire types (`Value`/`Op`/`Event`/...): those
@@ -671,13 +671,24 @@ function aggregateFieldSpecs(
671
671
  f.kind === "string" && ownIdFields.has(name)
672
672
  ? aggregateIdDartType(agg.id)
673
673
  : undefined;
674
- const spec = aggFieldSpec(
674
+ const base = aggFieldSpec(
675
675
  name,
676
676
  f,
677
677
  enumDartTypes,
678
678
  refIdClassForAggregate,
679
679
  valueClass,
680
680
  );
681
+ // PARTIAL-FOLD DOCTRINE (every read-model field is nullable). The projection
682
+ // serves PARTIAL folds: a freshly-created or not-yet-fully-folded aggregate may
683
+ // carry no value for ANY scalar (only the fields touched so far appear in the
684
+ // flat `data`). So a read-model SCALAR is ALWAYS nullable — never a strict
685
+ // `as bool`/`readInt` that throws on an absent field (the `watch`/`read`
686
+ // decode would crash mid-fold). `.optional()` on the DSL field no longer
687
+ // changes the read surface; it was nullable in practice — this makes the type
688
+ // honest. Collections are unchanged (they already empty-default when absent).
689
+ const spec: AggFieldSpec = isCollectionKind(base.kind)
690
+ ? base
691
+ : { ...base, optional: true };
681
692
  if (used.has(spec.name)) {
682
693
  throw new Error(
683
694
  `aggregate '${agg.id}' field '${spec.wireName}' maps to duplicate Dart field '${spec.name}'`,
@@ -828,7 +839,7 @@ function emitPermissionVocabulary(vocab: PermissionVocabulary | undefined): stri
828
839
  return [
829
840
  `/// Generated permission vocabulary from the domain role catalogue.`,
830
841
  `///`,
831
- `/// This is frontend read/dispatch vocabulary only. Domain execution and`,
842
+ `/// This is frontend read/offer vocabulary only. Domain execution and`,
832
843
  `/// admission still happen inside the installed GitHolon domain law.`,
833
844
  `class PermissionRole {`,
834
845
  ` const PermissionRole(this.value, this.capabilities);`,
@@ -1049,9 +1060,9 @@ function emitAggregate(
1049
1060
  `/// projection \`data\` map (the \`{type,id,data}\` row \`query()\`/\`query_by_id()\``,
1050
1061
  `/// emits) — e.g. \`{"name":"HQ","createdAt":1000}\`. The kernel aggregate`,
1051
1062
  `/// [aggregateId] is carried from the row \`id\`. Read it reactively through the`,
1052
- `/// generated \`watch…\` accessors below. Collection fields (\`set\`/\`map\`) default to`,
1053
- `/// EMPTY and \`.optional()\` scalars to \`null\` when a freshly-folded aggregate has`,
1054
- `/// not yet folded them; must-be-folded scalars still throw when absent.`,
1063
+ `/// generated \`watch…\` accessors below. The projection serves PARTIAL folds, so`,
1064
+ `/// EVERY scalar field is nullable — \`null\` when a freshly-folded aggregate has not`,
1065
+ `/// yet folded it (never a strict cast that throws); collections default to EMPTY.`,
1055
1066
  `class ${className} {`,
1056
1067
  fieldDecls,
1057
1068
  ``,
@@ -1062,9 +1073,8 @@ function emitAggregate(
1062
1073
  ` /// Parse from the FLAT projection \`data\` map (the \`{type,id,data}\` row's`,
1063
1074
  ` /// \`data\` that \`query()\`/\`query_by_id()\` emit — fields already`,
1064
1075
  ` /// decoded to bare JSON), carrying the kernel aggregate [id] from the row \`id\``,
1065
- ` /// (defaults to '' for a bare-map call). Missing REQUIRED fields throw a`,
1066
- ` /// [FormatException]; absent collections decode empty and absent \`.optional()\``,
1067
- ` /// scalars decode \`null\`.`,
1076
+ ` /// (defaults to '' for a bare-map call). PARTIAL folds: every scalar decodes`,
1077
+ ` /// \`null\` when absent (never throws); absent collections decode empty.`,
1068
1078
  ` factory ${className}.fromJson(Map<String, dynamic> data, [String id = '']) {`,
1069
1079
  ` return ${className}(`,
1070
1080
  parseLines,
@@ -1125,7 +1135,7 @@ function emitEvidenceRefClass(): string {
1125
1135
  ` );`,
1126
1136
  ` }`,
1127
1137
  ``,
1128
- ` /// The authored payload shape (for dispatch): the typed value-object the DSL`,
1138
+ ` /// The authored payload shape (for offer): the typed value-object the DSL`,
1129
1139
  ` /// \`t.evidence()\` field lowers to a kernel \`Value::Evidence\`.`,
1130
1140
  ` Map<String, Object?> toJson() => <String, Object?>{`,
1131
1141
  ` 'hash': hash,`,
@@ -1471,8 +1481,9 @@ function emitReadModel(
1471
1481
  `/// DSL-named; a \`json\` value-object leaf (e.g. \`location\`)`,
1472
1482
  `/// is a STRUCTURED \`Map<String, Object?>\` — the decoded shape the engine emits, NOT a`,
1473
1483
  `/// JSON string. This class is fully DSL-derived.`,
1474
- `/// Collection fields (\`set\`/\`map\`) default to EMPTY and \`.optional()\` scalars to`,
1475
- `/// \`null\` when a freshly-folded aggregate has not yet folded them.`,
1484
+ `/// The projection serves PARTIAL folds, so EVERY scalar field is nullable —`,
1485
+ `/// \`null\` when a freshly-folded aggregate has not yet folded it (never a strict`,
1486
+ `/// cast that throws); collection fields (\`set\`/\`map\`) default to EMPTY.`,
1476
1487
  `class ${className} {`,
1477
1488
  fieldDecls,
1478
1489
  ``,
@@ -1483,9 +1494,8 @@ function emitReadModel(
1483
1494
  ` /// Parse from the Rust read engine's already-decoded projected \`data\` map (the`,
1484
1495
  ` /// \`{type,id,data}\` row's \`data\` that \`query()\`/\`query_by_id()\` emit — every field`,
1485
1496
  ` /// already decoded to bare/structured JSON), carrying the kernel aggregate [id] from`,
1486
- ` /// the row \`id\` (defaults to '' for a bare-map call). Missing REQUIRED fields throw a`,
1487
- ` /// [FormatException]; absent collections decode empty and absent \`.optional()\` scalars`,
1488
- ` /// decode \`null\`.`,
1497
+ ` /// the row \`id\` (defaults to '' for a bare-map call). PARTIAL folds: every scalar`,
1498
+ ` /// decodes \`null\` when absent (never throws); absent collections decode empty.`,
1489
1499
  ` factory ${className}.fromProjected(Map<String, dynamic> data, [String id = '']) {`,
1490
1500
  ` return ${className}(`,
1491
1501
  parseLines,
@@ -1536,7 +1546,7 @@ function emitDirective(
1536
1546
  `/// Generated typed intent DTO for '${d.id}' (marker: ${d.marker},`,
1537
1547
  `/// aggregate: ${d.aggregateId}). It carries its [domain] + [intentType] and a`,
1538
1548
  `/// [toPayloadJson] that ships only typed fields as data. It builds no`,
1539
- `/// \`Event\` and no \`Hlc\`. Pass it to [dispatch]; the GitHolon executes`,
1549
+ `/// \`Event\` and no \`Hlc\`. Pass it to [offer]; the GitHolon executes`,
1540
1550
  `/// the installed domain plan.`,
1541
1551
  `class ${className} implements NomosIntent {`,
1542
1552
  ` /// The policy domain this intent belongs to (the kernel \`author\` \`domain\`).`,
@@ -1587,7 +1597,7 @@ export function toJsonExpr(spec: PayloadFieldSpec, accessor: string): string {
1587
1597
 
1588
1598
  // ---- read accessors (typed subscriptions) ----------------------------------
1589
1599
  //
1590
- // The READ half of the peer dispatch surface (Jack 2026-06-01): the app reads ONLY via
1600
+ // The READ half of the peer query surface (Jack 2026-06-18): the app reads ONLY via
1591
1601
  // these generated typed accessors over the GitHolon read surface
1592
1602
  // (`query(query_id, params_json)` + `query_by_id(aggregate_id)`, #140's indexed/PK
1593
1603
  // read-closure). It invents NO new Rust read primitive. Two emitters:
@@ -2503,11 +2513,11 @@ export interface DomainModule {
2503
2513
  */
2504
2514
  readModelAggregates?: string[];
2505
2515
  /**
2506
- * The domain's `impureCapability` capabilities (framework Order→Receipt; `impure_capability.ts`)
2516
+ * The domain's `impureCapability` capabilities (framework Obligation→Receipt; `impure_capability.ts`)
2507
2517
  * for which to ALSO emit the typed PROVIDER SDK — the symmetric twin of the peer
2508
- * payload classes. For each declared task the codegen emits a typed `XOrder` (+
2518
+ * payload classes. For each declared task the codegen emits a typed `XObligation` (+
2509
2519
  * `fromJson`), `XResult`/`XFailure`, a `XHandler` typedef and a `XProvider` dispatcher
2510
- * that decodes the incoming Order JSON and encodes the Receipt (reusing the
2520
+ * that decodes the incoming Obligation JSON and encodes the Receipt (reusing the
2511
2521
  * peer-codegen `CompleteXPayload`/`FailXPayload`, so the Receipt is an ordinary
2512
2522
  * intent through the ONE write path). ADDITIVE + OPTIONAL: a domain that omits this is
2513
2523
  * byte-identical to before. NOT carried into any manifest/hash — a pure provider-side
@@ -2571,10 +2581,10 @@ export function generateDartDomain(mod: DomainModule): string {
2571
2581
  const mintedBlock = emitMintedCreates(mod.aggregates, mod.directives, domainKey, dartTypes);
2572
2582
 
2573
2583
  // PROVIDER SDK (the symmetric twin of the peer payload classes): for each declared
2574
- // `impureCapability` capability, a typed Order/Result/Failure + handler typedef + dispatcher.
2584
+ // `impureCapability` capability, a typed Obligation/Result/Failure + handler typedef + dispatcher.
2575
2585
  // ADDITIVE + omit-when-empty: a domain with no `impureCapabilities` emits nothing (and the
2576
2586
  // `provider.dart` import is omitted), so it is byte-identical to before. The enum map is
2577
- // SHARED with the directive/aggregate codegen so an enum-typed order param references the
2587
+ // SHARED with the directive/aggregate codegen so an enum-typed obligation param references the
2578
2588
  // already-emitted domain enum (no duplicate decl).
2579
2589
  const providerBlock = emitProviderSdk(mod.impureCapabilities ?? [], dartTypes);
2580
2590
 
@@ -2766,7 +2776,7 @@ export function generateDartDomain(mod: DomainModule): string {
2766
2776
  ``,
2767
2777
  ]
2768
2778
  : []),
2769
- `// ---- public intents (typed DTOs; ship via the generic dispatch() verb) ----`,
2779
+ `// ---- public intents (typed DTOs; ship via the generic offer() verb) ----`,
2770
2780
  dirBlock,
2771
2781
  ``,
2772
2782
  ...(mintedBlock !== ""
@@ -13,7 +13,7 @@
13
13
  * hashed (the deployed law's content hash baked in), carrying a `<Domain>Client` over a connected
14
14
  * [NomosBridge] (the WebView runner) with:
15
15
  *
16
- * * a typed DISPATCH method per directive — takes the generated `…Payload`, serialises via its
16
+ * * a typed OFFER method per directive — takes the generated `…Payload`, serialises via its
17
17
  * `toPayloadJson()`, auto-stamps omitted `…At` timestamps (the same caller-clock sugar the TS
18
18
  * client applies), returns the new local head;
19
19
  * * a typed declared-QUERY accessor per `query(...)` — one-shot `Future<List<…ReadModel>>` + a
@@ -21,9 +21,10 @@
21
21
  * * by-id read/watch + a type-index `list…` per aggregate;
22
22
  * * typed O(1) count/sum accessors.
23
23
  *
24
- * It IMPORTS the sibling `<name>.dart` (for the read-model + payload classes) and the vendored
25
- * `nomos_dsl.dart` barrel (for `NomosBridge`). NO manifest/hash impact — pure frontend codegen, exactly
26
- * like `codegen_dart.ts`. NO FALLBACK: an unsupported shape throws at compile time.
24
+ * It IMPORTS the sibling `<name>.dart` (for the read-model + payload classes) and either the vendored
25
+ * `nomos_dsl.dart` barrel or the package `nomos_dsl` barrel (for `NomosBridge`). NO manifest/hash impact —
26
+ * pure frontend codegen, exactly like `codegen_dart.ts`. NO FALLBACK: an unsupported shape throws at compile
27
+ * time.
27
28
  */
28
29
  import type { z } from "zod";
29
30
 
@@ -56,6 +57,15 @@ export const dartClientClassName = (domain: string): string => `${cap(camel(doma
56
57
  /** `guestbook` → `guestbookDomainHash` — the Dart top-level const (lowerCamel by lint). */
57
58
  export const dartHashConstName = (fileBase: string): string => `${lcFirst(camel(fileBase))}DomainHash`;
58
59
 
60
+ /** `todo-nomos` → `todoNomosInterfaceJson` — the baked structural-interface JSON const (#72). */
61
+ export const interfaceConstNameDart = (fileBase: string): string => `${lcFirst(camel(fileBase))}InterfaceJson`;
62
+
63
+ /** Emit a Dart RAW triple-quoted string literal of an object's JSON (the interface is JSON-only
64
+ * data — no `'`/`$`/backslash escaping concerns under `r'''…'''`; JSON never contains `'''`). */
65
+ function dartRawJson(value: unknown): string {
66
+ return `r'''${JSON.stringify(value)}'''`;
67
+ }
68
+
59
69
  const dartStr = (s: string) => `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
60
70
 
61
71
  /** The Dart param type for a query index key (int-kinded keys → `int`, else `String`). */
@@ -81,8 +91,14 @@ function rowToModel(model: string, rowVar: string): string {
81
91
  }
82
92
 
83
93
  export interface DartBridgeClientOptions {
84
- /** sha256 of the built `.package.usda` — the law this client dispatches under. */
94
+ /** sha256 of the built `.package.usda` — the law this client offers under. */
85
95
  readonly domainHash: string;
96
+ /** This domain's STRUCTURAL INTERFACE (#72) — directives (payload shapes) + aggregates
97
+ * (read shapes, sid-keyed). Baked in as JSON so the app can negotiate shape-compatibility
98
+ * against the workspace's installed offer, NOT by law hash. */
99
+ readonly interface?: unknown;
100
+ /** Where the bridge/runtime support types are imported from. */
101
+ readonly supportImport?: "vendored" | "package";
86
102
  }
87
103
 
88
104
  /** Generate the `<name>.client.dart` async bridge client for one domain module. */
@@ -90,12 +106,16 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
90
106
  const domain = mod.domain ?? mod.name;
91
107
  const className = dartClientClassName(domain);
92
108
  const hashConst = dartHashConstName(mod.name);
109
+ const supportImport =
110
+ opts.supportImport === "package"
111
+ ? "package:nomos_dsl/nomos_dsl.dart"
112
+ : "nomos_dsl.dart";
93
113
  const aggById = new Map<string, AggregateHandle>();
94
114
  for (const a of mod.aggregates) aggById.set(a.id, a);
95
115
 
96
116
  const methods: string[] = [];
97
117
 
98
- // ── directives → typed dispatch ──
118
+ // ── directives → typed offer ──
99
119
  const dirs = (mod.directives as AnyDirective[]).filter(
100
120
  (d) => d && typeof d.id === "string" && typeof (d as { payloadSchema?: unknown }).payloadSchema === "object",
101
121
  );
@@ -106,12 +126,12 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
106
126
  if (stamps.length > 0) {
107
127
  methods.push(
108
128
  ` /// Author \`${domain}/${d.id}\` locally under the installed law; returns the new local head.`,
109
- ` /// Omitted ${stamps.map((s) => `\`${s}\``).join(" / ")} auto-stamp(s) ISO now() at dispatch.`,
129
+ ` /// Omitted ${stamps.map((s) => `\`${s}\``).join(" / ")} auto-stamp(s) ISO now() at offer.`,
110
130
  ` Future<String> ${method}(${payloadClass} payload) {`,
111
131
  ` final data = payload.toPayloadJson();`,
112
132
  ` final now = DateTime.now().toUtc().toIso8601String();`,
113
133
  ...stamps.map((s) => ` data.putIfAbsent(${dartStr(s)}, () => now);`),
114
- ` return bridge.dispatch(domain: ${dartStr(domain)}, directiveId: ${dartStr(d.id)}, payload: data, domainHash: domainHash);`,
134
+ ` return bridge.offer(intentBytes: _encodeIntentOffer(${dartStr(domain)}, ${dartStr(d.id)}, data, domainHash));`,
115
135
  ` }`,
116
136
  ``,
117
137
  );
@@ -119,7 +139,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
119
139
  methods.push(
120
140
  ` /// Author \`${domain}/${d.id}\` locally under the installed law; returns the new local head.`,
121
141
  ` Future<String> ${method}(${payloadClass} payload) =>`,
122
- ` bridge.dispatch(domain: ${dartStr(domain)}, directiveId: ${dartStr(d.id)}, payload: payload.toPayloadJson(), domainHash: domainHash);`,
142
+ ` bridge.offer(intentBytes: _encodeIntentOffer(${dartStr(domain)}, ${dartStr(d.id)}, payload.toPayloadJson(), domainHash));`,
123
143
  ``,
124
144
  );
125
145
  }
@@ -196,22 +216,64 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
196
216
  `// final app = ${className}(bridge);`,
197
217
  `// await app.${dirs[0] ? safeMethod(dirs[0].id) : "someDirective"}(/* typed payload */);`,
198
218
  ``,
199
- `import 'nomos_dsl.dart';`,
219
+ `import 'dart:convert';`,
220
+ ``,
221
+ `import '${supportImport}';`,
200
222
  `import '${mod.name}.dart';`,
201
223
  `// Re-export the sibling types (read models + payloads + the bridge runtime) so ONE import of this`,
202
224
  `// client file gives an app the whole typed surface.`,
203
225
  `export '${mod.name}.dart';`,
204
- `export 'nomos_dsl.dart' show NomosBridge, BridgeTransport, NomosBridgeError, kWebviewBridgeVersion;`,
226
+ `export '${supportImport}' show NomosBridge, BridgeTransport, NomosBridgeError, kWebviewBridgeVersion;`,
205
227
  ``,
206
- `/// sha256 of the deployed \`.package.usda\` — the installed law this client dispatches under.`,
228
+ `/// sha256 of the deployed \`.package.usda\` — the installed law this client offers under.`,
207
229
  `const String ${hashConst} = ${dartStr(opts.domainHash)};`,
208
230
  ``,
231
+ ...(opts.interface !== undefined
232
+ ? [
233
+ `/// This client's STRUCTURAL INTERFACE REQUIREMENT (#72) as JSON — the directives (payload`,
234
+ `/// shapes) + aggregates (read shapes, sid-keyed) it depends on. \`connect()\` negotiates this`,
235
+ `/// against the workspace's installed offer by SHAPE (stable-id subtyping), not by law hash,`,
236
+ `/// so the domain can evolve without republishing this client.`,
237
+ `const String ${interfaceConstNameDart(mod.name)} = ${dartRawJson(opts.interface)};`,
238
+ ``,
239
+ ]
240
+ : []),
241
+ `String _encodeIntentOffer(String domain, String directiveId, Object? payload, String domainHash) =>`,
242
+ ` base64Encode(utf8.encode(jsonEncode({`,
243
+ ` 'v': 2,`,
244
+ ` 'intent': {`,
245
+ ` 'domain': domain,`,
246
+ ` 'directiveId': directiveId,`,
247
+ ` 'payload': payload,`,
248
+ ` 'domainHash': domainHash,`,
249
+ ` },`,
250
+ ` })));`,
251
+ ``,
209
252
  `/// Typed async client for the \`${domain}\` domain, bound to a connected [NomosBridge].`,
210
253
  `class ${className} {`,
211
254
  ` final NomosBridge bridge;`,
212
255
  ` final String domainHash;`,
213
256
  ` ${className}(this.bridge, {this.domainHash = ${hashConst}});`,
214
257
  ``,
258
+ ...(opts.interface !== undefined
259
+ ? [
260
+ ` /// This client's structural interface requirement (#72) as JSON — hand it to the`,
261
+ ` /// bridge's compatibility check to negotiate by SHAPE against the workspace's offer.`,
262
+ ` String get interfaceJson => ${interfaceConstNameDart(mod.name)};`,
263
+ ``,
264
+ ` Map<String, Object?> get _requirement =>`,
265
+ ` {${dartStr(domain)}: (jsonDecode(interfaceJson) as Map).cast<String, Object?>()};`,
266
+ ``,
267
+ ` /// SHAPE COMPATIBILITY (#72): one-shot verdict for THIS client against the workspace's`,
268
+ ` /// ON-LEDGER offer — \`{ ok, missing[], incompatible[] }\`, by shape not law hash.`,
269
+ ` Future<Map<String, Object?>> checkCompatibility() => bridge.checkCompatibility(_requirement);`,
270
+ ``,
271
+ ` /// SHAPE COMPATIBILITY as a LIVE BINDING (#72): a stream of verdicts that re-emits whenever`,
272
+ ` /// the workspace's installed law changes (a synced install/upgrade from another peer).`,
273
+ ` Stream<Map<String, Object?>> watchCompatibility() => bridge.watchCompatibility(_requirement);`,
274
+ ``,
275
+ ]
276
+ : []),
215
277
  ...methods,
216
278
  `}`,
217
279
  ``,
@@ -9,32 +9,32 @@
9
9
  * Provider SDK codegen — the SYMMETRIC TWIN of the typed peer write codegen
10
10
  * (`codegen_dart.ts`).
11
11
  *
12
- * Order and Receipt are ordinary intent roles. This codegen gives a capability provider typed
12
+ * Obligation and Receipt are ordinary intent roles. This codegen gives a capability provider typed
13
13
  * decode/encode helpers; it does not create a separate admission job protocol or a privileged
14
14
  * mutation path.
15
15
  *
16
16
  * ── Why this exists (Jack 2026-06-04) ────────────────────────────────────────
17
17
  * Code is ~free; INTEGRATION POINTS are expensive + fuckup-guaranteed. A capability
18
18
  * PROVIDER (Whisper, a renderer, a multi-target PR transporter) sits at
19
- * the far end of the framework `impureCapability` Order→Receipt pair. Today a dev would
20
- * hand-parse the incoming Order intent JSON and hand-build the Receipt — exactly the
19
+ * the far end of the framework `impureCapability` Obligation→Receipt pair. Today a dev would
20
+ * hand-parse the incoming Obligation intent JSON and hand-build the Receipt — exactly the
21
21
  * brittle integration the peer write codegen eliminated on the WRITE side. This generator
22
22
  * eliminates it on the PROVIDE side: a dev writes ONE typed method
23
- * `handler(Order) -> Result | Failure`, and the generated dispatcher DECODES the Order
23
+ * `handler(Obligation) -> Result | Failure`, and the generated dispatcher DECODES the Obligation
24
24
  * and ENCODES the ordinary Receipt intent — proven in ISOLATION (a Dart unit test), with NO deploy /
25
25
  * daemon / e2e.
26
26
  *
27
27
  * ── What it emits, per `impureCapability` capability (the STANDARD shape, never invented) ──
28
- * 1. a typed `XOrder` — the Order params (envelope provenance `requestId` /
28
+ * 1. a typed `XObligation` — the Obligation params (envelope provenance `requestId` /
29
29
  * `requestedBy` / `requestedAt` ⊕ the tenant params from the DSL), with a
30
- * `fromJson` decoding the incoming Order intent's payload JSON;
30
+ * `fromJson` decoding the incoming Obligation intent's payload JSON;
31
31
  * 2. a typed `XResult` — the success payload the handler returns (`resultRef`, the
32
32
  * content-addressed Evidence handle — the framework's `completeSchema.resultRef`),
33
33
  * implementing `TaskResult.toResultRef()`;
34
34
  * 3. typed failure/blocked/DLQ outcomes;
35
- * 4. a handler typedef `XHandler = TaskOutcome<XResult> Function(XOrder)`;
35
+ * 4. a handler typedef `XHandler = TaskOutcome<XResult> Function(XObligation)`;
36
36
  * 5. a generated `XProvider` dispatcher (`implements TaskProvider`): given the
37
- * registered typed handler, `handleOrder(orderJson, now)` DECODES → calls the
37
+ * registered typed handler, `handleObligation(obligationJson, now)` DECODES → calls the
38
38
  * handler → ENCODES the typed outcome into the ordinary RECEIPT intent — REUSING the peer write
39
39
  * codegen's `CompleteXPayload` / `FailXPayload` / `BlockXPayload` /
40
40
  * `DeadLetterXPayload`. The Receipt is an
@@ -61,7 +61,7 @@ import {
61
61
  /**
62
62
  * The minimal shape of a `impureCapability` declaration the provider codegen needs. A
63
63
  * `ImpureCapability` (framework `impure_capability.ts`) carries EXACTLY this — its task `aggregate`
64
- * handle (whose `.id` is the wire type) and its `.order` / receipt directives
64
+ * handle (whose `.id` is the wire type) and its `.obligation` / receipt directives
65
65
  * directives — so a `impureCapability(...)` return value satisfies `ImpureCapabilityDecl`
66
66
  * STRUCTURALLY. The emit script passes the values straight through: no re-derivation, no
67
67
  * fragile structural inference over the flattened directive list.
@@ -69,8 +69,8 @@ import {
69
69
  export interface ImpureCapabilityDecl {
70
70
  /** The task aggregate handle; `.id` is the wire type (e.g. `TranscribeVoiceNoteAggregate`). */
71
71
  readonly aggregate: { readonly id: string };
72
- /** The ORDER directive: `.creates`, payload = envelope ⊕ tenant params. */
73
- readonly order: { readonly id: string; readonly payloadSchema: z.ZodTypeAny };
72
+ /** The Obligation directive: `.creates`, payload = envelope ⊕ tenant params. */
73
+ readonly obligation: { readonly id: string; readonly payloadSchema: z.ZodTypeAny };
74
74
  /** The COMPLETE (success) Receipt directive: payload = {requestId, resultRef, completedAt}. */
75
75
  readonly complete: { readonly id: string };
76
76
  /** The FAIL (failure) Receipt directive: payload = {requestId, failureReason, failedAt}. */
@@ -81,17 +81,23 @@ export interface ImpureCapabilityDecl {
81
81
  readonly deadLetter: { readonly id: string };
82
82
  }
83
83
 
84
- /** The framework ORDER-ENVELOPE field names — provenance, NOT tenant params. The Order
85
- * type carries them (the provider sees the whole Order intent) but the codegen labels
84
+ /** The framework Obligation envelope field names — provenance/timing, NOT tenant params. The Obligation
85
+ * type carries them (the provider sees the whole Obligation intent) but the codegen labels
86
86
  * them so the doc separates provenance from the tenant's request params. */
87
- const ORDER_ENVELOPE_FIELDS = new Set(["requestId", "requestedBy", "requestedAt"]);
87
+ const OBLIGATION_ENVELOPE_FIELDS = new Set([
88
+ "requestId",
89
+ "requestedBy",
90
+ "requestedAt",
91
+ "scheduledAt",
92
+ "deadlineAt",
93
+ ]);
88
94
 
89
- /** A stable PascalCase base for a task's generated class family, derived from its ORDER
90
- * directive id with a leading `create`/`order` verb stripped (so `createTranscribeVoiceNote`
95
+ /** A stable PascalCase base for a task's generated class family, derived from its Obligation
96
+ * directive id with a leading `create`/`oblige` verb stripped (so `createTranscribeVoiceNote`
91
97
  * → `TranscribeVoiceNote`). Pure + deterministic. */
92
- export function taskBaseName(orderDirectiveId: string): string {
93
- const stripped = orderDirectiveId.replace(/^(create|order)/, "");
94
- const base = stripped === "" ? orderDirectiveId : stripped;
98
+ export function taskBaseName(obligationDirectiveId: string): string {
99
+ const stripped = obligationDirectiveId.replace(/^(create|oblige)/, "");
100
+ const base = stripped === "" ? obligationDirectiveId : stripped;
95
101
  return cap(base);
96
102
  }
97
103
 
@@ -109,21 +115,21 @@ function deadLetterPayloadClass(deadLetterDirectiveId: string): string {
109
115
  return `${cap(deadLetterDirectiveId)}Payload`;
110
116
  }
111
117
 
112
- /** Dart field DECL for one Order field (optional → nullable). */
113
- function orderFieldDecl(s: PayloadFieldSpec): string {
114
- const provenance = ORDER_ENVELOPE_FIELDS.has(s.name)
118
+ /** Dart field DECL for one Obligation field (optional → nullable). */
119
+ function obligationFieldDecl(s: PayloadFieldSpec): string {
120
+ const provenance = OBLIGATION_ENVELOPE_FIELDS.has(s.name)
115
121
  ? ` // envelope provenance (framework)`
116
122
  : ``;
117
123
  return ` final ${s.dartType}${s.optional ? "?" : ""} ${s.name};${provenance}`;
118
124
  }
119
125
 
120
- /** Dart ctor PARAM for one Order field. */
121
- function orderCtorParam(s: PayloadFieldSpec): string {
126
+ /** Dart ctor PARAM for one Obligation field. */
127
+ function obligationCtorParam(s: PayloadFieldSpec): string {
122
128
  return ` ${s.optional ? "" : "required "}this.${s.name},`;
123
129
  }
124
130
 
125
131
  /**
126
- * Expression decoding ONE Order field FROM the already-`jsonDecode`d payload map `j`.
132
+ * Expression decoding ONE Obligation field FROM the already-`jsonDecode`d payload map `j`.
127
133
  * The REVERSE of the peer codegen's `toJsonExpr` — same field-kind matrix:
128
134
  * • scalar — read directly, cast to the Dart scalar type.
129
135
  * • enum — `XEnum.fromWire(j['f'] as String)`.
@@ -132,7 +138,7 @@ function orderCtorParam(s: PayloadFieldSpec): string {
132
138
  * Optional fields tolerate absence (return null); required fields cast strictly (a
133
139
  * missing required field throws on the cast — the strict-decode contract).
134
140
  */
135
- function orderFromJsonExpr(s: PayloadFieldSpec): string {
141
+ function obligationFromJsonExpr(s: PayloadFieldSpec): string {
136
142
  const raw = `j['${s.name}']`;
137
143
  switch (s.toJson) {
138
144
  case "scalar": {
@@ -168,22 +174,22 @@ function orderFromJsonExpr(s: PayloadFieldSpec): string {
168
174
  }
169
175
  }
170
176
 
171
- /** Emit the typed `XOrder` class — the DECODED incoming Order. */
172
- function emitOrderClass(
177
+ /** Emit the typed `XObligation` class — the DECODED incoming Obligation. */
178
+ function emitObligationClass(
173
179
  base: string,
174
- orderDirectiveId: string,
180
+ obligationDirectiveId: string,
175
181
  specs: PayloadFieldSpec[],
176
182
  ): string {
177
- const cls = `${base}Order`;
178
- const fieldDecls = specs.map(orderFieldDecl).join("\n");
179
- const ctorParams = specs.map(orderCtorParam).join("\n");
183
+ const cls = `${base}Obligation`;
184
+ const fieldDecls = specs.map(obligationFieldDecl).join("\n");
185
+ const ctorParams = specs.map(obligationCtorParam).join("\n");
180
186
  const fromJsonAssigns = specs
181
- .map((s) => ` ${s.name}: ${orderFromJsonExpr(s)},`)
187
+ .map((s) => ` ${s.name}: ${obligationFromJsonExpr(s)},`)
182
188
  .join("\n");
183
189
  return [
184
- `/// The typed ORDER for the \`${orderDirectiveId}\` capability — what the provider`,
190
+ `/// The typed Obligation for the \`${obligationDirectiveId}\` capability — what the provider`,
185
191
  `/// RECEIVES. Carries the framework envelope provenance (requestId / requestedBy /`,
186
- `/// requestedAt) ⊕ the tenant request params. Decoded from the incoming Order intent's`,
192
+ `/// requestedAt / scheduledAt / deadlineAt) ⊕ the tenant request params. Decoded from the incoming Obligation intent's`,
187
193
  `/// payload JSON via [${cls}.fromJson]; the handler sees ONLY this typed value.`,
188
194
  `class ${cls} {`,
189
195
  fieldDecls,
@@ -192,8 +198,8 @@ function emitOrderClass(
192
198
  ctorParams,
193
199
  ` });`,
194
200
  ``,
195
- ` /// Decode an Order from the already-\`jsonDecode\`d payload map (the EXACT shape the`,
196
- ` /// peer's \`${orderDirectiveId}\` \`toPayloadJson()\` produced). Reverse of the peer`,
201
+ ` /// Decode an Obligation from the already-\`jsonDecode\`d payload map (the EXACT shape the`,
202
+ ` /// peer's \`${obligationDirectiveId}\` \`toPayloadJson()\` produced). Reverse of the peer`,
197
203
  ` /// encode — types stop here, at the JSON serde.`,
198
204
  ` factory ${cls}.fromJson(Map<String, Object?> j) => ${cls}(`,
199
205
  fromJsonAssigns,
@@ -224,7 +230,7 @@ function emitResultClasses(base: string, completeDirectiveId: string): string {
224
230
  ``,
225
231
  `/// The typed FAILURE the \`${base}\` handler returns — the recorded reason the side-effect`,
226
232
  `/// failed. Maps to the framework \`fail…\` Receipt (status:failed + failureReason +`,
227
- `/// closesOrder); a recorded failure is terminal, NOT a retry storm.`,
233
+ `/// closesObligation); a recorded failure is terminal, NOT a retry storm.`,
228
234
  `class ${failureCls} {`,
229
235
  ` final String failureReason;`,
230
236
  ` const ${failureCls}({required this.failureReason});`,
@@ -237,7 +243,7 @@ function emitProviderClass(
237
243
  task: ImpureCapabilityDecl,
238
244
  base: string,
239
245
  ): string {
240
- const orderCls = `${base}Order`;
246
+ const obligationCls = `${base}Obligation`;
241
247
  const resultCls = `${base}Result`;
242
248
  const handlerTypedef = `${base}Handler`;
243
249
  const providerCls = `${base}Provider`;
@@ -247,25 +253,25 @@ function emitProviderClass(
247
253
  const deadLetterCls = deadLetterPayloadClass(task.deadLetter.id);
248
254
  return [
249
255
  `/// The typed HANDLER the dev writes for the \`${base}\` capability: given the typed`,
250
- `/// [${orderCls}], perform the side-effect and return a typed [TaskOutcome] — a`,
256
+ `/// [${obligationCls}], perform the side-effect and return a typed [TaskOutcome] — a`,
251
257
  `/// [TaskSuccess]<[${resultCls}]>, [TaskFailure], [TaskBlocked], or [TaskDeadLetter]. NO raw JSON, NO directive id, NO`,
252
258
  `/// Receipt construction: the dev writes pure typed business logic; the [${providerCls}]`,
253
259
  `/// does the decode/encode round-trip.`,
254
- `typedef ${handlerTypedef} = TaskOutcome<${resultCls}> Function(${orderCls} order);`,
260
+ `typedef ${handlerTypedef} = TaskOutcome<${resultCls}> Function(${obligationCls} obligation);`,
255
261
  ``,
256
262
  `/// The GENERATED dispatcher for the \`${base}\` capability — the symmetric twin of the`,
257
- `/// peer's typed dispatch. Register a typed [${handlerTypedef}]; [handleOrder] DECODES an`,
258
- `/// incoming Order intent JSON into [${orderCls}], runs the handler, and ENCODES the typed`,
263
+ `/// peer's typed dispatch. Register a typed [${handlerTypedef}]; [handleObligation] DECODES an`,
264
+ `/// incoming Obligation intent JSON into [${obligationCls}], runs the handler, and ENCODES the typed`,
259
265
  `/// outcome into the ordinary RECEIPT intent: [${completeCls}], [${failCls}],`,
260
266
  `/// [${blockCls}], or [${deadLetterCls}]. The returned [NomosIntent] ships through the SAME`,
261
267
  `/// \`dispatch\` gate as any authored write (ONE-WRITE-PATH).`,
262
- `class ${providerCls} implements TaskProvider<${orderCls}, ${resultCls}> {`,
268
+ `class ${providerCls} implements TaskProvider<${obligationCls}, ${resultCls}> {`,
263
269
  ` /// The dev's typed handler.`,
264
270
  ` final ${handlerTypedef} handler;`,
265
271
  ` const ${providerCls}(this.handler);`,
266
272
  ``,
267
273
  ` @override`,
268
- ` String get orderDirectiveId => '${task.order.id}';`,
274
+ ` String get obligationDirectiveId => '${task.obligation.id}';`,
269
275
  ` @override`,
270
276
  ` String get completeDirectiveId => '${task.complete.id}';`,
271
277
  ` @override`,
@@ -276,39 +282,39 @@ function emitProviderClass(
276
282
  ` String get deadLetterDirectiveId => '${task.deadLetter.id}';`,
277
283
  ``,
278
284
  ` @override`,
279
- ` NomosIntent handleOrder(String orderPayloadJson, {required HostClock now}) =>`,
280
- ` handleOrderMap(decodeOrderPayload(orderPayloadJson), now: now);`,
285
+ ` NomosIntent handleObligation(String obligationPayloadJson, {required HostClock now}) =>`,
286
+ ` handleObligationMap(decodeObligationPayload(obligationPayloadJson), now: now);`,
281
287
  ``,
282
288
  ` @override`,
283
- ` NomosIntent handleOrderMap(`,
284
- ` Map<String, Object?> orderPayload, {`,
289
+ ` NomosIntent handleObligationMap(`,
290
+ ` Map<String, Object?> obligationPayload, {`,
285
291
  ` required HostClock now,`,
286
292
  ` }) {`,
287
- ` final order = ${orderCls}.fromJson(orderPayload);`,
288
- ` final outcome = handler(order);`,
293
+ ` final obligation = ${obligationCls}.fromJson(obligationPayload);`,
294
+ ` final outcome = handler(obligation);`,
289
295
  ` // ENCODE the outcome into the ordinary RECEIPT intent (reusing the peer-codegen Receipt`,
290
296
  ` // payload classes — same generated file). The Receipt addresses the SAME instance`,
291
- ` // the Order created (keyed by requestId), so closesOrder == requestId by`,
297
+ ` // the Obligation created (keyed by requestId), so closesObligation == requestId by`,
292
298
  ` // construction in the engine's \`.plan()\` — terminal-once is the kernel's gate.`,
293
299
  ` return switch (outcome) {`,
294
300
  ` TaskSuccess<${resultCls}>(:final result) => ${completeCls}(`,
295
- ` requestId: order.requestId,`,
301
+ ` requestId: obligation.requestId,`,
296
302
  ` resultRef: result.toResultRef(),`,
297
303
  ` completedAt: now(),`,
298
304
  ` ),`,
299
305
  ` TaskFailure<${resultCls}>(:final failureReason) => ${failCls}(`,
300
- ` requestId: order.requestId,`,
306
+ ` requestId: obligation.requestId,`,
301
307
  ` failureReason: failureReason,`,
302
308
  ` failedAt: now(),`,
303
309
  ` ),`,
304
310
  ` TaskBlocked<${resultCls}>(:final blockedReason, :final nextAction) => ${blockCls}(`,
305
- ` requestId: order.requestId,`,
311
+ ` requestId: obligation.requestId,`,
306
312
  ` blockedReason: blockedReason,`,
307
313
  ` blockedAt: now(),`,
308
314
  ` nextAction: nextAction,`,
309
315
  ` ),`,
310
316
  ` TaskDeadLetter<${resultCls}>(:final deadLetterReason) => ${deadLetterCls}(`,
311
- ` requestId: order.requestId,`,
317
+ ` requestId: obligation.requestId,`,
312
318
  ` deadLetterReason: deadLetterReason,`,
313
319
  ` deadLetteredAt: now(),`,
314
320
  ` ),`,
@@ -319,20 +325,20 @@ function emitProviderClass(
319
325
  }
320
326
 
321
327
  /**
322
- * Emit the full provider SDK for ONE `impureCapability`: the typed Order/Result/Failure
328
+ * Emit the full provider SDK for ONE `impureCapability`: the typed Obligation/Result/Failure
323
329
  * classes + the handler typedef + the dispatcher. `enumDartTypes` is the SAME map the
324
- * domain's `collectEnums` produced (so an enum-typed order param references the
330
+ * domain's `collectEnums` produced (so an enum-typed obligation param references the
325
331
  * already-emitted domain enum — NO duplicate enum decl).
326
332
  */
327
333
  export function emitProviderTask(
328
334
  task: ImpureCapabilityDecl,
329
335
  enumDartTypes: Map<string, string>,
330
336
  ): string {
331
- const base = taskBaseName(task.order.id);
332
- const specs = introspectPayload(task.order.payloadSchema, enumDartTypes);
337
+ const base = taskBaseName(task.obligation.id);
338
+ const specs = introspectPayload(task.obligation.payloadSchema, enumDartTypes);
333
339
  return [
334
340
  `// ── provider SDK: ${base} (impureCapability '${task.aggregate.id}') ──`,
335
- emitOrderClass(base, task.order.id, specs),
341
+ emitObligationClass(base, task.obligation.id, specs),
336
342
  ``,
337
343
  emitResultClasses(base, task.complete.id),
338
344
  ``,