@githolon/dsl 0.37.0 → 0.38.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/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## 0.29.0
|
|
2
|
+
|
|
3
|
+
- LAW-MINTED IDS RETURN TO THE CALLER: `NomosBridge.offerCreates(...)` → `NomosOfferOutcome{head, created}` — a `.creates` plan mints its aggregate id inside the sealed intent (payloads never carry one); the offer now hands the minted ids back, so generated create methods return the typed id with no query-after round-trip. New types `NomosOfferOutcome` / `NomosCreated`.
|
|
4
|
+
- `NomosBridge.mintId(typeTag)` — reserve a kernel-minted typed id (`<TypeTag>_<uuidv7>`) via the holon's front-door mint, for app-side references. (`.creates` payloads still carry NO id — the law mints those.)
|
|
5
|
+
- Generated Dart clients (`nomos-compile` ≥0.38 tooling): `.creates(...)` directive methods now return `Future<NomosOfferOutcome>` instead of `Future<String>` — `outcome.created.single.id` is the new aggregate's typed id.
|
|
6
|
+
|
|
1
7
|
## 0.28.0
|
|
2
8
|
|
|
3
9
|
- `NomosTestHarness` (`nomos_headless.dart`): ephemeral real-law fixtures — one call births a throwaway cloud workspace, deploys compiled `*.deploy.json` law, and hands out independent client replicas (`connect(clientId:)`) over the node runner. Identity defaults to the `githolon login --agent` session (`~/.holon/credentials.json`). Live-proven: birth → deploy → write on one replica → converge + typed read on another, 14s end-to-end.
|
|
@@ -265,6 +265,23 @@ class SyncStatus {
|
|
|
265
265
|
);
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
+
/// One law-minted aggregate from a `.creates(...)` offer: the full typed [id] (`<domain>.<Type>_<uuidv7>`)
|
|
269
|
+
/// and its [type] (`<domain>.<Type>`).
|
|
270
|
+
class NomosCreated {
|
|
271
|
+
final String type;
|
|
272
|
+
final String id;
|
|
273
|
+
const NomosCreated({required this.type, required this.id});
|
|
274
|
+
@override
|
|
275
|
+
String toString() => id;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/// The result of [NomosBridge.offerCreates]: the new local [head] plus every aggregate the plan minted.
|
|
279
|
+
class NomosOfferOutcome {
|
|
280
|
+
final String head;
|
|
281
|
+
final List<NomosCreated> created;
|
|
282
|
+
const NomosOfferOutcome({required this.head, required this.created});
|
|
283
|
+
}
|
|
284
|
+
|
|
268
285
|
/// THE BRIDGE. Construct over a [BridgeTransport], `await ready`, then `connect(...)` and drive the holon.
|
|
269
286
|
class NomosBridge {
|
|
270
287
|
final BridgeTransport _transport;
|
|
@@ -509,6 +526,42 @@ class NomosBridge {
|
|
|
509
526
|
return r['head'].toString();
|
|
510
527
|
}
|
|
511
528
|
|
|
529
|
+
/// [offerDirective], answering the LAW-MINTED ids too. A `.creates(...)` plan mints its aggregate id
|
|
530
|
+
/// inside the sealed intent (callers never supply one); the runner hands back what was minted, so a
|
|
531
|
+
/// generated create method returns the typed id with NO query-after round-trip. [created] is empty for
|
|
532
|
+
/// pure mutations.
|
|
533
|
+
Future<NomosOfferOutcome> offerCreates({
|
|
534
|
+
required String domain,
|
|
535
|
+
required String directiveId,
|
|
536
|
+
required Object? payload,
|
|
537
|
+
String? actor,
|
|
538
|
+
String? domainHash,
|
|
539
|
+
}) async {
|
|
540
|
+
final r = _asMap(await _call('offerDirective', {
|
|
541
|
+
'domain': domain,
|
|
542
|
+
'directiveId': directiveId,
|
|
543
|
+
'payload': payload,
|
|
544
|
+
if (actor != null) 'actor': actor,
|
|
545
|
+
if (domainHash != null) 'domainHash': domainHash,
|
|
546
|
+
}));
|
|
547
|
+
return NomosOfferOutcome(
|
|
548
|
+
head: r['head'].toString(),
|
|
549
|
+
created: ((r['created'] as List?) ?? const [])
|
|
550
|
+
.whereType<Map>()
|
|
551
|
+
.map((m) => NomosCreated(
|
|
552
|
+
type: (m['type'] ?? '').toString(),
|
|
553
|
+
id: (m['id'] ?? '').toString()))
|
|
554
|
+
.toList(),
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/// Reserve a KERNEL-MINTED typed id (`<TypeTag>_<uuidv7>`) via the holon's front-door mint — for app-side
|
|
559
|
+
/// references (routing keys, optimistic UI). `.creates` payloads still carry NO id: the LAW mints those
|
|
560
|
+
/// (see [offerCreates], which returns them).
|
|
561
|
+
Future<String> mintId(String typeTag) async =>
|
|
562
|
+
(_asMap(await _call('mintId', {'typeTag': typeTag}))['id'] ?? '')
|
|
563
|
+
.toString();
|
|
564
|
+
|
|
512
565
|
/// UNDO the most-recent action via the kernel STRIKE primitive (the fold drops the struck intent by parity).
|
|
513
566
|
/// [scope] `'all'` undoes any author's most-recent action; `'user'` only THIS replica's own. [actions] is the
|
|
514
567
|
/// app's set of undoable directive ids (so undo never strikes a framework/control intent). [domain] +
|
package/dart/pubspec.yaml
CHANGED
|
@@ -11,7 +11,7 @@ description: >-
|
|
|
11
11
|
Nomos 2 Dart frontend types — the hand-written kernel wire types plus the
|
|
12
12
|
code-generated per-domain aggregate + directive-payload types. (De)serializes
|
|
13
13
|
the EXACT serde JSON the Rust kernel + projection produce/consume.
|
|
14
|
-
version: 0.
|
|
14
|
+
version: 0.29.0
|
|
15
15
|
|
|
16
16
|
environment:
|
|
17
17
|
sdk: ^3.5.0
|
|
@@ -44,7 +44,9 @@ void main() {
|
|
|
44
44
|
try {
|
|
45
45
|
final writer = await harness.connect(clientId: 'writer');
|
|
46
46
|
final reader = await harness.connect(clientId: 'reader');
|
|
47
|
-
await writer.
|
|
47
|
+
final minted = await writer.mintId('Fixture');
|
|
48
|
+
expect(minted, startsWith('Fixture_'));
|
|
49
|
+
final outcome = await writer.offerCreates(
|
|
48
50
|
domain: 'guestbook',
|
|
49
51
|
directiveId: 'signGuestbook',
|
|
50
52
|
payload: {
|
|
@@ -53,10 +55,13 @@ void main() {
|
|
|
53
55
|
'mood': 'delighted',
|
|
54
56
|
'signedAt': DateTime.now().toUtc().toIso8601String(),
|
|
55
57
|
});
|
|
58
|
+
expect(outcome.created, hasLength(1));
|
|
59
|
+
expect(outcome.created.single.type, 'guestbook.GuestbookEntry');
|
|
56
60
|
await writer.sync(admit: true);
|
|
57
61
|
await reader.sync(admit: false);
|
|
58
62
|
final rows = await reader.query('entriesByAuthor', {'author': 'harness'});
|
|
59
63
|
expect(rows, hasLength(1));
|
|
64
|
+
expect(rows.first['id'], outcome.created.single.id);
|
|
60
65
|
expect(
|
|
61
66
|
(rows.first['data'] as Map)['message'], 'ephemeral fixture green');
|
|
62
67
|
stdout.writeln(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.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",
|
|
@@ -142,7 +142,29 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
142
142
|
const gateDoc = gate !== undefined
|
|
143
143
|
? ` /// GATED: needs an \`actor\` (the \`${gate.relation}\` relation) — throws MissingOfferAuthority otherwise.`
|
|
144
144
|
: "";
|
|
145
|
-
if (
|
|
145
|
+
if ((d as { marker?: string }).marker === "creates") {
|
|
146
|
+
// LAW-MINTED ID RETURN: a `.creates` plan mints its aggregate id inside the sealed intent (the payload
|
|
147
|
+
// carries none — callers never supply ids). The bridge's offerCreates hands the minted ids back with the
|
|
148
|
+
// head, so the create method returns the typed id with NO query-after round-trip.
|
|
149
|
+
methods.push(
|
|
150
|
+
` /// Author \`${domain}/${d.id}\` — the LAW mints the ${(d as { aggregateId?: string }).aggregateId ?? "aggregate"} id inside the sealed`,
|
|
151
|
+
` /// intent; it returns here (\`outcome.created\`) with the new local head. No id in the payload, no query-after.`,
|
|
152
|
+
...(stamps.length > 0 ? [` /// Omitted ${stamps.map((s) => `\`${s}\``).join(" / ")} auto-stamp(s) ISO now() at offer.`] : []),
|
|
153
|
+
...(gateDoc ? [gateDoc] : []),
|
|
154
|
+
` Future<NomosOfferOutcome> ${method}(${payloadClass} payload) {`,
|
|
155
|
+
...(gateGuard ? [gateGuard] : []),
|
|
156
|
+
` final data = payload.toPayloadJson();`,
|
|
157
|
+
...(stamps.length > 0
|
|
158
|
+
? [
|
|
159
|
+
` final now = DateTime.now().toUtc().toIso8601String();`,
|
|
160
|
+
...stamps.map((s) => ` data.putIfAbsent(${dartStr(s)}, () => now);`),
|
|
161
|
+
]
|
|
162
|
+
: []),
|
|
163
|
+
` return bridge.offerCreates(domain: ${dartStr(domain)}, directiveId: ${dartStr(d.id)}, payload: data, actor: actor, domainHash: domainHash);`,
|
|
164
|
+
` }`,
|
|
165
|
+
``,
|
|
166
|
+
);
|
|
167
|
+
} else if (stamps.length > 0) {
|
|
146
168
|
methods.push(
|
|
147
169
|
` /// Author \`${domain}/${d.id}\` locally under the installed law; returns the new local head.`,
|
|
148
170
|
` /// Omitted ${stamps.map((s) => `\`${s}\``).join(" / ")} auto-stamp(s) ISO now() at offer.`,
|
package/src/codegen_ts.ts
CHANGED
|
@@ -400,7 +400,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
400
400
|
` /** THE ONE ENCODER offer (the single v2-envelope codec lives in @githolon/client \`encodeIntentOfferBytes\`):`,
|
|
401
401
|
` * hand the holon a PLANNED directive + the verified \`actor\`; the holon encodes + offers. The generated`,
|
|
402
402
|
` * client calls THIS, never building offer bytes itself — so there is exactly ONE copy of the wire format. */`,
|
|
403
|
-
` offerDirective(domain: string, directiveId: string, payload: unknown, opts?: { domainHash?: string; actor?: string }): Promise<string>;`,
|
|
403
|
+
` offerDirective(domain: string, directiveId: string, payload: unknown, opts?: { domainHash?: string; actor?: string; returnCreated?: boolean }): Promise<string>;`,
|
|
404
404
|
` /** Opaque-bytes offer — retained for carriers that already hold an encoded envelope (back-compat). */`,
|
|
405
405
|
` offer(intentBytes: string | Uint8Array): Promise<string>;`,
|
|
406
406
|
` query(queryId: string, params?: Record<string, unknown>): Promise<unknown>;`,
|
|
@@ -753,6 +753,22 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
753
753
|
continue;
|
|
754
754
|
}
|
|
755
755
|
const offerPayload = stamps.length ? `stampNow(payload, ${JSON.stringify(stamps)})` : `payload`;
|
|
756
|
+
if (d.marker === "creates") {
|
|
757
|
+
// LAW-MINTED ID RETURN: a `.creates` plan mints its aggregate id inside the sealed intent (the payload
|
|
758
|
+
// carries none). The holon hands the minted ids back on the offer (`returnCreated`), so the create
|
|
759
|
+
// method returns the typed id with NO query-after. Older holons (pre-returnCreated) answer a bare head
|
|
760
|
+
// string — degrade to id:"" rather than refuse (structural binding, era-safe).
|
|
761
|
+
methods.push(
|
|
762
|
+
` /** Author \`${domain}/${d.id}\`: the LAW mints the ${d.aggregateId ?? "aggregate"} id inside the sealed intent`,
|
|
763
|
+
` * and it returns here with the new local head — no id in the payload, no query-after.${gate ? ` GATED: needs \`{ actor }\` (\`${gate.relation}\`).` : ""} */`,
|
|
764
|
+
` ${d.id}: async (payload: ${payloadName}): Promise<{ head: string; id: string }> => {`,
|
|
765
|
+
...(gateGuard ? [gateGuard.replace(/\n$/, "")] : []),
|
|
766
|
+
` const r = await (holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${offerPayload}, { domainHash, actor, returnCreated: true }) as Promise<string | { head: string; created?: { type: string; id: string }[] }>);`,
|
|
767
|
+
` return typeof r === "object" && r !== null ? { head: r.head, id: r.created?.[0]?.id ?? "" } : { head: String(r), id: "" };`,
|
|
768
|
+
` },`,
|
|
769
|
+
);
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
756
772
|
const offerCall = `holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${offerPayload}, { domainHash, actor })`;
|
|
757
773
|
if (gateGuard) {
|
|
758
774
|
// GATED directive: a block body that throws MissingOfferAuthority when no actor is bound — an
|