@githolon/dsl 0.13.0 → 0.15.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 +157 -29
- package/dart/test/webview_bridge_test.dart +193 -50
- package/package.json +5 -3
- package/src/build_package.ts +1 -1
- package/src/capability_exports.ts +2 -2
- package/src/codegen_dart.ts +12 -12
- package/src/codegen_dart_client.ts +20 -7
- package/src/codegen_provider_dart.ts +67 -61
- package/src/codegen_ts.ts +18 -15
- package/src/compile_package_main.ts +13 -16
- package/src/framework/capability.ts +10 -10
- package/src/framework/impure_capability.ts +73 -67
- package/src/framework/sync_lifecycle.ts +3 -3
- package/src/framework/workspaces.ts +7 -0
- package/src/index.ts +1 -1
- package/src/usd.ts +156 -0
- package/src/usd_layers.ts +170 -26
- package/src/workspace_sharding.ts +2 -7
|
@@ -19,7 +19,8 @@ class FakeRunner implements BridgeTransport {
|
|
|
19
19
|
|
|
20
20
|
FakeRunner() {
|
|
21
21
|
// the runner announces readiness at construction.
|
|
22
|
-
scheduleMicrotask(
|
|
22
|
+
scheduleMicrotask(
|
|
23
|
+
() => _incoming.add({'v': kWebviewBridgeVersion, 'event': 'ready'}));
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
@override
|
|
@@ -32,34 +33,97 @@ class FakeRunner implements BridgeTransport {
|
|
|
32
33
|
final op = message['op'] as String;
|
|
33
34
|
final args = (message['args'] as Map).cast<String, Object?>();
|
|
34
35
|
// reply exactly as the runner's ops do (the contract both ends share).
|
|
35
|
-
Map<String, Object?> reply(Object? result) =>
|
|
36
|
+
Map<String, Object?> reply(Object? result) =>
|
|
37
|
+
{'v': kWebviewBridgeVersion, 'id': id, 'ok': true, 'result': result};
|
|
36
38
|
scheduleMicrotask(() {
|
|
37
39
|
switch (op) {
|
|
38
|
-
case 'connect':
|
|
39
|
-
|
|
40
|
+
case 'connect':
|
|
41
|
+
_incoming.add(reply({'workspace': args['workspace']}));
|
|
42
|
+
break;
|
|
43
|
+
case 'offer':
|
|
40
44
|
// a directive the law refuses answers ok:false (the runner frames the gate's verdict).
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
final intentJson =
|
|
46
|
+
utf8.decode(base64Decode(args['intentBytes'] as String));
|
|
47
|
+
if (intentJson.contains('"directiveId":"refuseMe"')) {
|
|
48
|
+
_incoming.add({
|
|
49
|
+
'v': kWebviewBridgeVersion,
|
|
50
|
+
'id': id,
|
|
51
|
+
'ok': false,
|
|
52
|
+
'error': 'the law refused this intent'
|
|
53
|
+
});
|
|
43
54
|
} else {
|
|
44
|
-
_incoming.add(reply({'head': 'HEAD
|
|
55
|
+
_incoming.add(reply({'head': 'HEAD-offer'}));
|
|
45
56
|
}
|
|
46
57
|
break;
|
|
47
|
-
case '
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
case 'query':
|
|
59
|
+
_incoming.add(reply({
|
|
60
|
+
'rows': [
|
|
61
|
+
{
|
|
62
|
+
'id': 'a',
|
|
63
|
+
'data': {'q': args['queryId']}
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
}));
|
|
67
|
+
break;
|
|
68
|
+
case 'queryById':
|
|
69
|
+
_incoming.add(reply({
|
|
70
|
+
'rows': [
|
|
71
|
+
{
|
|
72
|
+
'id': args['id'],
|
|
73
|
+
'data': {'ok': true}
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}));
|
|
77
|
+
break;
|
|
78
|
+
case 'count':
|
|
79
|
+
_incoming.add(reply({'count': 7}));
|
|
80
|
+
break;
|
|
81
|
+
case 'sync':
|
|
82
|
+
_incoming.add(reply({'pushed': true}));
|
|
83
|
+
break;
|
|
84
|
+
case 'autoSync':
|
|
85
|
+
_incoming.add(reply({'started': true}));
|
|
86
|
+
break;
|
|
87
|
+
case 'lawStatus':
|
|
88
|
+
_incoming.add(reply({'installed': true, 'drifted': false}));
|
|
89
|
+
break;
|
|
90
|
+
case 'badop':
|
|
91
|
+
_incoming.add({
|
|
92
|
+
'v': kWebviewBridgeVersion,
|
|
93
|
+
'id': id,
|
|
94
|
+
'ok': false,
|
|
95
|
+
'error': 'the law refused this intent'
|
|
96
|
+
});
|
|
97
|
+
break;
|
|
98
|
+
case 'watch':
|
|
99
|
+
lastWatchSubId = args['subId'] as String;
|
|
100
|
+
_incoming.add(reply({'subId': args['subId']}));
|
|
101
|
+
break;
|
|
102
|
+
case 'unwatch':
|
|
103
|
+
_incoming.add(reply({'subId': args['subId']}));
|
|
104
|
+
break;
|
|
105
|
+
default:
|
|
106
|
+
_incoming.add({
|
|
107
|
+
'v': kWebviewBridgeVersion,
|
|
108
|
+
'id': id,
|
|
109
|
+
'ok': false,
|
|
110
|
+
'error': "unknown op '$op'"
|
|
111
|
+
});
|
|
57
112
|
}
|
|
58
113
|
});
|
|
59
114
|
}
|
|
60
115
|
|
|
61
116
|
/// Simulate the holon firing a reactive change to an active watch.
|
|
62
|
-
void pushWatch(String subId, List<dynamic> rows) => _incoming.add({
|
|
117
|
+
void pushWatch(String subId, List<dynamic> rows) => _incoming.add({
|
|
118
|
+
'v': kWebviewBridgeVersion,
|
|
119
|
+
'event': 'watch',
|
|
120
|
+
'subId': subId,
|
|
121
|
+
'rows': rows
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
/// Simulate the runner forwarding a signal holon projection.
|
|
125
|
+
void pushSignal(Map<String, Object?> signal) => _incoming
|
|
126
|
+
.add({'v': kWebviewBridgeVersion, 'event': 'signal', 'signal': signal});
|
|
63
127
|
}
|
|
64
128
|
|
|
65
129
|
/// A transport that answers EVERY request with one canned `result` (the golden corpus's reply) and records
|
|
@@ -69,7 +133,8 @@ class ReplayTransport implements BridgeTransport {
|
|
|
69
133
|
final _incoming = StreamController<Map<String, Object?>>.broadcast();
|
|
70
134
|
Map<String, Object?>? lastRequest;
|
|
71
135
|
ReplayTransport(this.result) {
|
|
72
|
-
scheduleMicrotask(
|
|
136
|
+
scheduleMicrotask(
|
|
137
|
+
() => _incoming.add({'v': kWebviewBridgeVersion, 'event': 'ready'}));
|
|
73
138
|
}
|
|
74
139
|
@override
|
|
75
140
|
Stream<Map<String, Object?>> get messages => _incoming.stream;
|
|
@@ -77,7 +142,8 @@ class ReplayTransport implements BridgeTransport {
|
|
|
77
142
|
void send(Map<String, Object?> message) {
|
|
78
143
|
lastRequest = message;
|
|
79
144
|
final id = message['id'];
|
|
80
|
-
scheduleMicrotask(() => _incoming.add(
|
|
145
|
+
scheduleMicrotask(() => _incoming.add(
|
|
146
|
+
{'v': kWebviewBridgeVersion, 'id': id, 'ok': true, 'result': result}));
|
|
81
147
|
}
|
|
82
148
|
}
|
|
83
149
|
|
|
@@ -91,24 +157,29 @@ Map<String, Object?> _loadCorpus() {
|
|
|
91
157
|
];
|
|
92
158
|
for (final p in candidates) {
|
|
93
159
|
final f = File(p);
|
|
94
|
-
if (f.existsSync())
|
|
160
|
+
if (f.existsSync())
|
|
161
|
+
return (jsonDecode(f.readAsStringSync()) as Map).cast<String, Object?>();
|
|
95
162
|
}
|
|
96
|
-
fail(
|
|
163
|
+
fail(
|
|
164
|
+
'golden corpus not found from ${Directory.current.path}; tried $candidates');
|
|
97
165
|
}
|
|
98
166
|
|
|
99
167
|
/// Drive one corpus op through the typed bridge method and return its typed result (so we can compare it to
|
|
100
168
|
/// what the corpus says the unwrapped value should be).
|
|
101
|
-
Future<Object?> _invoke(
|
|
169
|
+
Future<Object?> _invoke(
|
|
170
|
+
NomosBridge b, String op, Map<String, Object?> a) async {
|
|
102
171
|
switch (op) {
|
|
103
172
|
case 'connect':
|
|
104
|
-
await b.connect(
|
|
173
|
+
await b.connect(
|
|
174
|
+
cloud: a['cloud'] as String,
|
|
175
|
+
workspace: a['workspace'] as String,
|
|
176
|
+
clientId: a['clientId'] as String?);
|
|
105
177
|
return null;
|
|
106
|
-
case '
|
|
107
|
-
return b.
|
|
108
|
-
case 'deliverShare':
|
|
109
|
-
return b.deliverShare(quote: (a['quote'] as Map).cast<String, Object?>(), domainHash: a['domainHash'] as String);
|
|
178
|
+
case 'offer':
|
|
179
|
+
return b.offer(intentBytes: a['intentBytes'] as String);
|
|
110
180
|
case 'query':
|
|
111
|
-
return b.query(
|
|
181
|
+
return b.query(
|
|
182
|
+
a['queryId'] as String, (a['params'] as Map).cast<String, Object?>());
|
|
112
183
|
case 'queryById':
|
|
113
184
|
return b.queryById(a['id'] as String);
|
|
114
185
|
case 'count':
|
|
@@ -127,8 +198,9 @@ Future<Object?> _invoke(NomosBridge b, String op, Map<String, Object?> a) async
|
|
|
127
198
|
/// What the corpus says the typed (unwrapped) return value should be, given the reply's `result`.
|
|
128
199
|
Object? _expectedTyped(String op, Map<String, Object?> result) {
|
|
129
200
|
switch (op) {
|
|
130
|
-
case '
|
|
131
|
-
|
|
201
|
+
case 'offer':
|
|
202
|
+
return result['head'];
|
|
203
|
+
case 'offer':
|
|
132
204
|
return result['head'];
|
|
133
205
|
case 'query':
|
|
134
206
|
case 'queryById':
|
|
@@ -153,7 +225,8 @@ void main() {
|
|
|
153
225
|
group('golden corpus (shared with the JS runner)', () {
|
|
154
226
|
final corpus = _loadCorpus();
|
|
155
227
|
final cases = (corpus['cases'] as List).cast<Map<String, Object?>>();
|
|
156
|
-
test('version pins the bridge version',
|
|
228
|
+
test('version pins the bridge version',
|
|
229
|
+
() => expect(corpus['version'], kWebviewBridgeVersion));
|
|
157
230
|
for (final c in cases) {
|
|
158
231
|
final op = c['op'] as String;
|
|
159
232
|
final args = (c['args'] as Map).cast<String, Object?>();
|
|
@@ -163,8 +236,12 @@ void main() {
|
|
|
163
236
|
final bridge = NomosBridge(transport);
|
|
164
237
|
await bridge.ready;
|
|
165
238
|
final typed = await _invoke(bridge, op, args);
|
|
166
|
-
expect((transport.lastRequest!['args'] as Map).cast<String, Object?>(),
|
|
167
|
-
|
|
239
|
+
expect((transport.lastRequest!['args'] as Map).cast<String, Object?>(),
|
|
240
|
+
equals(args),
|
|
241
|
+
reason: 'sent request args');
|
|
242
|
+
if (op != 'connect')
|
|
243
|
+
expect(typed, equals(_expectedTyped(op, result)),
|
|
244
|
+
reason: 'parsed typed result');
|
|
168
245
|
await bridge.dispose();
|
|
169
246
|
});
|
|
170
247
|
}
|
|
@@ -172,50 +249,116 @@ void main() {
|
|
|
172
249
|
|
|
173
250
|
late FakeRunner runner;
|
|
174
251
|
late NomosBridge bridge;
|
|
175
|
-
setUp(() {
|
|
252
|
+
setUp(() {
|
|
253
|
+
runner = FakeRunner();
|
|
254
|
+
bridge = NomosBridge(runner);
|
|
255
|
+
});
|
|
176
256
|
tearDown(() => bridge.dispose());
|
|
177
257
|
|
|
178
258
|
test('ready completes once the runner boots', () async {
|
|
179
259
|
await bridge.ready.timeout(const Duration(seconds: 1));
|
|
180
260
|
});
|
|
181
261
|
|
|
182
|
-
test('
|
|
262
|
+
test('offer round-trips a typed head; args reach the runner verbatim',
|
|
263
|
+
() async {
|
|
183
264
|
await bridge.ready;
|
|
184
|
-
final
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
265
|
+
final intentBytes = base64Encode(utf8.encode(jsonEncode({
|
|
266
|
+
'v': 2,
|
|
267
|
+
'intent': {
|
|
268
|
+
'domain': 'guestbook',
|
|
269
|
+
'directiveId': 'signGuestbook',
|
|
270
|
+
'payload': {'author': 'ada'},
|
|
271
|
+
'domainHash': 'abc'
|
|
272
|
+
}
|
|
273
|
+
})));
|
|
274
|
+
final head = await bridge.offer(intentBytes: intentBytes);
|
|
275
|
+
expect(head, 'HEAD-offer');
|
|
276
|
+
final req = runner.sent.firstWhere((m) => m['op'] == 'offer');
|
|
277
|
+
expect((req['args'] as Map)['intentBytes'], intentBytes);
|
|
189
278
|
});
|
|
190
279
|
|
|
191
|
-
test(
|
|
280
|
+
test(
|
|
281
|
+
'connect / query / queryById / count / sync / lawStatus all wrap the runner',
|
|
282
|
+
() async {
|
|
192
283
|
await bridge.ready;
|
|
193
284
|
await bridge.connect(cloud: 'https://c', workspace: 'ws', clientId: 'c1');
|
|
194
|
-
expect(await bridge.
|
|
195
|
-
|
|
285
|
+
expect((await bridge.query('byAuthor', {'author': 'ada'})).first['data'],
|
|
286
|
+
{'q': 'byAuthor'});
|
|
196
287
|
expect((await bridge.queryById('Entry_1')).first['id'], 'Entry_1');
|
|
197
288
|
expect(await bridge.count('entries'), 7);
|
|
198
289
|
expect(await bridge.sync(), {'pushed': true});
|
|
199
|
-
expect(
|
|
290
|
+
expect(
|
|
291
|
+
await bridge.lawStatus('abc'), {'installed': true, 'drifted': false});
|
|
200
292
|
});
|
|
201
293
|
|
|
202
|
-
test(
|
|
294
|
+
test(
|
|
295
|
+
'watch is a reactive stream; a runner push reaches the listener; cancel unwatches',
|
|
296
|
+
() async {
|
|
203
297
|
await bridge.ready;
|
|
204
298
|
final got = <List<dynamic>>[];
|
|
205
299
|
final sub = bridge.watch('byAuthor', {'author': 'ada'}).listen(got.add);
|
|
206
300
|
await Future<void>.delayed(Duration.zero); // let the watch register
|
|
207
|
-
runner.pushWatch(runner.lastWatchSubId!, [
|
|
301
|
+
runner.pushWatch(runner.lastWatchSubId!, [
|
|
302
|
+
{
|
|
303
|
+
'id': 'a',
|
|
304
|
+
'data': {'n': 2}
|
|
305
|
+
}
|
|
306
|
+
]);
|
|
208
307
|
await Future<void>.delayed(Duration.zero);
|
|
209
|
-
expect(got, [
|
|
308
|
+
expect(got, [
|
|
309
|
+
[
|
|
310
|
+
{
|
|
311
|
+
'id': 'a',
|
|
312
|
+
'data': {'n': 2}
|
|
313
|
+
}
|
|
314
|
+
]
|
|
315
|
+
]);
|
|
210
316
|
await sub.cancel();
|
|
211
317
|
expect(runner.sent.any((m) => m['op'] == 'unwatch'), isTrue);
|
|
212
318
|
});
|
|
213
319
|
|
|
214
|
-
test('
|
|
320
|
+
test('signal events are forwarded on their own stream', () async {
|
|
321
|
+
await bridge.ready;
|
|
322
|
+
final got = <Map<String, Object?>>[];
|
|
323
|
+
final sub = bridge.signalEvents.listen(got.add);
|
|
324
|
+
runner.pushSignal({
|
|
325
|
+
'type': 'signal.projection',
|
|
326
|
+
'domain': 'presence',
|
|
327
|
+
'projection': {
|
|
328
|
+
'peers': [
|
|
329
|
+
{'clientId': 'prof-a', 'behindBest': 141},
|
|
330
|
+
],
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
await Future<void>.delayed(Duration.zero);
|
|
334
|
+
expect(got.single['signal'], isA<Map>());
|
|
335
|
+
expect(((got.single['signal'] as Map)['projection'] as Map)['peers'],
|
|
336
|
+
isA<List>());
|
|
337
|
+
await sub.cancel();
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test('autoSync forwards a scoped signal id', () async {
|
|
341
|
+
await bridge.ready;
|
|
342
|
+
await bridge.autoSync(signalId: 'signal:ws:list:main');
|
|
343
|
+
final req = runner.sent.firstWhere((m) => m['op'] == 'autoSync');
|
|
344
|
+
expect((req['args'] as Map)['signalId'], 'signal:ws:list:main');
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test('a law refusal throws NomosBridgeError with the verbatim message',
|
|
348
|
+
() async {
|
|
215
349
|
await bridge.ready;
|
|
216
350
|
Object? err;
|
|
217
351
|
try {
|
|
218
|
-
await bridge.
|
|
352
|
+
await bridge.offer(
|
|
353
|
+
intentBytes: base64Encode(utf8.encode(jsonEncode({
|
|
354
|
+
'v': 2,
|
|
355
|
+
'intent': {
|
|
356
|
+
'domain': 'x',
|
|
357
|
+
'directiveId': 'refuseMe',
|
|
358
|
+
'payload': {},
|
|
359
|
+
'domainHash': 'h'
|
|
360
|
+
}
|
|
361
|
+
}))));
|
|
219
362
|
} on NomosBridgeError catch (e) {
|
|
220
363
|
err = e;
|
|
221
364
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
},
|
|
12
12
|
"main": "src/index.ts",
|
|
13
13
|
"bin": {
|
|
14
|
-
"nomos-compile": "
|
|
15
|
-
"holon-compile": "
|
|
14
|
+
"nomos-compile": "compile_package.mjs",
|
|
15
|
+
"holon-compile": "compile_package.mjs"
|
|
16
16
|
},
|
|
17
17
|
"exports": {
|
|
18
18
|
".": "./src/index.ts",
|
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
"src",
|
|
45
45
|
"!src/**/*.test.ts",
|
|
46
46
|
"dart",
|
|
47
|
+
"!dart/.dart_tool",
|
|
48
|
+
"!dart/build",
|
|
47
49
|
"compile_package.mjs"
|
|
48
50
|
],
|
|
49
51
|
"publishConfig": {
|
package/src/build_package.ts
CHANGED
|
@@ -157,7 +157,7 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
157
157
|
const aggregates = [...byId.values()];
|
|
158
158
|
const directiveIds = new Set(directives.map((d) => d.id));
|
|
159
159
|
for (const task of spec.impureCapabilities ?? []) {
|
|
160
|
-
for (const directive of [task.
|
|
160
|
+
for (const directive of [task.obligation, task.complete, task.fail, task.block, task.deadLetter]) {
|
|
161
161
|
if (!directiveIds.has(directive.id)) {
|
|
162
162
|
throw new Error(
|
|
163
163
|
`domain '${spec.domain ?? spec.name}' declares impureCapability '${task.aggregate.id}' ` +
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* properties — invisible to those walks. This helper, applied at each plane's
|
|
17
17
|
* merge point (the engine lump's `mergeModules`, the package composer's merge,
|
|
18
18
|
* the compile CLI's merge), ADDS the bundle's pieces to the exports bag under
|
|
19
|
-
* synthetic names (`<export>$aggregate`, `<export>$
|
|
19
|
+
* synthetic names (`<export>$aggregate`, `<export>$obligation`, …) so every
|
|
20
20
|
* existing shape-walk discovers them with ZERO new walk semantics. The
|
|
21
21
|
* original bundle export stays (config-named lanes still resolve it); the
|
|
22
22
|
* walks ignore its unmatched shape exactly as before.
|
|
@@ -34,7 +34,7 @@ export function isCapabilityDecl(v: unknown): v is Record<string, unknown> {
|
|
|
34
34
|
);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
const PIECES = ["aggregate", "
|
|
37
|
+
const PIECES = ["aggregate", "obligation", "complete", "fail", "block", "deadLetter", "tasks"] as const;
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* Expand every `capability()` bundle in an exports bag into discoverable
|
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
|
|
@@ -828,7 +828,7 @@ function emitPermissionVocabulary(vocab: PermissionVocabulary | undefined): stri
|
|
|
828
828
|
return [
|
|
829
829
|
`/// Generated permission vocabulary from the domain role catalogue.`,
|
|
830
830
|
`///`,
|
|
831
|
-
`/// This is frontend read/
|
|
831
|
+
`/// This is frontend read/offer vocabulary only. Domain execution and`,
|
|
832
832
|
`/// admission still happen inside the installed GitHolon domain law.`,
|
|
833
833
|
`class PermissionRole {`,
|
|
834
834
|
` const PermissionRole(this.value, this.capabilities);`,
|
|
@@ -1125,7 +1125,7 @@ function emitEvidenceRefClass(): string {
|
|
|
1125
1125
|
` );`,
|
|
1126
1126
|
` }`,
|
|
1127
1127
|
``,
|
|
1128
|
-
` /// The authored payload shape (for
|
|
1128
|
+
` /// The authored payload shape (for offer): the typed value-object the DSL`,
|
|
1129
1129
|
` /// \`t.evidence()\` field lowers to a kernel \`Value::Evidence\`.`,
|
|
1130
1130
|
` Map<String, Object?> toJson() => <String, Object?>{`,
|
|
1131
1131
|
` 'hash': hash,`,
|
|
@@ -1536,7 +1536,7 @@ function emitDirective(
|
|
|
1536
1536
|
`/// Generated typed intent DTO for '${d.id}' (marker: ${d.marker},`,
|
|
1537
1537
|
`/// aggregate: ${d.aggregateId}). It carries its [domain] + [intentType] and a`,
|
|
1538
1538
|
`/// [toPayloadJson] that ships only typed fields as data. It builds no`,
|
|
1539
|
-
`/// \`Event\` and no \`Hlc\`. Pass it to [
|
|
1539
|
+
`/// \`Event\` and no \`Hlc\`. Pass it to [offer]; the GitHolon executes`,
|
|
1540
1540
|
`/// the installed domain plan.`,
|
|
1541
1541
|
`class ${className} implements NomosIntent {`,
|
|
1542
1542
|
` /// The policy domain this intent belongs to (the kernel \`author\` \`domain\`).`,
|
|
@@ -1587,7 +1587,7 @@ export function toJsonExpr(spec: PayloadFieldSpec, accessor: string): string {
|
|
|
1587
1587
|
|
|
1588
1588
|
// ---- read accessors (typed subscriptions) ----------------------------------
|
|
1589
1589
|
//
|
|
1590
|
-
// The READ half of the peer
|
|
1590
|
+
// The READ half of the peer query surface (Jack 2026-06-18): the app reads ONLY via
|
|
1591
1591
|
// these generated typed accessors over the GitHolon read surface
|
|
1592
1592
|
// (`query(query_id, params_json)` + `query_by_id(aggregate_id)`, #140's indexed/PK
|
|
1593
1593
|
// read-closure). It invents NO new Rust read primitive. Two emitters:
|
|
@@ -2503,11 +2503,11 @@ export interface DomainModule {
|
|
|
2503
2503
|
*/
|
|
2504
2504
|
readModelAggregates?: string[];
|
|
2505
2505
|
/**
|
|
2506
|
-
* The domain's `impureCapability` capabilities (framework
|
|
2506
|
+
* The domain's `impureCapability` capabilities (framework Obligation→Receipt; `impure_capability.ts`)
|
|
2507
2507
|
* 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 `
|
|
2508
|
+
* payload classes. For each declared task the codegen emits a typed `XObligation` (+
|
|
2509
2509
|
* `fromJson`), `XResult`/`XFailure`, a `XHandler` typedef and a `XProvider` dispatcher
|
|
2510
|
-
* that decodes the incoming
|
|
2510
|
+
* that decodes the incoming Obligation JSON and encodes the Receipt (reusing the
|
|
2511
2511
|
* peer-codegen `CompleteXPayload`/`FailXPayload`, so the Receipt is an ordinary
|
|
2512
2512
|
* intent through the ONE write path). ADDITIVE + OPTIONAL: a domain that omits this is
|
|
2513
2513
|
* byte-identical to before. NOT carried into any manifest/hash — a pure provider-side
|
|
@@ -2571,10 +2571,10 @@ export function generateDartDomain(mod: DomainModule): string {
|
|
|
2571
2571
|
const mintedBlock = emitMintedCreates(mod.aggregates, mod.directives, domainKey, dartTypes);
|
|
2572
2572
|
|
|
2573
2573
|
// PROVIDER SDK (the symmetric twin of the peer payload classes): for each declared
|
|
2574
|
-
// `impureCapability` capability, a typed
|
|
2574
|
+
// `impureCapability` capability, a typed Obligation/Result/Failure + handler typedef + dispatcher.
|
|
2575
2575
|
// ADDITIVE + omit-when-empty: a domain with no `impureCapabilities` emits nothing (and the
|
|
2576
2576
|
// `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
|
|
2577
|
+
// SHARED with the directive/aggregate codegen so an enum-typed obligation param references the
|
|
2578
2578
|
// already-emitted domain enum (no duplicate decl).
|
|
2579
2579
|
const providerBlock = emitProviderSdk(mod.impureCapabilities ?? [], dartTypes);
|
|
2580
2580
|
|
|
@@ -2766,7 +2766,7 @@ export function generateDartDomain(mod: DomainModule): string {
|
|
|
2766
2766
|
``,
|
|
2767
2767
|
]
|
|
2768
2768
|
: []),
|
|
2769
|
-
`// ---- public intents (typed DTOs; ship via the generic
|
|
2769
|
+
`// ---- public intents (typed DTOs; ship via the generic offer() verb) ----`,
|
|
2770
2770
|
dirBlock,
|
|
2771
2771
|
``,
|
|
2772
2772
|
...(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
|
|
@@ -81,7 +81,7 @@ function rowToModel(model: string, rowVar: string): string {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
export interface DartBridgeClientOptions {
|
|
84
|
-
/** sha256 of the built `.package.usda` — the law this client
|
|
84
|
+
/** sha256 of the built `.package.usda` — the law this client offers under. */
|
|
85
85
|
readonly domainHash: string;
|
|
86
86
|
}
|
|
87
87
|
|
|
@@ -95,7 +95,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
95
95
|
|
|
96
96
|
const methods: string[] = [];
|
|
97
97
|
|
|
98
|
-
// ── directives → typed
|
|
98
|
+
// ── directives → typed offer ──
|
|
99
99
|
const dirs = (mod.directives as AnyDirective[]).filter(
|
|
100
100
|
(d) => d && typeof d.id === "string" && typeof (d as { payloadSchema?: unknown }).payloadSchema === "object",
|
|
101
101
|
);
|
|
@@ -106,12 +106,12 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
106
106
|
if (stamps.length > 0) {
|
|
107
107
|
methods.push(
|
|
108
108
|
` /// 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
|
|
109
|
+
` /// Omitted ${stamps.map((s) => `\`${s}\``).join(" / ")} auto-stamp(s) ISO now() at offer.`,
|
|
110
110
|
` Future<String> ${method}(${payloadClass} payload) {`,
|
|
111
111
|
` final data = payload.toPayloadJson();`,
|
|
112
112
|
` final now = DateTime.now().toUtc().toIso8601String();`,
|
|
113
113
|
...stamps.map((s) => ` data.putIfAbsent(${dartStr(s)}, () => now);`),
|
|
114
|
-
` return bridge.
|
|
114
|
+
` return bridge.offer(intentBytes: _encodeIntentOffer(${dartStr(domain)}, ${dartStr(d.id)}, data, domainHash));`,
|
|
115
115
|
` }`,
|
|
116
116
|
``,
|
|
117
117
|
);
|
|
@@ -119,7 +119,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
119
119
|
methods.push(
|
|
120
120
|
` /// Author \`${domain}/${d.id}\` locally under the installed law; returns the new local head.`,
|
|
121
121
|
` Future<String> ${method}(${payloadClass} payload) =>`,
|
|
122
|
-
` bridge.
|
|
122
|
+
` bridge.offer(intentBytes: _encodeIntentOffer(${dartStr(domain)}, ${dartStr(d.id)}, payload.toPayloadJson(), domainHash));`,
|
|
123
123
|
``,
|
|
124
124
|
);
|
|
125
125
|
}
|
|
@@ -196,6 +196,8 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
196
196
|
`// final app = ${className}(bridge);`,
|
|
197
197
|
`// await app.${dirs[0] ? safeMethod(dirs[0].id) : "someDirective"}(/* typed payload */);`,
|
|
198
198
|
``,
|
|
199
|
+
`import 'dart:convert';`,
|
|
200
|
+
``,
|
|
199
201
|
`import 'nomos_dsl.dart';`,
|
|
200
202
|
`import '${mod.name}.dart';`,
|
|
201
203
|
`// Re-export the sibling types (read models + payloads + the bridge runtime) so ONE import of this`,
|
|
@@ -203,9 +205,20 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
203
205
|
`export '${mod.name}.dart';`,
|
|
204
206
|
`export 'nomos_dsl.dart' show NomosBridge, BridgeTransport, NomosBridgeError, kWebviewBridgeVersion;`,
|
|
205
207
|
``,
|
|
206
|
-
`/// sha256 of the deployed \`.package.usda\` — the installed law this client
|
|
208
|
+
`/// sha256 of the deployed \`.package.usda\` — the installed law this client offers under.`,
|
|
207
209
|
`const String ${hashConst} = ${dartStr(opts.domainHash)};`,
|
|
208
210
|
``,
|
|
211
|
+
`String _encodeIntentOffer(String domain, String directiveId, Object? payload, String domainHash) =>`,
|
|
212
|
+
` base64Encode(utf8.encode(jsonEncode({`,
|
|
213
|
+
` 'v': 2,`,
|
|
214
|
+
` 'intent': {`,
|
|
215
|
+
` 'domain': domain,`,
|
|
216
|
+
` 'directiveId': directiveId,`,
|
|
217
|
+
` 'payload': payload,`,
|
|
218
|
+
` 'domainHash': domainHash,`,
|
|
219
|
+
` },`,
|
|
220
|
+
` })));`,
|
|
221
|
+
``,
|
|
209
222
|
`/// Typed async client for the \`${domain}\` domain, bound to a connected [NomosBridge].`,
|
|
210
223
|
`class ${className} {`,
|
|
211
224
|
` final NomosBridge bridge;`,
|