@githolon/dsl 0.105.8 → 0.105.11

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,12 @@
1
+ ## 0.64.26
2
+
3
+ - Restaged with the current kernel candidate: regenerated vendored assets (kernel release identity is baked into them). Published pub.dev archives are immutable, so regenerated bytes ride their own version.
4
+
5
+ ## 0.64.25
6
+
7
+ - Rebuild the vendored Dart runner from `@githolon/client` 0.105.8. Web-client source is an input to
8
+ this immutable pub.dev archive, so the Dart package now advances whenever that source changes.
9
+
1
10
  ## 0.64.24
2
11
 
3
12
  - Restaged with the current kernel candidate: regenerated vendored assets (kernel release identity is baked into them). Published pub.dev archives are immutable, so regenerated bytes ride their own version.
@@ -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: "08719339b0ecfb39b75543bf40422e84832153cc463b00283c0e84f6e38ce359", version: "kernel-08719339", features: ["compoundKeys", "capturedReads", "invariantGate", "surfacedMerge", "fieldProvenance", "nullSemantics", "attestedReads", "conflictSnapshots", "ownedAudience", "temporalEnum"], protocol: 1 }, testMint: { hash: "ab5c45c029a57b39711a559d803ed7d23e8eb94c11bbba652df7f8b3abd3c6df", version: "kernel-ab5c45c0", features: ["compoundKeys", "capturedReads", "invariantGate", "surfacedMerge", "fieldProvenance", "nullSemantics", "attestedReads", "conflictSnapshots", "ownedAudience", "temporalEnum"], protocol: 1 } };
4
+ var define_NOMOS_KERNEL_RELEASE_default = { production: { hash: "7602d5c014cbf79a1069ff0f542b7b5dd3dde6d4920f44e56710bbe23ee3257d", version: "kernel-7602d5c0", features: ["compoundKeys", "capturedReads", "invariantGate", "surfacedMerge", "fieldProvenance", "nullSemantics", "attestedReads", "conflictSnapshots", "ownedAudience", "temporalEnum"], protocol: 1 }, testMint: { hash: "6e5e6d2f655c0a951e8a04a2a4334131cc7237aa3ff8605d575c50598c2ee803", version: "kernel-6e5e6d2f", 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";
@@ -11103,7 +11103,7 @@ ${String(commit).toLowerCase()}`;
11103
11103
  }));
11104
11104
  await offerDirective(domain, OWNED_RECOVERY_COMPLETE_DIRECTIVE, {
11105
11105
  requestId,
11106
- resultRef: JSON.stringify({ ownedCustody: String(prepared.ownedCustody || "") }),
11106
+ ownedCustody: String(prepared.ownedCustody || ""),
11107
11107
  completedAt: Date.now()
11108
11108
  }, { domainHash, actor: connectionPrincipal });
11109
11109
  }
@@ -11126,21 +11126,17 @@ ${String(commit).toLowerCase()}`;
11126
11126
  let orders = await ownedRecoveryOrdersRaw(recoveryPrincipal);
11127
11127
  let order = orders.find((row) => row.ownerPrincipal === connectionPrincipal && row.deviceRequest === deviceRequest);
11128
11128
  if (!order) {
11129
- const minted = JSON.parse(call("query", {
11130
- queryBytes: qBytes({ op: "mint", typeTag: "NomosOwnedRecoveryOrder", nowMillis: Date.now() })
11131
- }));
11132
- const requestId2 = String(minted.id || "");
11133
- const now = Date.now();
11134
- await offerDirective(domain, OWNED_RECOVERY_REQUEST_DIRECTIVE, {
11135
- requestId: requestId2,
11136
- requestedBy: connectionPrincipal,
11137
- requestedAt: now,
11138
- scheduledAt: now,
11139
- deadlineAt: now + 30 * 24 * 60 * 60 * 1e3,
11129
+ const accepted = await offerDirective(domain, OWNED_RECOVERY_REQUEST_DIRECTIVE, {
11140
11130
  ownerPrincipal: connectionPrincipal,
11141
11131
  recoveryPrincipal,
11142
11132
  deviceRequest
11143
- }, { domainHash, actor: connectionPrincipal });
11133
+ }, { domainHash, actor: connectionPrincipal, returnCreated: true });
11134
+ const requestId2 = accepted.created?.find(
11135
+ (ref) => ref.type === "NomosOwnedRecoveryOrder"
11136
+ )?.id ?? "";
11137
+ if (!requestId2) {
11138
+ throw new Error("owned recovery request was accepted without its created obligation reference");
11139
+ }
11144
11140
  orders = await ownedRecoveryOrdersRaw(recoveryPrincipal);
11145
11141
  order = orders.find((row) => row.requestId === requestId2) ?? {
11146
11142
  id: requestId2,
@@ -11153,12 +11149,7 @@ ${String(commit).toLowerCase()}`;
11153
11149
  }
11154
11150
  const requestId = String(order.requestId || order.id || "");
11155
11151
  if (order.status === "completed") {
11156
- let receipt = {};
11157
- try {
11158
- receipt = JSON.parse(String(order.resultRef || "{}"));
11159
- } catch {
11160
- }
11161
- const ownedCustody = String(receipt.ownedCustody || "");
11152
+ const ownedCustody = String(order.ownedCustody || "");
11162
11153
  if (!ownedCustody) {
11163
11154
  throw new OwnedRecoveryPendingError("Recovery is still being prepared; Nomos will continue safely.", {
11164
11155
  workspace,
@@ -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: "08719339b0ecfb39b75543bf40422e84832153cc463b00283c0e84f6e38ce359", version: "kernel-08719339", features: ["compoundKeys", "capturedReads", "invariantGate", "surfacedMerge", "fieldProvenance", "nullSemantics", "attestedReads", "conflictSnapshots", "ownedAudience", "temporalEnum"], protocol: 1 }, testMint: { hash: "ab5c45c029a57b39711a559d803ed7d23e8eb94c11bbba652df7f8b3abd3c6df", version: "kernel-ab5c45c0", features: ["compoundKeys", "capturedReads", "invariantGate", "surfacedMerge", "fieldProvenance", "nullSemantics", "attestedReads", "conflictSnapshots", "ownedAudience", "temporalEnum"], protocol: 1 } };
9
+ var define_NOMOS_KERNEL_RELEASE_default = { production: { hash: "7602d5c014cbf79a1069ff0f542b7b5dd3dde6d4920f44e56710bbe23ee3257d", version: "kernel-7602d5c0", features: ["compoundKeys", "capturedReads", "invariantGate", "surfacedMerge", "fieldProvenance", "nullSemantics", "attestedReads", "conflictSnapshots", "ownedAudience", "temporalEnum"], protocol: 1 }, testMint: { hash: "6e5e6d2f655c0a951e8a04a2a4334131cc7237aa3ff8605d575c50598c2ee803", version: "kernel-6e5e6d2f", 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.24
10
+ version: 0.64.26
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.105.8",
3
+ "version": "0.105.11",
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.",
@@ -10134,7 +10134,7 @@ ${String(commit).toLowerCase()}`;
10134
10134
  }));
10135
10135
  await offerDirective(domain, OWNED_RECOVERY_COMPLETE_DIRECTIVE, {
10136
10136
  requestId,
10137
- resultRef: JSON.stringify({ ownedCustody: String(prepared.ownedCustody || "") }),
10137
+ ownedCustody: String(prepared.ownedCustody || ""),
10138
10138
  completedAt: Date.now()
10139
10139
  }, { domainHash, actor: connectionPrincipal });
10140
10140
  }
@@ -10157,21 +10157,17 @@ ${String(commit).toLowerCase()}`;
10157
10157
  let orders = await ownedRecoveryOrdersRaw(recoveryPrincipal);
10158
10158
  let order = orders.find((row) => row.ownerPrincipal === connectionPrincipal && row.deviceRequest === deviceRequest);
10159
10159
  if (!order) {
10160
- const minted = JSON.parse(call("query", {
10161
- queryBytes: qBytes({ op: "mint", typeTag: "NomosOwnedRecoveryOrder", nowMillis: Date.now() })
10162
- }));
10163
- const requestId2 = String(minted.id || "");
10164
- const now = Date.now();
10165
- await offerDirective(domain, OWNED_RECOVERY_REQUEST_DIRECTIVE, {
10166
- requestId: requestId2,
10167
- requestedBy: connectionPrincipal,
10168
- requestedAt: now,
10169
- scheduledAt: now,
10170
- deadlineAt: now + 30 * 24 * 60 * 60 * 1e3,
10160
+ const accepted = await offerDirective(domain, OWNED_RECOVERY_REQUEST_DIRECTIVE, {
10171
10161
  ownerPrincipal: connectionPrincipal,
10172
10162
  recoveryPrincipal,
10173
10163
  deviceRequest
10174
- }, { domainHash, actor: connectionPrincipal });
10164
+ }, { domainHash, actor: connectionPrincipal, returnCreated: true });
10165
+ const requestId2 = accepted.created?.find(
10166
+ (ref) => ref.type === "NomosOwnedRecoveryOrder"
10167
+ )?.id ?? "";
10168
+ if (!requestId2) {
10169
+ throw new Error("owned recovery request was accepted without its created obligation reference");
10170
+ }
10175
10171
  orders = await ownedRecoveryOrdersRaw(recoveryPrincipal);
10176
10172
  order = orders.find((row) => row.requestId === requestId2) ?? {
10177
10173
  id: requestId2,
@@ -10184,12 +10180,7 @@ ${String(commit).toLowerCase()}`;
10184
10180
  }
10185
10181
  const requestId = String(order.requestId || order.id || "");
10186
10182
  if (order.status === "completed") {
10187
- let receipt = {};
10188
- try {
10189
- receipt = JSON.parse(String(order.resultRef || "{}"));
10190
- } catch {
10191
- }
10192
- const ownedCustody = String(receipt.ownedCustody || "");
10183
+ const ownedCustody = String(order.ownedCustody || "");
10193
10184
  if (!ownedCustody) {
10194
10185
  throw new OwnedRecoveryPendingError("Recovery is still being prepared; Nomos will continue safely.", {
10195
10186
  workspace,
@@ -34,6 +34,26 @@ export function isCapabilityDecl(v: unknown): v is Record<string, unknown> {
34
34
  );
35
35
  }
36
36
 
37
+ /**
38
+ * Compiler-only link carried by every lifecycle directive produced by `capability()`.
39
+ * A public business intent may wrap or compose that directive; following this typed link
40
+ * lets every compilation plane retain the declaration without a magic second re-export.
41
+ * A symbol keeps the link out of canonical law/property walks while object spread preserves
42
+ * it through the immutable directive and intent builders.
43
+ */
44
+ export const NOMOS_CAPABILITY_DECLARATIONS = Symbol.for(
45
+ "nomos.capabilityDeclarations",
46
+ );
47
+
48
+ /** Capability declarations directly exported or carried by a wrapped/composed directive. */
49
+ export function capabilityDeclarationsOf(v: unknown): Record<string, unknown>[] {
50
+ if (isCapabilityDecl(v)) return [v];
51
+ if (typeof v !== "object" || v === null) return [];
52
+ const attached = (v as Record<PropertyKey, unknown>)[NOMOS_CAPABILITY_DECLARATIONS];
53
+ if (!Array.isArray(attached)) return [];
54
+ return attached.filter(isCapabilityDecl);
55
+ }
56
+
37
57
  // `relations` carries the receipt fence's relation SCHEMA. It MUST expand with the bundle:
38
58
  // `.requires(...)` is inert in a law that declares no relation schema, so omitting it would
39
59
  // leave every capability looking fenced while enforcing nothing.
@@ -47,11 +67,16 @@ const PIECES = ["aggregate", "obligation", "complete", "fail", "block", "deadLet
47
67
  export function expandCapabilityExports<M extends Record<string, unknown>>(mod: M): M {
48
68
  let out: Record<string, unknown> | null = null;
49
69
  for (const [k, v] of Object.entries(mod)) {
50
- if (!isCapabilityDecl(v)) continue;
51
- out ??= { ...mod };
52
- for (const piece of PIECES) {
53
- const member = (v as Record<string, unknown>)[piece];
54
- if (member !== undefined) out[`${k}$${piece}`] = member;
70
+ const capabilities = capabilityDeclarationsOf(v);
71
+ for (let index = 0; index < capabilities.length; index += 1) {
72
+ const capability = capabilities[index]!;
73
+ out ??= { ...mod };
74
+ const stem = capabilities.length === 1 ? k : `${k}$capability${index}`;
75
+ if (capability !== v) out[`${stem}$declaration`] = capability;
76
+ for (const piece of PIECES) {
77
+ const member = capability[piece];
78
+ if (member !== undefined) out[`${stem}$${piece}`] = member;
79
+ }
55
80
  }
56
81
  }
57
82
  return (out ?? mod) as M;
@@ -25,9 +25,10 @@
25
25
  * daemon / e2e.
26
26
  *
27
27
  * ── What it emits, per `impureCapability` capability (the STANDARD shape, never invented) ──
28
- * 1. a typed `XObligation` — the Obligation params (envelope provenance `requestId` /
29
- * `requestedBy` / `requestedAt` ⊕ the tenant params from the DSL), with a
30
- * `fromJson` decoding the incoming Obligation intent's payload JSON;
28
+ * 1. a typed `XObligation` — the accepted task projection (kernel envelope provenance
29
+ * `requestId` / `requestedBy` / `requestedAt` ⊕ tenant params), with a `fromJson`
30
+ * decoding the accepted Obligation intent. This is intentionally richer than the
31
+ * peer's public request payload, which contains business params only;
31
32
  * 2. a typed `XResult` — the success payload the handler returns: the capability's DECLARED
32
33
  * receipt fields as TYPED members (they travel to the Receipt as typed USDA values, the
33
34
  * mirror of the Obligation's request params), plus an optional `resultRef` naming a
@@ -76,8 +77,10 @@ export interface ImpureCapabilityDecl {
76
77
  readonly tasks?: { readonly id: string };
77
78
  /** The task aggregate handle; `.id` is the wire type (e.g. `TranscribeVoiceNoteAggregate`). */
78
79
  readonly aggregate: { readonly id: string };
79
- /** The Obligation directive: `.creates`, payload = envelope ⊕ tenant params. */
80
+ /** The public Obligation directive: `.creates`, payload = tenant business params only. */
80
81
  readonly obligation: { readonly id: string; readonly payloadSchema: z.ZodTypeAny };
82
+ /** Accepted task view delivered to providers: kernel envelope ⊕ tenant params. */
83
+ readonly providerPayloadSchema: z.ZodTypeAny;
81
84
  /**
82
85
  * The COMPLETE (success) Receipt directive: payload = the complete envelope
83
86
  * `{requestId, completedAt}` ⊕ the capability's DECLARED receipt fields (typed USDA values,
@@ -261,8 +264,8 @@ function emitObligationClass(
261
264
  ` });`,
262
265
  ``,
263
266
  ` /// Decode an Obligation from the already-\`jsonDecode\`d payload map (the EXACT shape the`,
264
- ` /// peer's \`${obligationDirectiveId}\` generated payload encoder produced). Reverse of the peer`,
265
- ` /// encode — types stop here, at the JSON serde.`,
267
+ ` /// accepted task projection). The public request payload intentionally omits the`,
268
+ ` /// framework-owned envelope; types stop here, at the provider serde boundary.`,
266
269
  ` factory ${cls}.fromJson(Map<String, Object?> j) => ${cls}(`,
267
270
  fromJsonAssigns,
268
271
  ` );`,
@@ -439,7 +442,7 @@ export function emitProviderTask(
439
442
  enumDartTypes: Map<string, string>,
440
443
  ): string {
441
444
  const base = taskBaseName(task.obligation.id);
442
- const specs = introspectPayload(task.obligation.payloadSchema, enumDartTypes);
445
+ const specs = introspectPayload(task.providerPayloadSchema, enumDartTypes);
443
446
  // The declared receipt fields = the complete payload minus the framework envelope. Typed
444
447
  // here so the generated result class states them individually instead of a stringly bag.
445
448
  const receiptSpecs = task.complete.payloadSchema
@@ -1660,8 +1660,8 @@ const NOMOS_TYPES_DEP = "^0.8.7";
1660
1660
  // 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
1661
1661
  // DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
1662
1662
  // REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
1663
- const NOMOS_CLIENT_DEP = "^0.64.24";
1664
- const NOMOS_FLUTTER_DEP = "^0.63.46";
1663
+ const NOMOS_CLIENT_DEP = "^0.64.26";
1664
+ const NOMOS_FLUTTER_DEP = "^0.63.49";
1665
1665
  // DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in dsl/dart/pubspec.yaml
1666
1666
  // and dsl/dart_types/pubspec.yaml — a Dart release without this bump fails the suite, so tenants never
1667
1667
  // land back in dependency_overrides (the exact co2 report this closes, third occurrence).
@@ -67,6 +67,8 @@ import {
67
67
  import { key, relationRef, relationType, self, user } from "../relation_schema.js";
68
68
  import { directive } from "../directive.js";
69
69
  import { RelationTuple, removeTuple, writeTuple } from "./relations.js";
70
+ import { idOf } from "../value_object.js";
71
+ import { NOMOS_CAPABILITY_DECLARATIONS } from "../capability_exports.js";
70
72
 
71
73
  /** The shipped CapabilityBinding name grammar (worker + governance law agree). */
72
74
  export const CAPABILITY_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/;
@@ -111,6 +113,24 @@ export function capabilityPascal(name: string): string {
111
113
  /** The zod RAW SHAPE derived from request fields (each Field carries its own zod). */
112
114
  type RequestShape<Req extends Record<string, Field>> = { [K in keyof Req]: Req[K]["zod"] };
113
115
 
116
+ /**
117
+ * Turn a stored field declaration into the equivalent public payload schema without
118
+ * throwing away its nominal identity. A plain `Field.zod` is sufficient for scalar and
119
+ * structured values; an id-stored reference needs `idOf(Target)` so generated callers and
120
+ * the aggregate read model name the same type. Optionality belongs to the field declaration
121
+ * and is preserved here as well.
122
+ */
123
+ function capabilityPayloadFieldSchema(field: Field): z.ZodTypeAny {
124
+ const target = field.kind === "ref"
125
+ && field.nomosRef === undefined
126
+ && field.versionedForeignRef === undefined
127
+ ? field.refAggregateId
128
+ : undefined;
129
+ if (target === undefined) return field.zod;
130
+ const schema = idOf(target);
131
+ return field.isOptional ? schema.optional() : schema;
132
+ }
133
+
114
134
  /**
115
135
  * What `capability()` returns: the full shipped quartet (so every existing
116
136
  * consumer — codegen, lowering, the reactor helpers — works unchanged) plus the
@@ -184,6 +204,17 @@ export function capability<
184
204
  receipt?: Rec;
185
205
  endpoint?: string;
186
206
  declassify?: string;
207
+ /** Authorization required to request the capability. Declare it here so the
208
+ * capability's one obligation object remains identical everywhere it is composed. */
209
+ requestRequires?: {
210
+ relation: string;
211
+ object?: string;
212
+ objectFrom?: { recordType: string; idField: string };
213
+ };
214
+ /** Delay before a provider should attempt accepted work. */
215
+ scheduleAfterMs?: number;
216
+ /** Time from acceptance until the obligation is overdue. Defaults to 24h. */
217
+ deadlineAfterMs?: number;
187
218
  /**
188
219
  * THE ACKNOWLEDGED WAIVER — open this capability's Receipts to any authorised author.
189
220
  *
@@ -235,10 +266,10 @@ export function capability<
235
266
  const taskAggregate = impureCapabilityAggregate(aggregateId, params);
236
267
 
237
268
  const requestShape = Object.fromEntries(
238
- Object.entries(spec.request).map(([k, f]) => [k, f.zod]),
269
+ Object.entries(spec.request).map(([k, f]) => [k, capabilityPayloadFieldSchema(f)]),
239
270
  ) as RequestShape<Req>;
240
271
  const receiptShape = Object.fromEntries(
241
- Object.entries(receipt).map(([k, f]) => [k, f.zod]),
272
+ Object.entries(receipt).map(([k, f]) => [k, capabilityPayloadFieldSchema(f)]),
242
273
  ) as { [K in keyof Rec]: Rec[K]["zod"] };
243
274
  const receiptEntries = Object.entries(receipt) as [string, Field][];
244
275
 
@@ -271,6 +302,12 @@ export function capability<
271
302
  }
272
303
  : {}),
273
304
  capability: name,
305
+ ...(spec.scheduleAfterMs !== undefined
306
+ ? { scheduleAfterMs: spec.scheduleAfterMs }
307
+ : {}),
308
+ ...(spec.deadlineAfterMs !== undefined
309
+ ? { deadlineAfterMs: spec.deadlineAfterMs }
310
+ : {}),
274
311
  });
275
312
 
276
313
  const camel = pascal[0]!.toLowerCase() + pascal.slice(1);
@@ -328,8 +365,19 @@ export function capability<
328
365
  object: capabilityProviderObject(name),
329
366
  }) as D);
330
367
 
331
- return {
368
+ const obligation = spec.requestRequires === undefined
369
+ ? quartet.obligation
370
+ : quartet.obligation.requires(spec.requestRequires.relation, {
371
+ ...(spec.requestRequires.object !== undefined
372
+ ? { object: spec.requestRequires.object }
373
+ : {}),
374
+ ...(spec.requestRequires.objectFrom !== undefined
375
+ ? { objectFrom: spec.requestRequires.objectFrom }
376
+ : {}),
377
+ });
378
+ const declaration = {
332
379
  ...quartet,
380
+ obligation,
333
381
  complete: fenced(quartet.complete),
334
382
  fail: fenced(quartet.fail),
335
383
  block: fenced(quartet.block),
@@ -353,4 +401,24 @@ export function capability<
353
401
  // discovers this bundle's aggregate + quartet + scan query without re-exports.
354
402
  __isCapabilityDecl: true,
355
403
  } as CapabilityDecl<Req, Rec>;
404
+
405
+ // Carry the declaration through any ordinary `intent().performs(...)` or compound
406
+ // `.directs(...).atomically()` wrapper. This is compiler metadata, not law and not a
407
+ // second runtime path: it merely ensures the one declared capability cannot be lost
408
+ // because the tenant sensibly split it into a small file.
409
+ for (const lifecycle of [
410
+ declaration.obligation,
411
+ declaration.complete,
412
+ declaration.fail,
413
+ declaration.block,
414
+ declaration.deadLetter,
415
+ ]) {
416
+ Object.defineProperty(lifecycle, NOMOS_CAPABILITY_DECLARATIONS, {
417
+ value: Object.freeze([declaration]),
418
+ enumerable: true,
419
+ configurable: false,
420
+ writable: false,
421
+ });
422
+ }
423
+ return declaration;
356
424
  }
@@ -20,7 +20,7 @@
20
20
  * an **Obligation → Receipt** pair of ordinary `WireIntent`s with a first-class lifecycle law:
21
21
  *
22
22
  * • **Obligation** — a normal intent whose role is the request: a peer dispatches
23
- * `oblige…(params, status:'requested')`,
23
+ * `oblige…(businessParams)`,
24
24
  * `.creates` a task aggregate keyed by a KERNEL-MINTED obligation id
25
25
  * (`<ObligationType>_<uuidv7>`, via the marker-driven front-door; idempotent on
26
26
  * replay by capture).
@@ -48,8 +48,8 @@
48
48
  * Framework (here): the task LIFECYCLE shape — the `requestId / status / resultRef? /
49
49
  * failureReason? / completedAt? / blockedReason? / nextAction? / deadLetterReason?`
50
50
  * envelope, the `requested|completed|failed|blocked|deadLettered` status
51
- * vocabulary, the deterministic instance-id rule (the task is keyed by its own
52
- * requestId), and the three plan builders that write that envelope. Tenant: the task's
51
+ * vocabulary, kernel-minted task identity, captured author/clock, and the plan builders
52
+ * that write that envelope. Tenant: the task's
53
53
  * NAME (`exportJob`, `thingExport`) and its request PARAMS (which group, which
54
54
  * filters) — the domain dev supplies those via `impureCapability({...})`.
55
55
  *
@@ -67,7 +67,8 @@
67
67
  */
68
68
  import { z } from "zod";
69
69
  import { aggregate, instance, type AggregateHandle } from "../aggregate.js";
70
- import { t, type Field } from "../fields.js";
70
+ import { create, type AggregateRef } from "../authoring.js";
71
+ import { attributionField, t, type Field } from "../fields.js";
71
72
  import { Lww } from "../drivers.js";
72
73
  import { set, type PlannedOp } from "../ops.js";
73
74
  import { directive, type RequirableDirective } from "../directive.js";
@@ -75,6 +76,7 @@ import { executeDirectiveToIntent } from "../wire_encode.js";
75
76
  import type { WireIntent } from "../wire.js";
76
77
  import type { Ports } from "../ctx.js";
77
78
  import { idOf } from "../value_object.js";
79
+ import type { AttributionPrincipal } from "../principal_evidence.js";
78
80
 
79
81
  /**
80
82
  * The impure-capability lifecycle vocabulary — framework-fixed. A task is `requested` when
@@ -101,6 +103,8 @@ export type ImpureCapabilityStatus = (typeof IMPURE_CAPABILITY_STATUSES)[number]
101
103
  * create-only gap; modelled Lww).
102
104
  * - requestedBy Lww — who obligated the task (provenance). Immutable-after-create.
103
105
  * - requestedAt Int — epoch-ms the obligation was authored. Immutable-after-create.
106
+ * - scheduledAt Int — epoch-ms when a provider may start work. Framework-derived.
107
+ * - deadlineAt Int — epoch-ms when accepted work becomes overdue. Framework-derived.
104
108
  * - status Lww enum — `requested` → the reactor flips to a terminal status.
105
109
  * - resultRef Lww string (OPTIONAL) — the content-addressed ARTIFACT the provider
106
110
  * records on completion: a blob ref / download url, and nothing else. It
@@ -127,7 +131,7 @@ export type ImpureCapabilityStatus = (typeof IMPURE_CAPABILITY_STATUSES)[number]
127
131
  */
128
132
  export const IMPURE_CAPABILITY_ENVELOPE = {
129
133
  requestId: t.string().merge(Lww),
130
- requestedBy: t.string().merge(Lww),
134
+ requestedBy: attributionField(t.string().merge(Lww)),
131
135
  requestedAt: t.int().merge(Lww),
132
136
  scheduledAt: t.int().merge(Lww),
133
137
  deadlineAt: t.int().merge(Lww),
@@ -186,21 +190,24 @@ export function impureCapabilityInstanceId(requestId: string): string {
186
190
  return requestId;
187
191
  }
188
192
 
189
- /** The Zod schema for the framework envelope fields the Obligation payload always carries. */
190
- const obligationEnvelopeSchema = z.object({
193
+ /** The receipt-COMPLETE envelope every capability carries, whatever its result shape. */
194
+ const completeEnvelopeSchema = z.object({
191
195
  requestId: z.string(),
196
+ completedAt: z.number().int(),
197
+ });
198
+
199
+ /**
200
+ * What a provider receives after the Obligation has passed the gate and become a task.
201
+ * Deliberately NOT the public request payload: callers supply business params, while the
202
+ * accepted projection adds kernel-owned identity, provenance and lifecycle timing.
203
+ */
204
+ const providerEnvelopeSchema = z.object({
192
205
  requestedBy: z.string(),
193
206
  requestedAt: z.number().int(),
194
207
  scheduledAt: z.number().int(),
195
208
  deadlineAt: z.number().int(),
196
209
  });
197
210
 
198
- /** The receipt-COMPLETE envelope every capability carries, whatever its result shape. */
199
- const completeEnvelopeSchema = z.object({
200
- requestId: z.string(),
201
- completedAt: z.number().int(),
202
- });
203
-
204
211
  /**
205
212
  * The receipt-COMPLETE payload for a capability declaring NO typed receipt fields: the
206
213
  * envelope plus a required content-addressed ref to the produced ARTIFACT (blob ref / url).
@@ -266,7 +273,13 @@ function envelopeInstance(aggregateId: string, requestId: string) {
266
273
  */
267
274
  export function obligationOps(
268
275
  aggregateId: string,
269
- p: z.infer<typeof obligationEnvelopeSchema>,
276
+ p: {
277
+ requestId: string;
278
+ requestedBy: string | AttributionPrincipal;
279
+ requestedAt: number;
280
+ scheduledAt: number;
281
+ deadlineAt: number;
282
+ },
270
283
  paramOps: PlannedOp[] = [],
271
284
  ): PlannedOp[] {
272
285
  const taskInstance = envelopeInstance(aggregateId, p.requestId);
@@ -366,10 +379,11 @@ export function deadLetterOps(
366
379
  * • `obligation` — `.creates` directive (the authored Obligation),
367
380
  * • `complete` — `.mutates` directive (the reactor's success Receipt),
368
381
  * • `fail` — `.mutates` directive (the reactor's failure Receipt),
369
- * plus the deterministic `instanceId` rule. The `obligation` payload is the framework
370
- * obligation-envelope MERGED with the tenant's `paramsSchema`; the `complete` payload is
371
- * SYMMETRICALLY the complete-envelope merged with the tenant's `receiptSchema` (typed USDA
372
- * values, never an encoded blob); `fail`/`block`/`deadLetter` are fixed.
382
+ * plus the deterministic `instanceId` rule. The public `obligation` payload contains ONLY
383
+ * tenant business params. `providerPayloadSchema` describes the accepted task projection
384
+ * the provider receives: kernel-owned envelope plus those params. The `complete` payload is
385
+ * the complete-envelope merged with the tenant's `receiptSchema` (typed USDA values, never
386
+ * an encoded blob); `fail`/`block`/`deadLetter` are fixed.
373
387
  */
374
388
  // Infer from the EXTENDED schema, exactly as ObligationPayload does — a conditional or an
375
389
  // intersection around this turns a `.optional()` receipt field into a REQUIRED key of type
@@ -392,6 +406,8 @@ export interface ImpureCapability<
392
406
  > {
393
407
  readonly aggregate: AggregateHandle<Id, typeof IMPURE_CAPABILITY_ENVELOPE & F>;
394
408
  readonly obligation: RequirableDirective<ObligationPayload<ParamsShape>>;
409
+ /** Typed accepted-task view delivered to a provider (kernel envelope plus business params). */
410
+ readonly providerPayloadSchema: z.ZodTypeAny;
395
411
  readonly complete: RequirableDirective<CompletePayload<ReceiptShape>>;
396
412
  readonly fail: RequirableDirective<z.infer<typeof failSchema>>;
397
413
  readonly block: RequirableDirective<z.infer<typeof blockSchema>>;
@@ -412,13 +428,11 @@ export interface ImpureCapability<
412
428
  }
413
429
 
414
430
  /**
415
- * The inferred Obligation payload type = the framework envelope schema EXTENDED with the
416
- * tenant's param shape — the EXACT inference `obligationEnvelopeSchema.extend(paramsSchema)`
417
- * produces (so the declared `ImpureCapability.obligation` directive type and the value the factory
418
- * builds are the same type, with no brittle hand-written intersection).
431
+ * The inferred public Obligation payload type: tenant business params only. Task identity,
432
+ * provenance and lifecycle timing are kernel-owned and appear on the accepted provider view.
419
433
  */
420
434
  export type ObligationPayload<ParamsShape extends z.ZodRawShape> = z.infer<
421
- ReturnType<typeof obligationEnvelopeSchema.extend<ParamsShape>>
435
+ z.ZodObject<ParamsShape>
422
436
  >;
423
437
 
424
438
  /**
@@ -507,14 +521,19 @@ export function impureCapability<
507
521
  * Omit-when-absent — declaring nothing changes nothing on the wire.
508
522
  */
509
523
  capability?: string;
524
+ /** Delay before the provider should attempt the task. Captured from the
525
+ * accepted offer's clock; callers never author lifecycle timestamps. */
526
+ scheduleAfterMs?: number;
527
+ /** Time from acceptance until the task is overdue. Defaults to 24 hours. */
528
+ deadlineAfterMs?: number;
510
529
  }): ImpureCapability<Id, ParamsShape, F, ReceiptShape> {
511
530
  const taskAggregate = impureCapabilityAggregate(spec.aggregateId, spec.params);
531
+ const obligationPayload = z.object(spec.paramsSchema);
512
532
  const typedRequestId = { requestId: idOf(taskAggregate) };
513
- const obligationPayloadBase = obligationEnvelopeSchema.extend(spec.paramsSchema);
514
- // `idOf()` deliberately has the same parsed value (`string`) as the schema it
515
- // replaces; its extra aggregate identity lives in Zod metadata for lint/codegen.
516
- // Preserve the generic payload type while installing that richer runtime schema.
517
- const obligationPayload = obligationPayloadBase.extend(typedRequestId) as typeof obligationPayloadBase;
533
+ const providerPayloadSchema = providerEnvelopeSchema.extend({
534
+ ...typedRequestId,
535
+ ...spec.paramsSchema,
536
+ });
518
537
  // THE RECEIPT IS TYPED LAW, SYMMETRICAL WITH THE REQUEST. A capability that declares
519
538
  // receipt fields carries them as typed values merged onto the complete envelope, exactly
520
539
  // as the obligation merges its params. `resultRef` then keeps only its honest meaning —
@@ -536,18 +555,28 @@ export function impureCapability<
536
555
  )
537
556
  .creates(taskAggregate)
538
557
  .payload(obligationPayload)
539
- .plan((p) => {
540
- // The validated payload carries BOTH the envelope (required string/int) and the
541
- // tenant params. `obligationOps` writes the envelope; the tenant's `writeParams`
542
- // writes its own fields. Extract the envelope explicitly (it is required on the
543
- // schema, so these are present non-undefined values).
544
- const envelope = obligationEnvelopeSchema.parse(p);
558
+ .plan((p, ctx) => {
559
+ // The caller supplies BUSINESS PARAMS only. Nomos owns the task identity,
560
+ // provenance and lifecycle clock so generated clients cannot accidentally
561
+ // spoof or hand-roll the obligation envelope.
545
562
  const params = p as z.infer<z.ZodObject<ParamsShape>>;
546
- return obligationOps(
547
- spec.aggregateId,
548
- envelope,
549
- spec.writeParams(impureCapabilityInstanceId(envelope.requestId), params),
550
- );
563
+ const task = create(taskAggregate);
564
+ const requestId = task.__id;
565
+ const requestedAt = ctx.clock().physical;
566
+ const scheduledAt = requestedAt + (spec.scheduleAfterMs ?? 0);
567
+ const deadlineAt = requestedAt + (spec.deadlineAfterMs ?? 24 * 60 * 60 * 1000);
568
+ // Narrow to the framework-owned facet while writing its envelope. This keeps every
569
+ // field name and value checked without pretending the tenant's generic param keys are
570
+ // known here; the full task still carries both facets on the wire and in generated reads.
571
+ const envelopeTask = task as unknown as AggregateRef<typeof IMPURE_CAPABILITY_ENVELOPE>;
572
+ envelopeTask
573
+ .set("requestId", requestId)
574
+ .set("requestedBy", ctx.principal().asAttribution())
575
+ .set("requestedAt", requestedAt)
576
+ .set("scheduledAt", scheduledAt)
577
+ .set("deadlineAt", deadlineAt)
578
+ .set("status", "requested");
579
+ return spec.writeParams(impureCapabilityInstanceId(requestId), params);
551
580
  });
552
581
 
553
582
  // The two branches above are distinct object schemas; the declared payload TYPE is the
@@ -588,6 +617,7 @@ export function impureCapability<
588
617
  return {
589
618
  aggregate: taskAggregate,
590
619
  obligation,
620
+ providerPayloadSchema,
591
621
  complete,
592
622
  fail,
593
623
  block,
@@ -119,6 +119,7 @@ const ownedRecoveryLifecycle = impureCapability({
119
119
  // have one typed provider contract; the kernel may then retain the sealed obligation locally until
120
120
  // that principal opens the workspace, rather than inventing an untyped host work description.
121
121
  capability: "nomos.owned-recovery",
122
+ deadlineAfterMs: 30 * 24 * 60 * 60 * 1000,
122
123
  });
123
124
  const hiddenRecoveryLifecycle = {
124
125
  aggregate: hidden(OwnedRecoveryOrder),
package/src/intent.ts CHANGED
@@ -32,6 +32,10 @@ import {
32
32
  } from "./business_birth.js";
33
33
  import type { BusinessBirthDecl } from "./births.js";
34
34
  import type { WorkspaceSharingPolicy } from "./workspace_sharing.js";
35
+ import {
36
+ capabilityDeclarationsOf,
37
+ NOMOS_CAPABILITY_DECLARATIONS,
38
+ } from "./capability_exports.js";
35
39
 
36
40
  const INTENT_BRAND = "__nomos_business_intent_v1" as const;
37
41
  const LANGUAGE_BRAND = "__nomos_application_language_v1" as const;
@@ -498,6 +502,9 @@ class IntentBuilder<I> {
498
502
  const businessBirth = (birthSteps[0]?.directive as BusinessBirthDirective | undefined)?.businessBirth;
499
503
  const bindBusinessBirthPolicy =
500
504
  (birthSteps[0]?.directive as BusinessBirthDirective | undefined)?.[BUSINESS_BIRTH_POLICY_BINDING];
505
+ const carriedCapabilities = [
506
+ ...new Set(this.steps.flatMap((step) => capabilityDeclarationsOf(step.directive))),
507
+ ];
501
508
  return {
502
509
  [INTENT_BRAND]: true,
503
510
  id: this.id,
@@ -550,6 +557,11 @@ class IntentBuilder<I> {
550
557
  publicPayloadSchema: this.contractDeclaration.accepts,
551
558
  },
552
559
  }),
560
+ ...(carriedCapabilities.length === 0
561
+ ? {}
562
+ : {
563
+ [NOMOS_CAPABILITY_DECLARATIONS]: Object.freeze(carriedCapabilities),
564
+ }),
553
565
  };
554
566
  }
555
567
  }