@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.
- package/dart/example/nomos_webview_transport.dart +2 -2
- package/dart/lib/nomos_dsl.dart +4 -4
- package/dart/lib/src/{dispatch.dart → offer.dart} +7 -7
- package/dart/lib/src/provider.dart +30 -30
- package/dart/lib/src/subscriptions.dart +3 -3
- package/dart/lib/src/webview_bridge.dart +56 -21
- package/dart/test/webview_bridge_test.dart +35 -34
- package/package.json +5 -3
- package/src/aggregate.ts +9 -1
- package/src/authoring.ts +10 -1
- package/src/build_package.ts +1 -1
- package/src/capability_exports.ts +2 -2
- package/src/codegen_dart.ts +34 -24
- package/src/codegen_dart_client.ts +74 -12
- package/src/codegen_provider_dart.ts +67 -61
- package/src/codegen_ts.ts +45 -17
- package/src/compile_package_main.ts +103 -36
- package/src/ctx.ts +13 -0
- package/src/engine_entry.ts +22 -5
- package/src/fields.ts +44 -0
- package/src/flow_gate.ts +208 -0
- package/src/framework/capability.ts +29 -11
- package/src/framework/encryption_scope.ts +9 -1
- package/src/framework/genesis.ts +5 -1
- package/src/framework/impure_capability.ts +73 -67
- package/src/framework/relations.ts +23 -4
- package/src/framework/signing_identity.ts +1 -1
- package/src/framework/sync_lifecycle.ts +3 -3
- package/src/framework/workspace_invariant.ts +29 -8
- package/src/framework/workspaces.ts +7 -0
- package/src/index.ts +25 -1
- package/src/manifest.ts +11 -17
- package/src/ops.ts +42 -1
- package/src/principal_evidence.ts +112 -0
- package/src/relation_sugar.ts +10 -4
- package/src/shape_compat.ts +208 -0
- package/src/usd.ts +287 -0
- package/src/usd_layers.ts +213 -26
- package/src/wire.ts +8 -0
- package/src/wire_encode.ts +75 -2
- package/src/workspace_sharding.ts +197 -24
- package/src/workspace_type.ts +1 -0
package/src/codegen_dart.ts
CHANGED
|
@@ -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
|
|
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 [`
|
|
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
|
|
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/
|
|
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.
|
|
1053
|
-
`///
|
|
1054
|
-
`///
|
|
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).
|
|
1066
|
-
` ///
|
|
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
|
|
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
|
-
`///
|
|
1475
|
-
`/// \`null\` when a freshly-folded aggregate has not yet folded
|
|
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).
|
|
1487
|
-
` ///
|
|
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 [
|
|
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
|
|
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
|
|
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 `
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 —
|
|
26
|
-
* like `codegen_dart.ts`. NO FALLBACK: an unsupported shape throws at compile
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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 '
|
|
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 '
|
|
226
|
+
`export '${supportImport}' show NomosBridge, BridgeTransport, NomosBridgeError, kWebviewBridgeVersion;`,
|
|
205
227
|
``,
|
|
206
|
-
`/// sha256 of the deployed \`.package.usda\` — the installed law this client
|
|
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
|
-
*
|
|
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`
|
|
20
|
-
* hand-parse the incoming
|
|
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(
|
|
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 `
|
|
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
|
|
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(
|
|
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, `
|
|
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 `.
|
|
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
|
|
73
|
-
readonly
|
|
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
|
|
85
|
-
* type carries them (the provider sees the whole
|
|
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
|
|
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
|
|
90
|
-
* directive id with a leading `create`/`
|
|
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(
|
|
93
|
-
const stripped =
|
|
94
|
-
const base = 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
|
|
113
|
-
function
|
|
114
|
-
const provenance =
|
|
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
|
|
121
|
-
function
|
|
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
|
|
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
|
|
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 `
|
|
172
|
-
function
|
|
177
|
+
/** Emit the typed `XObligation` class — the DECODED incoming Obligation. */
|
|
178
|
+
function emitObligationClass(
|
|
173
179
|
base: string,
|
|
174
|
-
|
|
180
|
+
obligationDirectiveId: string,
|
|
175
181
|
specs: PayloadFieldSpec[],
|
|
176
182
|
): string {
|
|
177
|
-
const cls = `${base}
|
|
178
|
-
const fieldDecls = specs.map(
|
|
179
|
-
const ctorParams = specs.map(
|
|
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}: ${
|
|
187
|
+
.map((s) => ` ${s.name}: ${obligationFromJsonExpr(s)},`)
|
|
182
188
|
.join("\n");
|
|
183
189
|
return [
|
|
184
|
-
`/// The typed
|
|
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
|
|
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
|
|
196
|
-
` /// peer's \`${
|
|
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
|
-
`///
|
|
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
|
|
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
|
-
`/// [${
|
|
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(${
|
|
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}]; [
|
|
258
|
-
`/// incoming
|
|
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<${
|
|
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
|
|
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
|
|
280
|
-
`
|
|
285
|
+
` NomosIntent handleObligation(String obligationPayloadJson, {required HostClock now}) =>`,
|
|
286
|
+
` handleObligationMap(decodeObligationPayload(obligationPayloadJson), now: now);`,
|
|
281
287
|
``,
|
|
282
288
|
` @override`,
|
|
283
|
-
` NomosIntent
|
|
284
|
-
` Map<String, Object?>
|
|
289
|
+
` NomosIntent handleObligationMap(`,
|
|
290
|
+
` Map<String, Object?> obligationPayload, {`,
|
|
285
291
|
` required HostClock now,`,
|
|
286
292
|
` }) {`,
|
|
287
|
-
` final
|
|
288
|
-
` final outcome = handler(
|
|
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
|
|
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:
|
|
301
|
+
` requestId: obligation.requestId,`,
|
|
296
302
|
` resultRef: result.toResultRef(),`,
|
|
297
303
|
` completedAt: now(),`,
|
|
298
304
|
` ),`,
|
|
299
305
|
` TaskFailure<${resultCls}>(:final failureReason) => ${failCls}(`,
|
|
300
|
-
` 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:
|
|
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:
|
|
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
|
|
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
|
|
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.
|
|
332
|
-
const specs = introspectPayload(task.
|
|
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
|
-
|
|
341
|
+
emitObligationClass(base, task.obligation.id, specs),
|
|
336
342
|
``,
|
|
337
343
|
emitResultClasses(base, task.complete.id),
|
|
338
344
|
``,
|