@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 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 can encode into a Receipt's content-addressed
94
- /// `resultRef`. Every generated `…Result` class implements this: it serialises to the
95
- /// single JSON string the framework `complete…` Receipt records as its `resultRef` (the
96
- /// authoritative artifact). The framework's `resultRef` is a JSON STRING leaf
97
- /// (`impure_capability.ts` `completeSchema.resultRef`), so a structured result is JSON-encoded
98
- /// into it and decodes losslessly on the read side.
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
- /// Encode this result to the Receipt's `resultRef` JSON string.
101
- String toResultRef();
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: "8a39dd1041ce56b820ee321eab4d10cb5e53131f216a6ab8f1135799a2cc5f98", version: "kernel-8a39dd10", features: ["compoundKeys", "capturedReads", "invariantGate", "surfacedMerge", "fieldProvenance", "nullSemantics", "attestedReads", "conflictSnapshots", "ownedAudience", "temporalEnum"], protocol: 1 }, testMint: { hash: "4ec42988fffc7dd1c4f5553aec83720a7a9bc3f27eb35da211254e66eac2661c", version: "kernel-4ec42988", features: ["compoundKeys", "capturedReads", "invariantGate", "surfacedMerge", "fieldProvenance", "nullSemantics", "attestedReads", "conflictSnapshots", "ownedAudience", "temporalEnum"], protocol: 1 } };
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: "8a39dd1041ce56b820ee321eab4d10cb5e53131f216a6ab8f1135799a2cc5f98", version: "kernel-8a39dd10", features: ["compoundKeys", "capturedReads", "invariantGate", "surfacedMerge", "fieldProvenance", "nullSemantics", "attestedReads", "conflictSnapshots", "ownedAudience", "temporalEnum"], protocol: 1 }, testMint: { hash: "4ec42988fffc7dd1c4f5553aec83720a7a9bc3f27eb35da211254e66eac2661c", version: "kernel-4ec42988", features: ["compoundKeys", "capturedReads", "invariantGate", "surfacedMerge", "fieldProvenance", "nullSemantics", "attestedReads", "conflictSnapshots", "ownedAudience", "temporalEnum"], protocol: 1 } };
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.15
10
+ version: 0.64.16
11
11
 
12
12
  environment:
13
13
  sdk: ^3.5.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.103.18",
3
+ "version": "0.104.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
@@ -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 userInvariant !== undefined ? userInvariant(snapshot) : { accept: true };
580
+ return afterFulfilment !== undefined ? afterFulfilment(snapshot) : { accept: true };
541
581
  }
542
- : userInvariant;
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
- const PIECES = ["aggregate", "obligation", "complete", "fail", "block", "deadLetter", "tasks"] as const;
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
@@ -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 specs = aggregateFieldSpecs(
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
- const targetBinding = isBusinessIntent(d) ? d.targetBinding : undefined;
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
- const targetBinding = isBusinessIntent(d) ? d.targetBinding : undefined;
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 (`resultRef`, the
32
- * content-addressed Evidence handle — the framework's `completeSchema.resultRef`),
33
- * implementing `TaskResult.toResultRef()`;
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
- /** The COMPLETE (success) Receipt directive: payload = {requestId, resultRef, completedAt}. */
81
- readonly complete: { readonly id: string };
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(base: string, completeDirectiveId: string): string {
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. Carries the content-addressed`,
265
- `/// [resultRef] (the Evidence handle — the framework \`${completeDirectiveId}\` Receipt's`,
266
- `/// authoritative \`resultRef\`). [toResultRef] encodes it for the Receipt; the framework`,
267
- `/// Receipt records it verbatim and a tenant \`completeResult\` may ALSO derive a typed`,
268
- `/// result field from it (e.g. a voice-note's \`transcription\`).`,
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
- ` /// The content-addressed result the side-effect produced (a blob ref / url / text`,
271
- ` /// captured as Evidence) — recorded verbatim as the Receipt's \`resultRef\`.`,
272
- ` final String resultRef;`,
273
- ` const ${resultCls}({required this.resultRef});`,
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 toResultRef() => resultRef;`,
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.4";
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.14";
1656
- const NOMOS_FLUTTER_DEP = "^0.63.22";
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).