@githolon/dsl 0.103.18 → 0.104.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 +4 -0
- package/dart/lib/src/provider.dart +17 -8
- package/dart/lib/src/runner/node-entry.mjs +3 -2
- package/dart/lib/src/runner/offline-install.mjs +1 -1
- package/dart/pubspec.yaml +1 -1
- package/package.json +1 -1
- package/runtime/generated-application-host.js +2 -1
- package/src/aggregate.ts +43 -2
- package/src/capability_exports.ts +4 -1
- package/src/codegen_dart.ts +170 -2
- package/src/codegen_dart_client.ts +4 -1
- package/src/codegen_provider_dart.ts +81 -18
- package/src/compile_package_main.ts +4 -4
- package/src/framework/capability.ts +151 -42
- package/src/framework/impure_capability.ts +91 -36
- package/src/framework/owned_custody.ts +26 -2
- package/src/framework/sync_lifecycle.ts +23 -14
- package/src/fulfilment.ts +187 -0
- package/src/index.ts +8 -0
- package/src/law_graph_usda.ts +5 -0
- package/src/query/compile.ts +0 -0
package/dart/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
## 0.64.16
|
|
2
|
+
|
|
3
|
+
- Rebundle the runner runtime (`node-entry.mjs`, `offline-install.mjs`) generated from `@githolon/client` 0.103.19, which carries the evolve-gate fix: `promoteDomain` dispositions are read from the law that DECLARES the call, so a law upgrade with a genuine field/aggregate removal is promotable through the offer lane.
|
|
4
|
+
|
|
1
5
|
## 0.64.15
|
|
2
6
|
|
|
3
7
|
- `DomainClientModelAdoptionRequired.accept` gains an optional `dispositions` parameter, threaded through to the bridge's `nomosFrameworkAdoptGeneratedModel` — a tenant can now acknowledge a genuine field/aggregate removal (`{"retired": [sid, ...]}`) from a generated client's `model.adopt()` decision instead of only via a raw adapter script.
|
|
@@ -90,15 +90,24 @@ final class TaskDeadLetter<R> extends TaskOutcome<R> {
|
|
|
90
90
|
const TaskDeadLetter(this.deadLetterReason);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
/// A typed RESULT a provider handler
|
|
94
|
-
///
|
|
95
|
-
///
|
|
96
|
-
///
|
|
97
|
-
///
|
|
98
|
-
///
|
|
93
|
+
/// A typed RESULT a provider handler returns for a Receipt.
|
|
94
|
+
///
|
|
95
|
+
/// A capability's DECLARED receipt fields travel as TYPED USDA values on the `complete…`
|
|
96
|
+
/// payload — the mirror of the Obligation's request params — so [toReceiptFields] returns them
|
|
97
|
+
/// keyed by their law field names and the dispatcher spreads them beside
|
|
98
|
+
/// `{requestId, completedAt}`.
|
|
99
|
+
///
|
|
100
|
+
/// [toResultRef] is the address of a produced ARTIFACT (a blob ref / url) and is `null` unless
|
|
101
|
+
/// the side-effect produced one. It was previously the single JSON string a structured result
|
|
102
|
+
/// was encoded into; that made receipt values invisible to the law — untypeable, unindexable,
|
|
103
|
+
/// and silently dropped when a key was misspelled.
|
|
99
104
|
abstract interface class TaskResult {
|
|
100
|
-
///
|
|
101
|
-
|
|
105
|
+
/// The address of a produced artifact, or `null` when the result is carried by declared
|
|
106
|
+
/// receipt fields.
|
|
107
|
+
String? toResultRef();
|
|
108
|
+
|
|
109
|
+
/// The declared receipt values, keyed by their law field names.
|
|
110
|
+
Map<String, Object?> toReceiptFields();
|
|
102
111
|
}
|
|
103
112
|
|
|
104
113
|
/// The HOST CLOCK seam: the provider stamps the Receipt's terminal time (epoch-ms) from
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// <define:__NOMOS_KERNEL_RELEASE__>
|
|
4
|
-
var define_NOMOS_KERNEL_RELEASE_default = { production: { hash: "
|
|
4
|
+
var define_NOMOS_KERNEL_RELEASE_default = { production: { hash: "7d3fcc3501bef23849493026e10ce75b3ef3d1400f789181d03f4a3b0b5b1fdb", version: "kernel-7d3fcc35", features: ["compoundKeys", "capturedReads", "invariantGate", "surfacedMerge", "fieldProvenance", "nullSemantics", "attestedReads", "conflictSnapshots", "ownedAudience", "temporalEnum"], protocol: 1 }, testMint: { hash: "0ae2f9ba0fa5ec2eccb798fdcb99f46a82d76c03d851a9fc62eed47ffc25c394", version: "kernel-0ae2f9ba", features: ["compoundKeys", "capturedReads", "invariantGate", "surfacedMerge", "fieldProvenance", "nullSemantics", "attestedReads", "conflictSnapshots", "ownedAudience", "temporalEnum"], protocol: 1 } };
|
|
5
5
|
|
|
6
6
|
// runner/node-entry.ts
|
|
7
7
|
import { createInterface } from "node:readline";
|
|
@@ -10112,7 +10112,8 @@ ${String(commit).toLowerCase()}`;
|
|
|
10112
10112
|
try {
|
|
10113
10113
|
const outcome = await offerDirective("nomos", "promoteDomain", prepared.payload, {
|
|
10114
10114
|
actor: connectionPrincipal,
|
|
10115
|
-
returnCreated: false
|
|
10115
|
+
returnCreated: false,
|
|
10116
|
+
domainHash: String(currentLawInner()?.["nomos"] ?? "")
|
|
10116
10117
|
});
|
|
10117
10118
|
const head = typeof outcome === "string" ? outcome : String(outcome?.head ?? "");
|
|
10118
10119
|
const readiness = await prepareDomainReady(domain, candidateHash);
|
|
@@ -6,7 +6,7 @@ var __export = (target, all) => {
|
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
// <define:__NOMOS_KERNEL_RELEASE__>
|
|
9
|
-
var define_NOMOS_KERNEL_RELEASE_default = { production: { hash: "
|
|
9
|
+
var define_NOMOS_KERNEL_RELEASE_default = { production: { hash: "7d3fcc3501bef23849493026e10ce75b3ef3d1400f789181d03f4a3b0b5b1fdb", version: "kernel-7d3fcc35", features: ["compoundKeys", "capturedReads", "invariantGate", "surfacedMerge", "fieldProvenance", "nullSemantics", "attestedReads", "conflictSnapshots", "ownedAudience", "temporalEnum"], protocol: 1 }, testMint: { hash: "0ae2f9ba0fa5ec2eccb798fdcb99f46a82d76c03d851a9fc62eed47ffc25c394", version: "kernel-0ae2f9ba", features: ["compoundKeys", "capturedReads", "invariantGate", "surfacedMerge", "fieldProvenance", "nullSemantics", "attestedReads", "conflictSnapshots", "ownedAudience", "temporalEnum"], protocol: 1 } };
|
|
10
10
|
|
|
11
11
|
// runner/offline-install.ts
|
|
12
12
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync } from "node:fs";
|
package/dart/pubspec.yaml
CHANGED
|
@@ -7,7 +7,7 @@ repository: https://github.com/Captain-App/nomos2
|
|
|
7
7
|
description: >-
|
|
8
8
|
Nomos 2 Dart client — generated domain values, intent payloads and application
|
|
9
9
|
lifecycle types over the typed Nomos kernel protocol and USDA custody model.
|
|
10
|
-
version: 0.64.
|
|
10
|
+
version: 0.64.16
|
|
11
11
|
|
|
12
12
|
environment:
|
|
13
13
|
sdk: ^3.5.0
|
package/package.json
CHANGED
|
@@ -9163,7 +9163,8 @@ ${String(commit).toLowerCase()}`;
|
|
|
9163
9163
|
try {
|
|
9164
9164
|
const outcome = await offerDirective("nomos", "promoteDomain", prepared.payload, {
|
|
9165
9165
|
actor: connectionPrincipal,
|
|
9166
|
-
returnCreated: false
|
|
9166
|
+
returnCreated: false,
|
|
9167
|
+
domainHash: String(currentLawInner()?.["nomos"] ?? "")
|
|
9167
9168
|
});
|
|
9168
9169
|
const head = typeof outcome === "string" ? outcome : String(outcome?.head ?? "");
|
|
9169
9170
|
const readiness = await prepareDomainReady(domain, candidateHash);
|
package/src/aggregate.ts
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
import type { Field } from "./fields.js";
|
|
17
17
|
import { admitsNull, attributionField, t } from "./fields.js";
|
|
18
18
|
import { satisfiesContract, type ContractHandle } from "./contract.js";
|
|
19
|
+
import type { FulfilmentDecl } from "./fulfilment.js";
|
|
20
|
+
import { Lww } from "./drivers.js";
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* A pure, replay-stable predicate over one aggregate's PROJECTED snapshot —
|
|
@@ -196,6 +198,10 @@ export interface AggregateHandle<
|
|
|
196
198
|
readonly hasMany: HasManyFields<F>;
|
|
197
199
|
/** Brand so a plain string is never accepted where a handle is required. */
|
|
198
200
|
readonly __isAggregateHandle: true;
|
|
201
|
+
/** The completeness declaration (`fulfilment.ts`), when this aggregate declares one. Codegen
|
|
202
|
+
* reads it to emit the SEALED read model whose variants make each tier's guarantee a
|
|
203
|
+
* NON-NULLABLE field — the law's guarantee becoming a type-level one. */
|
|
204
|
+
readonly fulfilment?: FulfilmentDecl;
|
|
199
205
|
/** Name-only dependency handle. Export-safe, but never itself discoverable as domain law. */
|
|
200
206
|
readonly __isForwardRef?: true;
|
|
201
207
|
/** DOMAIN CONTRACTS this aggregate `.implements(...)` — the stable public shapes it satisfies (compile-
|
|
@@ -330,6 +336,13 @@ export interface AudienceMethods<Id extends string, F extends Record<string, Fie
|
|
|
330
336
|
const DART_TYPE_NAME_RE = /^[A-Z][A-Za-z0-9_]*$/;
|
|
331
337
|
|
|
332
338
|
type AggregateOptions = {
|
|
339
|
+
/**
|
|
340
|
+
* COMPLETENESS AS LAW (`fulfilment.ts`). Declares the record's completeness TIERS: at tier T
|
|
341
|
+
* the guarantees of T and every tier beneath it must hold, so an early record is LEGAL at a
|
|
342
|
+
* low tier instead of being refused into fabricated placeholders, while a record CLAIMING a
|
|
343
|
+
* tier it has not reached is a typed refusal.
|
|
344
|
+
*/
|
|
345
|
+
fulfils?: FulfilmentDecl;
|
|
333
346
|
invariant?: AggregateInvariantFn;
|
|
334
347
|
invariantDeclassify?: string;
|
|
335
348
|
cap?: number;
|
|
@@ -364,6 +377,22 @@ export function aggregate<const Id extends string, F extends Record<string, Fiel
|
|
|
364
377
|
);
|
|
365
378
|
}
|
|
366
379
|
const { stored, hasMany } = partitionFields(fields);
|
|
380
|
+
// THE TIER FIELDS (`fulfilment.ts`). Completeness is law-state, so it is a real declared field
|
|
381
|
+
// — hashed, projected, queryable, evolvable — not something a reader re-derives by guessing
|
|
382
|
+
// which columns happen to be null. The blocked sibling records a TERMINAL reason, so "no
|
|
383
|
+
// address exists here" is distinguishable from "still resolving".
|
|
384
|
+
if (opts?.fulfils !== undefined) {
|
|
385
|
+
const decl = opts.fulfils;
|
|
386
|
+
for (const reserved of [decl.name, decl.blockedField]) {
|
|
387
|
+
if (stored[reserved] !== undefined) {
|
|
388
|
+
throw new Error(
|
|
389
|
+
`aggregate '${id}': field '${reserved}' collides with the fulfilment '${decl.name}' (it owns '${decl.name}' + '${decl.blockedField}') — rename your field`,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
stored[decl.name] = t.enum(decl.tiers as readonly [string, ...string[]]).merge(Lww).optional();
|
|
394
|
+
stored[decl.blockedField] = t.string().merge(Lww).optional();
|
|
395
|
+
}
|
|
367
396
|
// FOREIGN-REF SIBLING CHECK (pain point #4): a `t.foreignRef({ workspace, target })` field names a
|
|
368
397
|
// SIBLING string field (`workspaceField`) that holds the foreign workspace id — the target workspace is
|
|
369
398
|
// DYNAMIC, read from that sibling at resolve time. That link is STRINGLY-TYPED (a bare field name), so a
|
|
@@ -528,6 +557,17 @@ export function aggregate<const Id extends string, F extends Record<string, Fiel
|
|
|
528
557
|
.map(([name]) => name)
|
|
529
558
|
.sort();
|
|
530
559
|
const userInvariant = opts?.invariant;
|
|
560
|
+
// TIER-SCOPED COMPLETENESS (`fulfilment.ts`), composed like the fromBirth presence check and
|
|
561
|
+
// BEFORE the user's own law: structural claims first, domain judgement second.
|
|
562
|
+
const fulfils = opts?.fulfils;
|
|
563
|
+
const afterFulfilment: AggregateInvariantFn | undefined =
|
|
564
|
+
fulfils !== undefined
|
|
565
|
+
? (snapshot) => {
|
|
566
|
+
const verdict = fulfils.invariant(snapshot);
|
|
567
|
+
if ("reject" in verdict) return { reject: `${id}: ${verdict.reject}` };
|
|
568
|
+
return userInvariant !== undefined ? userInvariant(snapshot) : { accept: true };
|
|
569
|
+
}
|
|
570
|
+
: userInvariant;
|
|
531
571
|
const composedInvariant: AggregateInvariantFn | undefined =
|
|
532
572
|
fromBirthFields.length > 0
|
|
533
573
|
? (snapshot) => {
|
|
@@ -537,9 +577,9 @@ export function aggregate<const Id extends string, F extends Record<string, Fiel
|
|
|
537
577
|
return { reject: `${id}.${name} is fromBirth() — every ${id} must carry it from creation (the plan left it unset)` };
|
|
538
578
|
}
|
|
539
579
|
}
|
|
540
|
-
return
|
|
580
|
+
return afterFulfilment !== undefined ? afterFulfilment(snapshot) : { accept: true };
|
|
541
581
|
}
|
|
542
|
-
:
|
|
582
|
+
: afterFulfilment;
|
|
543
583
|
|
|
544
584
|
// The audience-free DATA shape; the audience is attached below (from the legacy `{visibility}` opt or a
|
|
545
585
|
// fluent `.public()`/`.readableBy()`/`.owned()` call).
|
|
@@ -548,6 +588,7 @@ export function aggregate<const Id extends string, F extends Record<string, Fiel
|
|
|
548
588
|
fields: stored as StoredFields<F>,
|
|
549
589
|
hasMany: hasMany as HasManyFields<F>,
|
|
550
590
|
__isAggregateHandle: true as const,
|
|
591
|
+
...(opts?.fulfils !== undefined ? { fulfilment: opts.fulfils } : {}),
|
|
551
592
|
...(composedInvariant !== undefined
|
|
552
593
|
? { hasInvariant: true as const, invariant: composedInvariant }
|
|
553
594
|
: {}),
|
|
@@ -34,7 +34,10 @@ export function isCapabilityDecl(v: unknown): v is Record<string, unknown> {
|
|
|
34
34
|
);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
// `relations` carries the receipt fence's relation SCHEMA. It MUST expand with the bundle:
|
|
38
|
+
// `.requires(...)` is inert in a law that declares no relation schema, so omitting it would
|
|
39
|
+
// leave every capability looking fenced while enforcing nothing.
|
|
40
|
+
const PIECES = ["aggregate", "obligation", "complete", "fail", "block", "deadLetter", "tasks", "relations", "grantProvider", "revokeProvider"] as const;
|
|
38
41
|
|
|
39
42
|
/**
|
|
40
43
|
* Expand every `capability()` bundle in an exports bag into discoverable
|
package/src/codegen_dart.ts
CHANGED
|
@@ -3411,6 +3411,146 @@ function parseReadModelField(s: AggFieldSpec, aggregate: string): string {
|
|
|
3411
3411
|
* `{type,id,data}` row location) plus a DSL-named typed field per DSL
|
|
3412
3412
|
* field declaration, and a [fromProjected] factory that reads the read engine's
|
|
3413
3413
|
* already-decoded projected `data` shape. */
|
|
3414
|
+
/**
|
|
3415
|
+
* THE SEALED COMPLETENESS TYPE (`fulfilment.ts`) — the tier's guarantee, made a TYPE guarantee.
|
|
3416
|
+
*
|
|
3417
|
+
* Declaring completeness in law fixed the domain dev's problem and, on its own, MOVED the app
|
|
3418
|
+
* dev's: a flat read model hands them `tier: String?` plus nullable guaranteed fields, so the UI
|
|
3419
|
+
* writes exactly the scattered conditional tiers were meant to dissolve — with more moving parts
|
|
3420
|
+
* than before.
|
|
3421
|
+
*
|
|
3422
|
+
* So each tier becomes a VARIANT whose guaranteed fields are NON-NULLABLE, and the app switches
|
|
3423
|
+
* exhaustively:
|
|
3424
|
+
*
|
|
3425
|
+
* ```dart
|
|
3426
|
+
* switch (site.siteFulfilment) {
|
|
3427
|
+
* SiteFulfilmentResolved(:final postalAddress) => Text(postalAddress.city),
|
|
3428
|
+
* SiteFulfilmentPinned() => const Text('Resolving…'),
|
|
3429
|
+
* SiteFulfilmentBlocked(:final reason) => Text(reason),
|
|
3430
|
+
* SiteFulfilmentUnfulfilled() => const Text('No location'),
|
|
3431
|
+
* }
|
|
3432
|
+
* ```
|
|
3433
|
+
*
|
|
3434
|
+
* No null checks, and adding a tier later fails COMPILATION until every switch handles it — the
|
|
3435
|
+
* same LOUD REQUIRED discipline the DSL applies, reaching the UI.
|
|
3436
|
+
*
|
|
3437
|
+
* This is only expressible because the tier is LAW-STATE. Were completeness inferred from which
|
|
3438
|
+
* fields happen to be null, codegen could not know which combinations are legal and could type
|
|
3439
|
+
* nothing non-nullable.
|
|
3440
|
+
*/
|
|
3441
|
+
function emitFulfilmentSealed(
|
|
3442
|
+
agg: AggregateHandle,
|
|
3443
|
+
specs: AggFieldSpec[],
|
|
3444
|
+
): { code: string; consumed: Set<string>; accessorType: string; accessorName: string } | null {
|
|
3445
|
+
const decl = (agg as { fulfilment?: {
|
|
3446
|
+
name: string;
|
|
3447
|
+
tiers: readonly string[];
|
|
3448
|
+
blockedField: string;
|
|
3449
|
+
guaranteesAt(tier: string): readonly string[];
|
|
3450
|
+
} }).fulfilment;
|
|
3451
|
+
if (decl === undefined) return null;
|
|
3452
|
+
|
|
3453
|
+
const base = `${dartNameStem(agg.id)}${cap(decl.name)}`;
|
|
3454
|
+
const byName = new Map(specs.map((s) => [s.name, s]));
|
|
3455
|
+
// A guarantee names a field path; the sealed variant carries its LEAF. A dotted path's parent
|
|
3456
|
+
// (a value object holding more than the guarantee) stays on the flat model — only a top-level
|
|
3457
|
+
// guarantee is fully absorbed.
|
|
3458
|
+
const leafOf = (path: string) => path.split(".").pop()!;
|
|
3459
|
+
const consumed = new Set<string>([decl.name, decl.blockedField]);
|
|
3460
|
+
for (const tier of decl.tiers) {
|
|
3461
|
+
for (const path of decl.guaranteesAt(tier)) {
|
|
3462
|
+
if (!path.includes(".") && byName.has(path)) consumed.add(path);
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
|
|
3466
|
+
const variantFields = (tier: string): string[] =>
|
|
3467
|
+
decl.guaranteesAt(tier).map((path) => {
|
|
3468
|
+
const spec = byName.get(path);
|
|
3469
|
+
const name = dartMemberName(leafOf(path));
|
|
3470
|
+
// NON-NULLABLE: the tier is exactly the claim that this is present.
|
|
3471
|
+
const type = spec !== undefined ? stripNullable(readModelDartType(spec)) : "Object";
|
|
3472
|
+
return ` final ${type} ${name};`;
|
|
3473
|
+
});
|
|
3474
|
+
const variantCtor = (tier: string): string => {
|
|
3475
|
+
const params = decl
|
|
3476
|
+
.guaranteesAt(tier)
|
|
3477
|
+
.map((path) => `required this.${dartMemberName(leafOf(path))},`);
|
|
3478
|
+
return params.length === 0
|
|
3479
|
+
? ` const ${base}${cap(tier)}();`
|
|
3480
|
+
: [` const ${base}${cap(tier)}({`, ...params.map((p) => ` ${p}`), ` });`].join("\n");
|
|
3481
|
+
};
|
|
3482
|
+
|
|
3483
|
+
const code = [
|
|
3484
|
+
`/// How complete this ${dartNameStem(agg.id)}'s '${decl.name}' is — a CLAIM the law enforces.`,
|
|
3485
|
+
`/// Switch exhaustively; each variant's fields are non-nullable because the tier guarantees them.`,
|
|
3486
|
+
`sealed class ${base} {`,
|
|
3487
|
+
` const ${base}();`,
|
|
3488
|
+
`}`,
|
|
3489
|
+
``,
|
|
3490
|
+
`/// No tier claimed yet: the record guarantees nothing, which is a legal state.`,
|
|
3491
|
+
`final class ${base}Unfulfilled extends ${base} {`,
|
|
3492
|
+
` const ${base}Unfulfilled();`,
|
|
3493
|
+
`}`,
|
|
3494
|
+
``,
|
|
3495
|
+
...decl.tiers.flatMap((tier) => [
|
|
3496
|
+
`/// Tier '${tier}' — guarantees ${decl.guaranteesAt(tier).map((g) => `'${g}'`).join(", ")}.`,
|
|
3497
|
+
`final class ${base}${cap(tier)} extends ${base} {`,
|
|
3498
|
+
...variantFields(tier),
|
|
3499
|
+
variantCtor(tier),
|
|
3500
|
+
`}`,
|
|
3501
|
+
``,
|
|
3502
|
+
]),
|
|
3503
|
+
`/// Terminally unfulfillable — the climb will never happen, and [reason] says why. Distinct`,
|
|
3504
|
+
`/// from "still resolving", so a progress indicator cannot lie forever.`,
|
|
3505
|
+
`final class ${base}Blocked extends ${base} {`,
|
|
3506
|
+
` final String reason;`,
|
|
3507
|
+
` const ${base}Blocked({required this.reason});`,
|
|
3508
|
+
`}`,
|
|
3509
|
+
``,
|
|
3510
|
+
`/// Decode the tier from a projected row. The projection serves PARTIAL folds, so a row may`,
|
|
3511
|
+
`/// name a tier before every guaranteed field has folded; that decodes as the highest tier`,
|
|
3512
|
+
`/// whose guarantees are actually present, never a throw and never a fabricated value.`,
|
|
3513
|
+
`${base} _decode${base}(Map<String, dynamic> data) {`,
|
|
3514
|
+
` final blocked = readStrOrNull(data, '${decl.blockedField}');`,
|
|
3515
|
+
` if (blocked != null && blocked.trim().isNotEmpty) {`,
|
|
3516
|
+
` return ${base}Blocked(reason: blocked);`,
|
|
3517
|
+
` }`,
|
|
3518
|
+
` final tier = readStrOrNull(data, '${decl.name}');`,
|
|
3519
|
+
...[...decl.tiers].reverse().flatMap((tier) => {
|
|
3520
|
+
const guards = decl
|
|
3521
|
+
.guaranteesAt(tier)
|
|
3522
|
+
.map((path) => `${dartMemberName(leafOf(path))} != null`);
|
|
3523
|
+
const reads = decl.guaranteesAt(tier).map((path) => {
|
|
3524
|
+
const spec = byName.get(path);
|
|
3525
|
+
const name = dartMemberName(leafOf(path));
|
|
3526
|
+
const type = spec !== undefined ? stripNullable(readModelDartType(spec)) : "Object";
|
|
3527
|
+
// The guaranteed fields are optional on the aggregate (absent at lower tiers), so the
|
|
3528
|
+
// ordinary read-model parse already yields a nullable value.
|
|
3529
|
+
return ` final ${name} = ${
|
|
3530
|
+
spec !== undefined
|
|
3531
|
+
? parseReadModelField(spec, agg.id)
|
|
3532
|
+
: `data['${leafOf(path)}'] as ${type}?`
|
|
3533
|
+
};`;
|
|
3534
|
+
});
|
|
3535
|
+
return [
|
|
3536
|
+
` if (tier == '${tier}') {`,
|
|
3537
|
+
...reads,
|
|
3538
|
+
` if (${guards.join(" && ")}) {`,
|
|
3539
|
+
` return ${base}${cap(tier)}(${decl
|
|
3540
|
+
.guaranteesAt(tier)
|
|
3541
|
+
.map((p) => `${dartMemberName(leafOf(p))}: ${dartMemberName(leafOf(p))}`)
|
|
3542
|
+
.join(", ")});`,
|
|
3543
|
+
` }`,
|
|
3544
|
+
` }`,
|
|
3545
|
+
];
|
|
3546
|
+
}),
|
|
3547
|
+
` return const ${base}Unfulfilled();`,
|
|
3548
|
+
`}`,
|
|
3549
|
+
``,
|
|
3550
|
+
].join("\n");
|
|
3551
|
+
return { code, consumed, accessorType: base, accessorName: decl.name };
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3414
3554
|
function emitReadModel(
|
|
3415
3555
|
agg: AggregateHandle,
|
|
3416
3556
|
enumDartTypes: Map<string, string>,
|
|
@@ -3422,13 +3562,20 @@ function emitReadModel(
|
|
|
3422
3562
|
): string {
|
|
3423
3563
|
const className = readModelClassName(agg.id);
|
|
3424
3564
|
const wireType = agg.id;
|
|
3425
|
-
const
|
|
3565
|
+
const allSpecs = aggregateFieldSpecs(
|
|
3426
3566
|
agg,
|
|
3427
3567
|
enumDartTypes,
|
|
3428
3568
|
refIdClassForAggregate,
|
|
3429
3569
|
ownIdFields,
|
|
3430
3570
|
aggregateWireTypeForAggregate,
|
|
3431
3571
|
);
|
|
3572
|
+
// THE SEALED COMPLETENESS TYPE REPLACES its fields on the flat model. Emitting both would
|
|
3573
|
+
// leave the nullable ones in reach, devs would use the familiar shape, and the guarantee
|
|
3574
|
+
// would buy nothing — the abstraction has to be the only surface to be worth having.
|
|
3575
|
+
const sealedFulfilment = emitFulfilmentSealed(agg, allSpecs);
|
|
3576
|
+
const specs = sealedFulfilment === null
|
|
3577
|
+
? allSpecs
|
|
3578
|
+
: allSpecs.filter((s) => !sealedFulfilment.consumed.has(s.name));
|
|
3432
3579
|
|
|
3433
3580
|
// DERIVED/COMBINED read fields are engine-projected JSON leaves. Their value schema is
|
|
3434
3581
|
// declared in the DSL via `.returns(z...)`, so the Dart surface is typed instead of
|
|
@@ -3443,6 +3590,13 @@ function emitReadModel(
|
|
|
3443
3590
|
` /// The canonical workspace-qualified identity of this ${concreteClassName(agg.id)}.`,
|
|
3444
3591
|
` /// Pass this exact value to generated actions, find and watch methods.`,
|
|
3445
3592
|
` final NomosRef<${concreteClassName(agg.id)}> ref;`,
|
|
3593
|
+
...(sealedFulfilment !== null
|
|
3594
|
+
? [
|
|
3595
|
+
` /// How complete this record is. Switch exhaustively — each variant's fields are`,
|
|
3596
|
+
` /// non-nullable because the tier GUARANTEES them, so there is nothing to null-check.`,
|
|
3597
|
+
` final ${sealedFulfilment.accessorType} ${dartMemberName(sealedFulfilment.accessorName)};`,
|
|
3598
|
+
]
|
|
3599
|
+
: []),
|
|
3446
3600
|
...specs.flatMap((s) => {
|
|
3447
3601
|
const doc = s.nullReason !== undefined ? [` /// null semantic: ${s.nullReason}`] : [];
|
|
3448
3602
|
const temporalGuidance = s.temporalEnumIds === undefined
|
|
@@ -3478,6 +3632,13 @@ function emitReadModel(
|
|
|
3478
3632
|
].join("\n");
|
|
3479
3633
|
const ctorParams = [
|
|
3480
3634
|
` required this.ref,`,
|
|
3635
|
+
...(sealedFulfilment !== null
|
|
3636
|
+
? [
|
|
3637
|
+
// Defaults to Unfulfilled: the projection serves PARTIAL folds, and "nothing claimed
|
|
3638
|
+
// yet" is the honest reading of a row whose tier has not folded.
|
|
3639
|
+
` this.${dartMemberName(sealedFulfilment.accessorName)} = const ${sealedFulfilment.accessorType}Unfulfilled(),`,
|
|
3640
|
+
]
|
|
3641
|
+
: []),
|
|
3481
3642
|
...specs.map((s) =>
|
|
3482
3643
|
// A required field is `required`; an optional scalar, a collection, OR a
|
|
3483
3644
|
// permissive json leaf (already nullable `Object?`) takes its default/null.
|
|
@@ -3523,6 +3684,9 @@ function emitReadModel(
|
|
|
3523
3684
|
|
|
3524
3685
|
const parseLines = [
|
|
3525
3686
|
` ref: NmsInternalNomosRefs.create<${concreteClassName(agg.id)}>(workspace: _location, type: _kind.isEmpty ? '${aggregateWireTypeForAggregate(agg.id)}' : _kind, id: id),`,
|
|
3687
|
+
...(sealedFulfilment !== null
|
|
3688
|
+
? [` ${dartMemberName(sealedFulfilment.accessorName)}: _decode${sealedFulfilment.accessorType}(data),`]
|
|
3689
|
+
: []),
|
|
3526
3690
|
...specs.map((s) =>
|
|
3527
3691
|
s.surfaced === true
|
|
3528
3692
|
? ` ${s.name}: SurfacedValue.decode<${readModelDartType(s)}>(data, '${s.wireName}', ${slotDecodeCallback(s, "readModel")}),`
|
|
@@ -3543,6 +3707,7 @@ function emitReadModel(
|
|
|
3543
3707
|
].join("\n");
|
|
3544
3708
|
|
|
3545
3709
|
return [
|
|
3710
|
+
...(sealedFulfilment !== null ? [sealedFulfilment.code] : []),
|
|
3546
3711
|
`/// Generated read model for '${agg.id}'.`,
|
|
3547
3712
|
`///`,
|
|
3548
3713
|
`/// Deserialized via [${className}.fromProjected] from the Rust read engine's already-`,
|
|
@@ -3590,7 +3755,10 @@ function emitDirective(
|
|
|
3590
3755
|
const foreignReferenceInputs = dartForeignReferenceInputs(d, aggregates);
|
|
3591
3756
|
const foreignReferenceIds = new Set(foreignReferenceInputs.map((input) => input.idField));
|
|
3592
3757
|
const principalField = d.businessPrincipalField;
|
|
3593
|
-
|
|
3758
|
+
// targetBinding is declared on AnyDirective — honour it everywhere (client method,
|
|
3759
|
+
// payload class, application surface) or the three emitters disagree and the
|
|
3760
|
+
// generated Dart cannot compile.
|
|
3761
|
+
const targetBinding = (d as { targetBinding?: import("./directive.js").DirectiveTargetBinding }).targetBinding;
|
|
3594
3762
|
const targetType = targetBinding === undefined
|
|
3595
3763
|
? undefined
|
|
3596
3764
|
: `NomosRef<${concreteClassName(targetBinding.aggregateId)}>`;
|
|
@@ -594,7 +594,10 @@ export function generateDartBridgeClient(
|
|
|
594
594
|
}
|
|
595
595
|
for (const d of dirs) {
|
|
596
596
|
const payloadClass = dartActionInputClassName(d);
|
|
597
|
-
|
|
597
|
+
// targetBinding is declared on AnyDirective (directive.ts) — honour it for EVERY
|
|
598
|
+
// directive, exactly as the application surface does (a split here emits a 2-arg
|
|
599
|
+
// application call against a 1-arg client method: generated Dart that cannot compile).
|
|
600
|
+
const targetBinding = (d as { targetBinding?: import("./directive.js").DirectiveTargetBinding }).targetBinding;
|
|
598
601
|
const targetType = targetBinding === undefined
|
|
599
602
|
? undefined
|
|
600
603
|
: `NomosRef<${concreteClassName(targetBinding.aggregateId)}>`;
|
|
@@ -28,9 +28,10 @@
|
|
|
28
28
|
* 1. a typed `XObligation` — the Obligation params (envelope provenance `requestId` /
|
|
29
29
|
* `requestedBy` / `requestedAt` ⊕ the tenant params from the DSL), with a
|
|
30
30
|
* `fromJson` decoding the incoming Obligation intent's payload JSON;
|
|
31
|
-
* 2. a typed `XResult` — the success payload the handler returns
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* 2. a typed `XResult` — the success payload the handler returns: the capability's DECLARED
|
|
32
|
+
* receipt fields as TYPED members (they travel to the Receipt as typed USDA values, the
|
|
33
|
+
* mirror of the Obligation's request params), plus an optional `resultRef` naming a
|
|
34
|
+
* produced ARTIFACT. Implements `TaskResult.toReceiptFields()`/`toResultRef()`;
|
|
34
35
|
* 3. typed failure/blocked/DLQ outcomes;
|
|
35
36
|
* 4. a handler typedef `XHandler = TaskOutcome<XResult> Function(XObligation)`;
|
|
36
37
|
* 5. a generated `XProvider` dispatcher (`implements TaskProvider`): given the
|
|
@@ -77,8 +78,16 @@ export interface ImpureCapabilityDecl {
|
|
|
77
78
|
readonly aggregate: { readonly id: string };
|
|
78
79
|
/** The Obligation directive: `.creates`, payload = envelope ⊕ tenant params. */
|
|
79
80
|
readonly obligation: { readonly id: string; readonly payloadSchema: z.ZodTypeAny };
|
|
80
|
-
/**
|
|
81
|
-
|
|
81
|
+
/**
|
|
82
|
+
* The COMPLETE (success) Receipt directive: payload = the complete envelope
|
|
83
|
+
* `{requestId, completedAt}` ⊕ the capability's DECLARED receipt fields (typed USDA values,
|
|
84
|
+
* symmetrical with the obligation's params) ⊕ an optional `resultRef` artifact address.
|
|
85
|
+
*
|
|
86
|
+
* The schema is exposed for the same reason the obligation's is: so the provider SDK emits
|
|
87
|
+
* the receipt as TYPED properties. Without it the generated contract could only offer an
|
|
88
|
+
* untyped bag, which is how receipt values ended up stringly in the first place.
|
|
89
|
+
*/
|
|
90
|
+
readonly complete: { readonly id: string; readonly payloadSchema?: z.ZodTypeAny };
|
|
82
91
|
/** The FAIL (failure) Receipt directive: payload = {requestId, failureReason, failedAt}. */
|
|
83
92
|
readonly fail: { readonly id: string };
|
|
84
93
|
/** The BLOCKED Receipt directive: payload = {requestId, blockedReason, blockedAt, nextAction}. */
|
|
@@ -98,6 +107,11 @@ const OBLIGATION_ENVELOPE_FIELDS = new Set([
|
|
|
98
107
|
"deadlineAt",
|
|
99
108
|
]);
|
|
100
109
|
|
|
110
|
+
/** The framework COMPLETE-envelope field names. Everything else on the complete payload is a
|
|
111
|
+
* DECLARED receipt field the provider must supply, so the generated result type can type them
|
|
112
|
+
* individually instead of offering an untyped bag. `resultRef` is the artifact address. */
|
|
113
|
+
export const COMPLETE_ENVELOPE_FIELDS = new Set(["requestId", "completedAt", "resultRef"]);
|
|
114
|
+
|
|
101
115
|
/** A stable PascalCase base for a task's generated class family, derived from its Obligation
|
|
102
116
|
* directive id with a leading `create`/`oblige` verb stripped (so `createTranscribeVoiceNote`
|
|
103
117
|
* → `TranscribeVoiceNote`). Pure + deterministic. */
|
|
@@ -257,23 +271,63 @@ function emitObligationClass(
|
|
|
257
271
|
}
|
|
258
272
|
|
|
259
273
|
/** Emit the typed `XResult` (success) + `XFailure` classes the handler returns. */
|
|
260
|
-
function emitResultClasses(
|
|
274
|
+
function emitResultClasses(
|
|
275
|
+
base: string,
|
|
276
|
+
completeDirectiveId: string,
|
|
277
|
+
receiptFields: PayloadFieldSpec[] = [],
|
|
278
|
+
): string {
|
|
261
279
|
const resultCls = `${base}Result`;
|
|
262
280
|
const failureCls = `${base}Failure`;
|
|
281
|
+
// The DECLARED receipt fields become typed constructor params, so a provider states its
|
|
282
|
+
// result in the capability's own vocabulary. `resultRef` stays for a produced ARTIFACT and is
|
|
283
|
+
// optional whenever the capability says what it returns through declared fields.
|
|
284
|
+
const hasReceipt = receiptFields.length > 0;
|
|
285
|
+
const receiptDecls = receiptFields.map(
|
|
286
|
+
(s) => ` final ${s.dartType}${s.optional ? "?" : ""} ${s.name};`,
|
|
287
|
+
);
|
|
288
|
+
const ctorParams = [
|
|
289
|
+
...receiptFields.map((s) => (s.optional ? ` this.${s.name},` : ` required this.${s.name},`)),
|
|
290
|
+
hasReceipt ? ` this.resultRef,` : ` required this.resultRef,`,
|
|
291
|
+
];
|
|
292
|
+
const receiptEntries = receiptFields.map(
|
|
293
|
+
(s) =>
|
|
294
|
+
s.optional
|
|
295
|
+
? ` if (${s.name} != null) '${s.name}': ${s.name},`
|
|
296
|
+
: ` '${s.name}': ${s.name},`,
|
|
297
|
+
);
|
|
263
298
|
return [
|
|
264
|
-
`/// The typed SUCCESS RESULT the \`${base}\` handler returns
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
299
|
+
`/// The typed SUCCESS RESULT the \`${base}\` handler returns.`,
|
|
300
|
+
...(hasReceipt
|
|
301
|
+
? [
|
|
302
|
+
`///`,
|
|
303
|
+
`/// The capability's DECLARED receipt fields are typed members here and travel to the`,
|
|
304
|
+
`/// \`${completeDirectiveId}\` Receipt as typed USDA values — the mirror of the`,
|
|
305
|
+
`/// Obligation's request params. [resultRef] is optional and means only what its name`,
|
|
306
|
+
`/// says: the address of a produced ARTIFACT.`,
|
|
307
|
+
]
|
|
308
|
+
: [
|
|
309
|
+
`///`,
|
|
310
|
+
`/// This capability declares no receipt fields, so it reports through [resultRef] — the`,
|
|
311
|
+
`/// content-addressed address of what it produced. Declaring receipt fields is how a`,
|
|
312
|
+
`/// capability says something structured.`,
|
|
313
|
+
]),
|
|
269
314
|
`class ${resultCls} implements TaskResult {`,
|
|
270
|
-
|
|
271
|
-
` ///
|
|
272
|
-
` final String resultRef;`,
|
|
273
|
-
` const ${resultCls}({
|
|
315
|
+
...receiptDecls,
|
|
316
|
+
` /// The content-addressed ARTIFACT the side-effect produced (a blob ref / url).`,
|
|
317
|
+
` final String${hasReceipt ? "?" : ""} resultRef;`,
|
|
318
|
+
` const ${resultCls}({`,
|
|
319
|
+
...ctorParams,
|
|
320
|
+
` });`,
|
|
321
|
+
``,
|
|
322
|
+
` @override`,
|
|
323
|
+
` String? toResultRef() => resultRef;`,
|
|
274
324
|
``,
|
|
325
|
+
` /// The declared receipt values, keyed by their law field names — spread onto the Receipt`,
|
|
326
|
+
` /// payload beside \`{requestId, completedAt}\`.`,
|
|
275
327
|
` @override`,
|
|
276
|
-
` String
|
|
328
|
+
` Map<String, Object?> toReceiptFields() => {`,
|
|
329
|
+
...receiptEntries,
|
|
330
|
+
` };`,
|
|
277
331
|
`}`,
|
|
278
332
|
``,
|
|
279
333
|
`/// The typed FAILURE the \`${base}\` handler returns — the recorded reason the side-effect`,
|
|
@@ -290,6 +344,7 @@ function emitResultClasses(base: string, completeDirectiveId: string): string {
|
|
|
290
344
|
function emitProviderClass(
|
|
291
345
|
task: ImpureCapabilityDecl,
|
|
292
346
|
base: string,
|
|
347
|
+
receiptFields: PayloadFieldSpec[] = [],
|
|
293
348
|
): string {
|
|
294
349
|
const obligationCls = `${base}Obligation`;
|
|
295
350
|
const resultCls = `${base}Result`;
|
|
@@ -347,6 +402,7 @@ function emitProviderClass(
|
|
|
347
402
|
` return switch (outcome) {`,
|
|
348
403
|
` TaskSuccess<${resultCls}>(:final result) => ${completeCls}(`,
|
|
349
404
|
` requestId: obligation.requestId,`,
|
|
405
|
+
...receiptFields.map((f) => ` ${f.name}: result.${f.name},`),
|
|
350
406
|
` resultRef: result.toResultRef(),`,
|
|
351
407
|
` completedAt: now(),`,
|
|
352
408
|
` ),`,
|
|
@@ -384,13 +440,20 @@ export function emitProviderTask(
|
|
|
384
440
|
): string {
|
|
385
441
|
const base = taskBaseName(task.obligation.id);
|
|
386
442
|
const specs = introspectPayload(task.obligation.payloadSchema, enumDartTypes);
|
|
443
|
+
// The declared receipt fields = the complete payload minus the framework envelope. Typed
|
|
444
|
+
// here so the generated result class states them individually instead of a stringly bag.
|
|
445
|
+
const receiptSpecs = task.complete.payloadSchema
|
|
446
|
+
? introspectPayload(task.complete.payloadSchema, enumDartTypes).filter(
|
|
447
|
+
(s) => !COMPLETE_ENVELOPE_FIELDS.has(s.name),
|
|
448
|
+
)
|
|
449
|
+
: [];
|
|
387
450
|
return [
|
|
388
451
|
`// ── provider SDK: ${base} (impureCapability '${task.aggregate.id}') ──`,
|
|
389
452
|
emitObligationClass(base, task.obligation.id, specs),
|
|
390
453
|
``,
|
|
391
|
-
emitResultClasses(base, task.complete.id),
|
|
454
|
+
emitResultClasses(base, task.complete.id, receiptSpecs),
|
|
392
455
|
``,
|
|
393
|
-
emitProviderClass(task, base),
|
|
456
|
+
emitProviderClass(task, base, receiptSpecs),
|
|
394
457
|
].join("\n");
|
|
395
458
|
}
|
|
396
459
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* export default {
|
|
15
15
|
* name: "guestbook", // → build/guestbook.law.usdz + readable USDA
|
|
16
16
|
* domains: [
|
|
17
|
-
* { key: "guestbook", modules: ["./domains/guestbook.ts"] },
|
|
17
|
+
* { key: "guestbook", features: [{ key: "guestbook", modules: ["./domains/guestbook.ts"] }] },
|
|
18
18
|
* ],
|
|
19
19
|
* };
|
|
20
20
|
*
|
|
@@ -1648,12 +1648,12 @@ const dartTypesImportFor = (generatedSource: string): string => {
|
|
|
1648
1648
|
* just the floor generated code needs — a lagging pin forces tenants into dependency_overrides. */
|
|
1649
1649
|
// 0.3.0: SurfacedValue<T>/SurfacedObservation/PendingConflictRow — generated Surfaced-field typing +
|
|
1650
1650
|
// the pendingConflicts read rows.
|
|
1651
|
-
const NOMOS_TYPES_DEP = "^0.8.
|
|
1651
|
+
const NOMOS_TYPES_DEP = "^0.8.5";
|
|
1652
1652
|
// 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
|
|
1653
1653
|
// DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
|
|
1654
1654
|
// REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
|
|
1655
|
-
const NOMOS_CLIENT_DEP = "^0.64.
|
|
1656
|
-
const NOMOS_FLUTTER_DEP = "^0.63.
|
|
1655
|
+
const NOMOS_CLIENT_DEP = "^0.64.16";
|
|
1656
|
+
const NOMOS_FLUTTER_DEP = "^0.63.24";
|
|
1657
1657
|
// DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in dsl/dart/pubspec.yaml
|
|
1658
1658
|
// and dsl/dart_types/pubspec.yaml — a Dart release without this bump fails the suite, so tenants never
|
|
1659
1659
|
// land back in dependency_overrides (the exact co2 report this closes, third occurrence).
|