@githolon/dsl 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dart/example/nomos_webview_transport.dart +2 -2
- package/dart/lib/nomos_dsl.dart +4 -4
- package/dart/lib/src/{dispatch.dart → offer.dart} +7 -7
- package/dart/lib/src/provider.dart +30 -30
- package/dart/lib/src/subscriptions.dart +3 -3
- package/dart/lib/src/webview_bridge.dart +157 -29
- package/dart/test/webview_bridge_test.dart +193 -50
- package/package.json +5 -3
- package/src/build_package.ts +1 -1
- package/src/capability_exports.ts +2 -2
- package/src/codegen_dart.ts +12 -12
- package/src/codegen_dart_client.ts +20 -7
- package/src/codegen_provider_dart.ts +67 -61
- package/src/codegen_ts.ts +18 -15
- package/src/compile_package_main.ts +13 -16
- package/src/framework/capability.ts +10 -10
- package/src/framework/impure_capability.ts +73 -67
- package/src/framework/sync_lifecycle.ts +3 -3
- package/src/framework/workspaces.ts +7 -0
- package/src/index.ts +1 -1
- package/src/usd.ts +156 -0
- package/src/usd_layers.ts +170 -26
- package/src/workspace_sharding.ts +2 -7
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* framework/impure_capability.ts — the FRAMEWORK
|
|
9
|
+
* framework/impure_capability.ts — the FRAMEWORK Obligation→Receipt mechanism (tenant-agnostic).
|
|
10
10
|
*
|
|
11
11
|
* ── Why this module exists (the framework↔tenant line) ───────────────────────
|
|
12
12
|
* A *provider-side intent* in Nomos 1 was an ephemeral job: dispatch → the
|
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
*
|
|
18
18
|
* Nomos 2's law is "**every authored intent folds SOME aggregate**" (contract §1) —
|
|
19
19
|
* there is no ephemeral job. The faithful shape of an async impure capability is therefore
|
|
20
|
-
* an **
|
|
20
|
+
* an **Obligation → Receipt** pair of ordinary `WireIntent`s with a first-class lifecycle law:
|
|
21
21
|
*
|
|
22
|
-
* • **
|
|
23
|
-
* `
|
|
24
|
-
* `.creates` a task aggregate keyed by a KERNEL-MINTED
|
|
25
|
-
* (`<
|
|
22
|
+
* • **Obligation** — a normal intent whose role is the request: a peer dispatches
|
|
23
|
+
* `oblige…(params, status:'requested')`,
|
|
24
|
+
* `.creates` a task aggregate keyed by a KERNEL-MINTED obligation id
|
|
25
|
+
* (`<ObligationType>_<uuidv7>`, via the marker-driven front-door; idempotent on
|
|
26
26
|
* replay by capture).
|
|
27
27
|
* • **(impure work)** — a capability provider watches `status:'requested'` rows, performs
|
|
28
28
|
* the side-effect (rendering / upload / multi-target provider work — a host/report
|
|
@@ -33,14 +33,14 @@
|
|
|
33
33
|
* Some domains also expose explicit blocked/DLQ receipts using the
|
|
34
34
|
* same envelope fields below.
|
|
35
35
|
*
|
|
36
|
-
*
|
|
36
|
+
* Obligation and Receipt are roles/facets of `Intent`, not special admission objects, job-table rows,
|
|
37
37
|
* or a privileged mutation channel.
|
|
38
38
|
*
|
|
39
39
|
* **Reporting upward = the authoring peer reactively WATCHES the task aggregate.** The receipt
|
|
40
40
|
* arrives as an ordinary projection update (no polling, no fire-and-forget push, no
|
|
41
41
|
* response-mutation). This is exactly the proven `ExportJobAggregate` shape
|
|
42
42
|
* (the tenant domains package `thing_structures`) generalised into a reusable framework pattern so a
|
|
43
|
-
* domain dev does NOT hand-roll the {aggregate +
|
|
43
|
+
* domain dev does NOT hand-roll the {aggregate + obligation + complete + fail} quartet each
|
|
44
44
|
* time — they DECLARE the task's params and get the quartet for free, like `relations.ts`
|
|
45
45
|
* gives grant/revoke for free.
|
|
46
46
|
*
|
|
@@ -77,7 +77,7 @@ import type { Ports } from "../ctx.js";
|
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
79
|
* The impure-capability lifecycle vocabulary — framework-fixed. A task is `requested` when
|
|
80
|
-
* the peer
|
|
80
|
+
* the peer obligates it; the provider authors the ordinary Receipt intent that flips it
|
|
81
81
|
* to `completed` (with a `resultRef`) or `failed` (with a `failureReason`). One-way after a
|
|
82
82
|
* terminal state: the lifecycle law is terminal-once at the gate/envelope, not a separate
|
|
83
83
|
* privileged mutation channel.
|
|
@@ -92,14 +92,14 @@ export const IMPURE_CAPABILITY_STATUSES = [
|
|
|
92
92
|
export type ImpureCapabilityStatus = (typeof IMPURE_CAPABILITY_STATUSES)[number];
|
|
93
93
|
|
|
94
94
|
/**
|
|
95
|
-
* The framework envelope fields EVERY impure-capability aggregate carries — the
|
|
95
|
+
* The framework envelope fields EVERY impure-capability aggregate carries — the Obligation→Receipt
|
|
96
96
|
* lifecycle, independent of the tenant's request params:
|
|
97
97
|
*
|
|
98
98
|
* - requestId Lww — the task's own id (== the aggregate instance id; a
|
|
99
|
-
* KERNEL-MINTED `<
|
|
99
|
+
* KERNEL-MINTED `<ObligationType>_<uuidv7>`). Immutable-after-create (the
|
|
100
100
|
* create-only gap; modelled Lww).
|
|
101
|
-
* - requestedBy Lww — who
|
|
102
|
-
* - requestedAt Int — epoch-ms the
|
|
101
|
+
* - requestedBy Lww — who obligated the task (provenance). Immutable-after-create.
|
|
102
|
+
* - requestedAt Int — epoch-ms the obligation was authored. Immutable-after-create.
|
|
103
103
|
* - status Lww enum — `requested` → the reactor flips to a terminal status.
|
|
104
104
|
* - resultRef Lww json (OPTIONAL) — the content-addressed RESULT the reactor
|
|
105
105
|
* records on completion (a blob ref / download url JSON leaf). Absent
|
|
@@ -111,10 +111,10 @@ export type ImpureCapabilityStatus = (typeof IMPURE_CAPABILITY_STATUSES)[number]
|
|
|
111
111
|
* external condition.
|
|
112
112
|
* - blockedAt Int (OPTIONAL) — epoch-ms the task was parked as blocked.
|
|
113
113
|
* - nextAction Lww (OPTIONAL) — human/host-action hint, e.g. refreshAuth.
|
|
114
|
-
* - deadLetterReason Lww (OPTIONAL) — why the
|
|
115
|
-
* - deadLetteredAt Int (OPTIONAL) — epoch-ms the
|
|
114
|
+
* - deadLetterReason Lww (OPTIONAL) — why the obligation was moved to the DLQ.
|
|
115
|
+
* - deadLetteredAt Int (OPTIONAL) — epoch-ms the obligation entered the DLQ.
|
|
116
116
|
*
|
|
117
|
-
* The OPTIONALs are `.optional()` so a freshly-
|
|
117
|
+
* The OPTIONALs are `.optional()` so a freshly-obligated task (which folds none of them)
|
|
118
118
|
* decodes on the generated read type — the same read-decode discipline the rest of the
|
|
119
119
|
* framework uses (identity NOTE 1a / gap 1).
|
|
120
120
|
*/
|
|
@@ -122,6 +122,8 @@ export const IMPURE_CAPABILITY_ENVELOPE = {
|
|
|
122
122
|
requestId: t.string().merge(Lww),
|
|
123
123
|
requestedBy: t.string().merge(Lww),
|
|
124
124
|
requestedAt: t.int().merge(Lww),
|
|
125
|
+
scheduledAt: t.int().merge(Lww),
|
|
126
|
+
deadlineAt: t.int().merge(Lww),
|
|
125
127
|
status: t.enum(IMPURE_CAPABILITY_STATUSES).merge(Lww),
|
|
126
128
|
resultRef: t.json().merge(Lww).optional(),
|
|
127
129
|
failureReason: t.string().merge(Lww).optional(),
|
|
@@ -131,16 +133,16 @@ export const IMPURE_CAPABILITY_ENVELOPE = {
|
|
|
131
133
|
nextAction: t.string().merge(Lww).optional(),
|
|
132
134
|
deadLetterReason: t.string().merge(Lww).optional(),
|
|
133
135
|
deadLetteredAt: t.int().merge(Lww).optional(),
|
|
134
|
-
// The FRAMEWORK
|
|
136
|
+
// The FRAMEWORK Obligation↔Receipt CLOSURE link (OPTIONAL): the id of the Obligation this
|
|
135
137
|
// RECEIPT closes (== the task's own requestId — the reactor mutates the task instance).
|
|
136
138
|
// GENERIC, written by every impureCapability Receipt (complete OR fail), so the read
|
|
137
|
-
// projection answers "is
|
|
138
|
-
// off the `(
|
|
139
|
-
// `requested` (a fresh
|
|
139
|
+
// projection answers "is Obligation O closed, by which Receipt, with what result" in O(1)
|
|
140
|
+
// off the `(closesObligation, obligation_id)` index — never a ledger scan. Absent while
|
|
141
|
+
// `requested` (a fresh obligation has no closing receipt). The kernel supplies its
|
|
140
142
|
// framework-fixed Lww driver (`kernel::manifest_view::IMPURE_CAPABILITY_RECEIPT_FIELDS`)
|
|
141
|
-
// and the gate enforces the lifecycle around it (`nomos_executor::
|
|
142
|
-
// admit step 5.5: OrphanReceipt /
|
|
143
|
-
|
|
143
|
+
// and the gate enforces the lifecycle around it (`nomos_executor::check_obligation_receipt` —
|
|
144
|
+
// admit step 5.5: OrphanReceipt / ReceiptOnTerminalObligation, terminal-once as law).
|
|
145
|
+
closesObligation: t.string().merge(Lww).optional(),
|
|
144
146
|
} as const;
|
|
145
147
|
|
|
146
148
|
/**
|
|
@@ -163,9 +165,9 @@ export function impureCapabilityAggregate<
|
|
|
163
165
|
}
|
|
164
166
|
|
|
165
167
|
/**
|
|
166
|
-
* The deterministic instance-id rule: a impure capability is keyed by its OWN
|
|
167
|
-
* That id is KERNEL-MINTED (`<
|
|
168
|
-
* captured + committed — so the `
|
|
168
|
+
* The deterministic instance-id rule: a impure capability is keyed by its OWN obligation id.
|
|
169
|
+
* That id is KERNEL-MINTED (`<ObligationType>_<uuidv7>`, via the marker-driven front-door),
|
|
170
|
+
* captured + committed — so the `obligation` create is idempotent on replay by capture, and the
|
|
169
171
|
* `complete`/`fail` receipt addresses the SAME instance by reading the committed minted id.
|
|
170
172
|
* Trivial today (identity), but centralised here so the keying rule has ONE home (the
|
|
171
173
|
* symmetry with `relations.ts`'s `relationTupleInstanceId`).
|
|
@@ -174,11 +176,13 @@ export function impureCapabilityInstanceId(requestId: string): string {
|
|
|
174
176
|
return requestId;
|
|
175
177
|
}
|
|
176
178
|
|
|
177
|
-
/** The Zod schema for the framework envelope fields the
|
|
178
|
-
const
|
|
179
|
+
/** The Zod schema for the framework envelope fields the Obligation payload always carries. */
|
|
180
|
+
const obligationEnvelopeSchema = z.object({
|
|
179
181
|
requestId: z.string(),
|
|
180
182
|
requestedBy: z.string(),
|
|
181
183
|
requestedAt: z.number().int(),
|
|
184
|
+
scheduledAt: z.number().int(),
|
|
185
|
+
deadlineAt: z.number().int(),
|
|
182
186
|
});
|
|
183
187
|
|
|
184
188
|
/** The receipt-COMPLETE payload: the request id + the content-addressed result + when. */
|
|
@@ -204,7 +208,7 @@ const blockSchema = z.object({
|
|
|
204
208
|
nextAction: z.string(),
|
|
205
209
|
});
|
|
206
210
|
|
|
207
|
-
/** The receipt-DLQ payload: terminal dead-letter, requiring a fresh
|
|
211
|
+
/** The receipt-DLQ payload: terminal dead-letter, requiring a fresh obligation to recover. */
|
|
208
212
|
const deadLetterSchema = z.object({
|
|
209
213
|
requestId: z.string(),
|
|
210
214
|
deadLetterReason: z.string(),
|
|
@@ -232,13 +236,13 @@ function envelopeInstance(aggregateId: string, requestId: string) {
|
|
|
232
236
|
}
|
|
233
237
|
|
|
234
238
|
/**
|
|
235
|
-
*
|
|
239
|
+
* obligationOps — the Obligation half. Seeds the envelope provenance + timing + `status:'requested'`,
|
|
236
240
|
* PLUS the tenant's request params (the `paramOps` the caller's `.plan` supplies for
|
|
237
241
|
* its own fields). The task is bound to its requestId instance (#105).
|
|
238
242
|
*/
|
|
239
|
-
export function
|
|
243
|
+
export function obligationOps(
|
|
240
244
|
aggregateId: string,
|
|
241
|
-
p: z.infer<typeof
|
|
245
|
+
p: z.infer<typeof obligationEnvelopeSchema>,
|
|
242
246
|
paramOps: PlannedOp[] = [],
|
|
243
247
|
): PlannedOp[] {
|
|
244
248
|
const taskInstance = envelopeInstance(aggregateId, p.requestId);
|
|
@@ -246,6 +250,8 @@ export function orderOps(
|
|
|
246
250
|
set(taskInstance, "requestId", p.requestId),
|
|
247
251
|
set(taskInstance, "requestedBy", p.requestedBy),
|
|
248
252
|
set(taskInstance, "requestedAt", p.requestedAt),
|
|
253
|
+
set(taskInstance, "scheduledAt", p.scheduledAt),
|
|
254
|
+
set(taskInstance, "deadlineAt", p.deadlineAt),
|
|
249
255
|
set(taskInstance, "status", "requested"),
|
|
250
256
|
...paramOps,
|
|
251
257
|
];
|
|
@@ -266,10 +272,10 @@ export function completeOps(
|
|
|
266
272
|
set(taskInstance, "status", "completed"),
|
|
267
273
|
set(taskInstance, "resultRef", p.resultRef),
|
|
268
274
|
set(taskInstance, "completedAt", p.completedAt),
|
|
269
|
-
// The FRAMEWORK
|
|
275
|
+
// The FRAMEWORK Obligation↔Receipt CLOSURE link — this Receipt closes the Obligation whose
|
|
270
276
|
// id == requestId (the task instance the reactor mutates). Indexed by the read
|
|
271
277
|
// projection for the O(1) closure read. Wire-identical to the Rust reactor's op.
|
|
272
|
-
set(taskInstance, "
|
|
278
|
+
set(taskInstance, "closesObligation", impureCapabilityInstanceId(p.requestId)),
|
|
273
279
|
// The TENANT RESULT FIELDS the Receipt writes into the task aggregate (e.g.
|
|
274
280
|
// `transcription = <text>`). The envelope is auto; these are the tenant's own
|
|
275
281
|
// structured result. Framework writes them verbatim; the tenant owns their meaning.
|
|
@@ -293,8 +299,8 @@ export function failOps(
|
|
|
293
299
|
set(taskInstance, "failureReason", p.failureReason),
|
|
294
300
|
set(taskInstance, "completedAt", p.failedAt),
|
|
295
301
|
// CLOSURE link — a FAILED task is still CLOSED by its receipt (the closure read
|
|
296
|
-
// reports it CLOSED with no result). Same `
|
|
297
|
-
set(taskInstance, "
|
|
302
|
+
// reports it CLOSED with no result). Same `closesObligation == requestId` as `completeOps`.
|
|
303
|
+
set(taskInstance, "closesObligation", impureCapabilityInstanceId(p.requestId)),
|
|
298
304
|
];
|
|
299
305
|
}
|
|
300
306
|
|
|
@@ -309,11 +315,11 @@ export function blockOps(
|
|
|
309
315
|
set(taskInstance, "blockedReason", p.blockedReason),
|
|
310
316
|
set(taskInstance, "blockedAt", p.blockedAt),
|
|
311
317
|
set(taskInstance, "nextAction", p.nextAction),
|
|
312
|
-
set(taskInstance, "
|
|
318
|
+
set(taskInstance, "closesObligation", impureCapabilityInstanceId(p.requestId)),
|
|
313
319
|
];
|
|
314
320
|
}
|
|
315
321
|
|
|
316
|
-
/** deadLetterOps — terminal DLQ receipt. Recovery is a fresh explicit
|
|
322
|
+
/** deadLetterOps — terminal DLQ receipt. Recovery is a fresh explicit Obligation. */
|
|
317
323
|
export function deadLetterOps(
|
|
318
324
|
aggregateId: string,
|
|
319
325
|
p: z.infer<typeof deadLetterSchema>,
|
|
@@ -323,7 +329,7 @@ export function deadLetterOps(
|
|
|
323
329
|
set(taskInstance, "status", "deadLettered"),
|
|
324
330
|
set(taskInstance, "deadLetterReason", p.deadLetterReason),
|
|
325
331
|
set(taskInstance, "deadLetteredAt", p.deadLetteredAt),
|
|
326
|
-
set(taskInstance, "
|
|
332
|
+
set(taskInstance, "closesObligation", impureCapabilityInstanceId(p.requestId)),
|
|
327
333
|
];
|
|
328
334
|
}
|
|
329
335
|
|
|
@@ -331,11 +337,11 @@ export function deadLetterOps(
|
|
|
331
337
|
* The generated directive QUARTET for a impure capability. A domain dev calls
|
|
332
338
|
* {@link impureCapability} ONCE and gets:
|
|
333
339
|
* • `aggregate` — the task aggregate handle (envelope ⊕ params),
|
|
334
|
-
* • `
|
|
340
|
+
* • `obligation` — `.creates` directive (the authored Obligation),
|
|
335
341
|
* • `complete` — `.mutates` directive (the reactor's success Receipt),
|
|
336
342
|
* • `fail` — `.mutates` directive (the reactor's failure Receipt),
|
|
337
|
-
* plus the deterministic `instanceId` rule. The `
|
|
338
|
-
*
|
|
343
|
+
* plus the deterministic `instanceId` rule. The `obligation` payload is the framework
|
|
344
|
+
* obligation-envelope MERGED with the tenant's `paramsSchema`; `complete`/`fail` are fixed.
|
|
339
345
|
*/
|
|
340
346
|
export interface ImpureCapability<
|
|
341
347
|
Id extends string,
|
|
@@ -343,7 +349,7 @@ export interface ImpureCapability<
|
|
|
343
349
|
F extends Record<string, Field>,
|
|
344
350
|
> {
|
|
345
351
|
readonly aggregate: AggregateHandle<Id, typeof IMPURE_CAPABILITY_ENVELOPE & F>;
|
|
346
|
-
readonly
|
|
352
|
+
readonly obligation: RequirableDirective<ObligationPayload<ParamsShape>>;
|
|
347
353
|
readonly complete: RequirableDirective<z.infer<typeof completeSchema>>;
|
|
348
354
|
readonly fail: RequirableDirective<z.infer<typeof failSchema>>;
|
|
349
355
|
readonly block: RequirableDirective<z.infer<typeof blockSchema>>;
|
|
@@ -364,17 +370,17 @@ export interface ImpureCapability<
|
|
|
364
370
|
}
|
|
365
371
|
|
|
366
372
|
/**
|
|
367
|
-
* The inferred
|
|
368
|
-
* tenant's param shape — the EXACT inference `
|
|
369
|
-
* produces (so the declared `ImpureCapability.
|
|
373
|
+
* The inferred Obligation payload type = the framework envelope schema EXTENDED with the
|
|
374
|
+
* tenant's param shape — the EXACT inference `obligationEnvelopeSchema.extend(paramsSchema)`
|
|
375
|
+
* produces (so the declared `ImpureCapability.obligation` directive type and the value the factory
|
|
370
376
|
* builds are the same type, with no brittle hand-written intersection).
|
|
371
377
|
*/
|
|
372
|
-
export type
|
|
373
|
-
ReturnType<typeof
|
|
378
|
+
export type ObligationPayload<ParamsShape extends z.ZodRawShape> = z.infer<
|
|
379
|
+
ReturnType<typeof obligationEnvelopeSchema.extend<ParamsShape>>
|
|
374
380
|
>;
|
|
375
381
|
|
|
376
382
|
/**
|
|
377
|
-
* impureCapability — the framework FACTORY. Declares an
|
|
383
|
+
* impureCapability — the framework FACTORY. Declares an Obligation→Receipt task in ONE call:
|
|
378
384
|
*
|
|
379
385
|
* ```ts
|
|
380
386
|
* // Build the aggregate handle FIRST so `writeParams` references it without a
|
|
@@ -384,7 +390,7 @@ export type OrderPayload<ParamsShape extends z.ZodRawShape> = z.infer<
|
|
|
384
390
|
*
|
|
385
391
|
* export const exportJob = impureCapability({
|
|
386
392
|
* aggregateId: "ExportJobAggregate",
|
|
387
|
-
*
|
|
393
|
+
* obligationDirectiveId: "createExportJob",
|
|
388
394
|
* completeDirectiveId: "completeExportJob",
|
|
389
395
|
* failDirectiveId: "failExportJob",
|
|
390
396
|
* blockDirectiveId: "blockExportJob",
|
|
@@ -400,33 +406,33 @@ export type OrderPayload<ParamsShape extends z.ZodRawShape> = z.infer<
|
|
|
400
406
|
*
|
|
401
407
|
* The tenant declares the wire ids (so they're stable, minify-safe DATA — the same
|
|
402
408
|
* declared-once rule as `aggregate()`/`directive()`), the param FIELDS (for the
|
|
403
|
-
* aggregate shape + codegen) and the param SCHEMA (for the
|
|
409
|
+
* aggregate shape + codegen) and the param SCHEMA (for the obligation payload + the
|
|
404
410
|
* `writeParams` plan fragment). The framework wires the lifecycle: status defaulting,
|
|
405
411
|
* the requestId keying, and the complete/fail receipt directives.
|
|
406
412
|
*
|
|
407
413
|
* `writeParams(taskInstanceId, params)` returns the `PlannedOp[]` for the tenant's OWN
|
|
408
|
-
* fields (the framework already writes the envelope) — the param half of the
|
|
414
|
+
* fields (the framework already writes the envelope) — the param half of the obligation
|
|
409
415
|
* plan, kept type-checked against the tenant's param fields via the `set()` helper.
|
|
410
416
|
*/
|
|
411
417
|
export function impureCapability<
|
|
412
418
|
const Id extends string,
|
|
413
|
-
const
|
|
419
|
+
const ObligationId extends string,
|
|
414
420
|
const CompleteId extends string,
|
|
415
421
|
const FailId extends string,
|
|
416
422
|
ParamsShape extends z.ZodRawShape,
|
|
417
423
|
F extends Record<string, Field>,
|
|
418
424
|
>(spec: {
|
|
419
425
|
aggregateId: Id;
|
|
420
|
-
|
|
426
|
+
obligationDirectiveId: ObligationId;
|
|
421
427
|
completeDirectiveId: CompleteId;
|
|
422
428
|
failDirectiveId: FailId;
|
|
423
429
|
blockDirectiveId: string;
|
|
424
430
|
deadLetterDirectiveId: string;
|
|
425
431
|
/** The task's request-param FIELDS (merged onto the envelope for the aggregate shape). */
|
|
426
432
|
params: F;
|
|
427
|
-
/** The task's request-param Zod SHAPE (merged onto the
|
|
433
|
+
/** The task's request-param Zod SHAPE (merged onto the obligation envelope schema). */
|
|
428
434
|
paramsSchema: ParamsShape;
|
|
429
|
-
/** Plan fragment writing the tenant's param fields for the
|
|
435
|
+
/** Plan fragment writing the tenant's param fields for the obligation (envelope is auto). */
|
|
430
436
|
writeParams: (
|
|
431
437
|
taskInstanceId: string,
|
|
432
438
|
params: z.infer<z.ZodObject<ParamsShape>>,
|
|
@@ -434,7 +440,7 @@ export function impureCapability<
|
|
|
434
440
|
/**
|
|
435
441
|
* OPTIONAL plan fragment writing the tenant's STRUCTURED RESULT fields onto the task
|
|
436
442
|
* aggregate when the `complete` Receipt is authored (the lifecycle envelope + the
|
|
437
|
-
* `
|
|
443
|
+
* `closesObligation` link is auto). The receipt's `resultRef` carries the content-addressed
|
|
438
444
|
* Evidence ref (the authoritative artifact); a tenant that ALSO wants the result in a
|
|
439
445
|
* typed, queryable field (e.g. a voice-note's `transcription`) supplies it here. The
|
|
440
446
|
* `resultRef` string is passed through so the tenant can derive the field value from the
|
|
@@ -451,22 +457,22 @@ export function impureCapability<
|
|
|
451
457
|
}): ImpureCapability<Id, ParamsShape, F> {
|
|
452
458
|
const taskAggregate = impureCapabilityAggregate(spec.aggregateId, spec.params);
|
|
453
459
|
|
|
454
|
-
// The
|
|
455
|
-
const
|
|
460
|
+
// The obligation payload = the framework provenance/timing envelope ⊕ the tenant's params.
|
|
461
|
+
const obligationPayload = obligationEnvelopeSchema.extend(spec.paramsSchema);
|
|
456
462
|
|
|
457
|
-
const
|
|
458
|
-
spec.
|
|
463
|
+
const obligation: RequirableDirective<ObligationPayload<ParamsShape>> = directive(
|
|
464
|
+
spec.obligationDirectiveId,
|
|
459
465
|
)
|
|
460
466
|
.creates(taskAggregate)
|
|
461
|
-
.payload(
|
|
467
|
+
.payload(obligationPayload)
|
|
462
468
|
.plan((p) => {
|
|
463
469
|
// The validated payload carries BOTH the envelope (required string/int) and the
|
|
464
|
-
// tenant params. `
|
|
470
|
+
// tenant params. `obligationOps` writes the envelope; the tenant's `writeParams`
|
|
465
471
|
// writes its own fields. Extract the envelope explicitly (it is required on the
|
|
466
472
|
// schema, so these are present non-undefined values).
|
|
467
|
-
const envelope =
|
|
473
|
+
const envelope = obligationEnvelopeSchema.parse(p);
|
|
468
474
|
const params = p as z.infer<z.ZodObject<ParamsShape>>;
|
|
469
|
-
return
|
|
475
|
+
return obligationOps(
|
|
470
476
|
spec.aggregateId,
|
|
471
477
|
envelope,
|
|
472
478
|
spec.writeParams(impureCapabilityInstanceId(envelope.requestId), params),
|
|
@@ -502,7 +508,7 @@ export function impureCapability<
|
|
|
502
508
|
|
|
503
509
|
return {
|
|
504
510
|
aggregate: taskAggregate,
|
|
505
|
-
|
|
511
|
+
obligation,
|
|
506
512
|
complete,
|
|
507
513
|
fail,
|
|
508
514
|
block,
|
|
@@ -516,7 +522,7 @@ export function impureCapability<
|
|
|
516
522
|
|
|
517
523
|
// ─────────────────────────── THE CAPABILITY PROVIDER ─────────────────────────
|
|
518
524
|
//
|
|
519
|
-
// The
|
|
525
|
+
// The Obligation half (`obligation`) is an ordinary Intent authored by the peer through the one write
|
|
520
526
|
// path. The Receipt half (`complete`/`fail`) is an ordinary Intent authored by a capability
|
|
521
527
|
// PROVIDER — itself a Nomos author (`admission-hosting.md`: hosted/provider-originated
|
|
522
528
|
// intents author *through* a policy unit too"). The provider is the runtime ROLE; this
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* Nomos sync lifecycle.
|
|
11
11
|
*
|
|
12
12
|
* Sync is a control-plane impure capability: Nomos records the desired workspace
|
|
13
|
-
* reconciliation as an ordinary
|
|
13
|
+
* reconciliation as an ordinary Obligation, a host/provider performs transport using
|
|
14
14
|
* explicit capabilities, and the outcome comes back as an ordinary Receipt. The
|
|
15
15
|
* remote/JWT/network is never ambient domain state; the ledger carries request,
|
|
16
16
|
* status, cursors, heads, and errors.
|
|
@@ -85,7 +85,7 @@ function syncResultOps(taskId: string, resultRef: string): PlannedOp[] {
|
|
|
85
85
|
|
|
86
86
|
const workspaceSyncTask = impureCapability({
|
|
87
87
|
aggregateId: "WorkspaceSyncOperation",
|
|
88
|
-
|
|
88
|
+
obligationDirectiveId: "requestWorkspaceSync",
|
|
89
89
|
completeDirectiveId: "completeWorkspaceSync",
|
|
90
90
|
failDirectiveId: "failWorkspaceSync",
|
|
91
91
|
blockDirectiveId: "blockWorkspaceSync",
|
|
@@ -113,7 +113,7 @@ const workspaceSyncTask = impureCapability({
|
|
|
113
113
|
completeResult: syncResultOps,
|
|
114
114
|
});
|
|
115
115
|
|
|
116
|
-
export const requestWorkspaceSync = workspaceSyncTask.
|
|
116
|
+
export const requestWorkspaceSync = workspaceSyncTask.obligation.requires("requestWorkspaceSync");
|
|
117
117
|
export const completeWorkspaceSync = workspaceSyncTask.complete.requires("completeWorkspaceSync");
|
|
118
118
|
export const failWorkspaceSync = workspaceSyncTask.fail.requires("failWorkspaceSync");
|
|
119
119
|
export const blockWorkspaceSync = workspaceSyncTask.block.requires("blockWorkspaceSync");
|
|
@@ -641,6 +641,11 @@ export const AuthProvider = aggregate("AuthProvider", {
|
|
|
641
641
|
issuer: t.string().merge(Lww),
|
|
642
642
|
/** The provider's public JWKS endpoint (asymmetric verification keys). */
|
|
643
643
|
jwksUrl: t.string().merge(Lww),
|
|
644
|
+
/** OPTIONAL expected `aud` claim. When the platform sets it, a token's `aud` MUST carry this value
|
|
645
|
+
* (defense-in-depth beyond the issuer match — e.g. a Firebase project id, an Auth0 API audience).
|
|
646
|
+
* Absent ⇒ issuer match alone. This is LAW-STATE the PLATFORM owns; the host merely enforces it as a
|
|
647
|
+
* capability (it never chooses the audience). */
|
|
648
|
+
audience: t.string().merge(Lww),
|
|
644
649
|
/** Human label for audit ("acme supabase", ...). */
|
|
645
650
|
label: t.string().merge(Lww),
|
|
646
651
|
/** Who set it (attribution). */
|
|
@@ -661,6 +666,7 @@ export const setAuthProvider = directive("setAuthProvider")
|
|
|
661
666
|
// deterministic engine does not provide — a regex keeps the plan pure.
|
|
662
667
|
issuer: z.string().regex(/^https?:\/\/\S+$/, "expected an http(s) URL"),
|
|
663
668
|
jwksUrl: z.string().regex(/^https?:\/\/\S+$/, "expected an http(s) URL"),
|
|
669
|
+
audience: z.string().min(1).optional(),
|
|
664
670
|
label: z.string().min(1),
|
|
665
671
|
setBy: z.string().min(1),
|
|
666
672
|
setAt: epochMs,
|
|
@@ -672,6 +678,7 @@ export const setAuthProvider = directive("setAuthProvider")
|
|
|
672
678
|
withMarker(set(ap, "scope", "platform"), "ensures"),
|
|
673
679
|
set(ap, "issuer", p.issuer),
|
|
674
680
|
set(ap, "jwksUrl", p.jwksUrl),
|
|
681
|
+
...(p.audience ? [set(ap, "audience", p.audience)] : []),
|
|
675
682
|
set(ap, "label", p.label),
|
|
676
683
|
set(ap, "setBy", p.setBy),
|
|
677
684
|
set(ap, "setAt", p.setAt),
|
package/src/index.ts
CHANGED
package/src/usd.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
type CanonicalManifest,
|
|
34
34
|
type CanonicalProjectionReturn,
|
|
35
35
|
} from "./manifest.js";
|
|
36
|
+
import type { AggregateHandle } from "./aggregate.js";
|
|
36
37
|
import {
|
|
37
38
|
mergeRequiresScope,
|
|
38
39
|
CompositionViolation,
|
|
@@ -126,10 +127,46 @@ export interface UsdCombined {
|
|
|
126
127
|
readonly returns: CanonicalProjectionReturn;
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
/** One field's read-projection descriptor: the field kind plus a map's value kind. */
|
|
131
|
+
export interface UsdFieldKindDescriptor {
|
|
132
|
+
readonly kind: string;
|
|
133
|
+
readonly mapValueKind?: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface UsdReadProjection {
|
|
137
|
+
readonly aggregateFieldKinds: Record<string, Record<string, UsdFieldKindDescriptor>>;
|
|
138
|
+
readonly queries: { id: string; key: { field: string; kind: string }[]; returns: string }[];
|
|
139
|
+
readonly counts: { id: string; of: string; by: string | null; where?: unknown }[];
|
|
140
|
+
readonly spatials: { id: string; of: string; on: string }[];
|
|
141
|
+
readonly deriveds: { id: string; of: string; returns: CanonicalProjectionReturn }[];
|
|
142
|
+
readonly combineds: { id: string; of: string; refField: string; reads: string; returns: CanonicalProjectionReturn }[];
|
|
143
|
+
readonly sums?: { id: string; of: string; field: string; by: string | null; where?: unknown }[];
|
|
144
|
+
readonly mins?: { id: string; of: string; field: string; by: string | null; where?: unknown }[];
|
|
145
|
+
readonly maxes?: { id: string; of: string; field: string; by: string | null; where?: unknown }[];
|
|
146
|
+
readonly orderedReads?: {
|
|
147
|
+
id: string;
|
|
148
|
+
of: string;
|
|
149
|
+
orderKey: string;
|
|
150
|
+
orderDesc?: true;
|
|
151
|
+
where?: unknown;
|
|
152
|
+
limit: number;
|
|
153
|
+
}[];
|
|
154
|
+
}
|
|
155
|
+
|
|
129
156
|
/** One USD layer: a stage path + its prims (canonically sorted by prim path). */
|
|
130
157
|
export interface UsdLayer {
|
|
131
158
|
readonly path: string;
|
|
132
159
|
readonly prims: UsdPrim[];
|
|
160
|
+
/**
|
|
161
|
+
* The canonical admission/merge manifest for this layer, committed inside the package.
|
|
162
|
+
* Hosts must not stage or mutate this view; holons derive it from active installed law.
|
|
163
|
+
*/
|
|
164
|
+
readonly nomosIdentityManifest?: CanonicalManifest;
|
|
165
|
+
/**
|
|
166
|
+
* The projection schema/read-routing fragment for this layer, committed inside the package.
|
|
167
|
+
* It carries the projection-only field-kind data that cannot be recovered from merge drivers.
|
|
168
|
+
*/
|
|
169
|
+
readonly nomosReadProjection?: UsdReadProjection;
|
|
133
170
|
/**
|
|
134
171
|
* The layer's NAMED, INDEXED read declarations (read-side closure step 1), SORTED
|
|
135
172
|
* by id. OMITTED (`undefined`) when the module declares no query, so a query-free
|
|
@@ -363,6 +400,123 @@ function encodeModuleToPrims(layerPath: string, mod: DomainModule): UsdPrim[] {
|
|
|
363
400
|
return sortPrims(prims);
|
|
364
401
|
}
|
|
365
402
|
|
|
403
|
+
function aggregateFieldKindsForModule(
|
|
404
|
+
mod: DomainModule,
|
|
405
|
+
): Record<string, Record<string, UsdFieldKindDescriptor>> {
|
|
406
|
+
const out: Record<string, Record<string, UsdFieldKindDescriptor>> = {};
|
|
407
|
+
for (const agg of [...(mod.aggregates as AggregateHandle[])].sort((a, b) =>
|
|
408
|
+
a.id < b.id ? -1 : a.id > b.id ? 1 : 0,
|
|
409
|
+
)) {
|
|
410
|
+
const fields: Record<string, UsdFieldKindDescriptor> = {};
|
|
411
|
+
for (const name of Object.keys(agg.fields).sort()) {
|
|
412
|
+
const field = agg.fields[name]!;
|
|
413
|
+
fields[name] = {
|
|
414
|
+
kind: field.kind,
|
|
415
|
+
...(field.mapValueKind !== undefined ? { mapValueKind: field.mapValueKind } : {}),
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
out[agg.id] = fields;
|
|
419
|
+
}
|
|
420
|
+
return out;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function readKeyFieldKind(
|
|
424
|
+
byType: Record<string, Record<string, UsdFieldKindDescriptor>>,
|
|
425
|
+
queryId: string,
|
|
426
|
+
returns: string,
|
|
427
|
+
keyField: string,
|
|
428
|
+
): string {
|
|
429
|
+
const schema = byType[returns];
|
|
430
|
+
if (schema === undefined) {
|
|
431
|
+
throw new Error(
|
|
432
|
+
`usd: query '${queryId}' returns '${returns}', which has no field schema in nomosReadProjection`,
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
const lookup = keyField.includes(".") ? keyField.split(".")[0]! : keyField;
|
|
436
|
+
const descriptor = schema[lookup];
|
|
437
|
+
if (descriptor === undefined) {
|
|
438
|
+
throw new Error(
|
|
439
|
+
`usd: query '${queryId}' is keyed on '${keyField}' (field '${lookup}'), which '${returns}' does not declare`,
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
return descriptor.kind;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function readProjectionForModule(mod: DomainModule, manifest: CanonicalManifest): UsdReadProjection {
|
|
446
|
+
const aggregateFieldKinds = aggregateFieldKindsForModule(mod);
|
|
447
|
+
return {
|
|
448
|
+
aggregateFieldKinds,
|
|
449
|
+
queries: (manifest.queries ?? []).map((q) => ({
|
|
450
|
+
id: q.id,
|
|
451
|
+
key: q.key.map((field) => ({
|
|
452
|
+
field,
|
|
453
|
+
kind: readKeyFieldKind(aggregateFieldKinds, q.id, q.returns, field),
|
|
454
|
+
})),
|
|
455
|
+
returns: q.returns,
|
|
456
|
+
})),
|
|
457
|
+
counts: (manifest.counts ?? []).map((c) => ({
|
|
458
|
+
id: c.id,
|
|
459
|
+
of: c.of,
|
|
460
|
+
by: c.by ?? null,
|
|
461
|
+
...(c.where !== undefined ? { where: c.where } : {}),
|
|
462
|
+
})),
|
|
463
|
+
spatials: (manifest.spatials ?? []).map((s) => ({ id: s.id, of: s.of, on: s.on })),
|
|
464
|
+
deriveds: (manifest.deriveds ?? []).map((d) => ({ id: d.id, of: d.of, returns: d.returns })),
|
|
465
|
+
combineds: (manifest.combineds ?? []).map((c) => ({
|
|
466
|
+
id: c.id,
|
|
467
|
+
of: c.of,
|
|
468
|
+
refField: c.refField,
|
|
469
|
+
reads: c.reads,
|
|
470
|
+
returns: c.returns,
|
|
471
|
+
})),
|
|
472
|
+
...((manifest.sums ?? []).length > 0
|
|
473
|
+
? {
|
|
474
|
+
sums: manifest.sums!.map((s) => ({
|
|
475
|
+
id: s.id,
|
|
476
|
+
of: s.of,
|
|
477
|
+
field: s.sumField,
|
|
478
|
+
by: s.by ?? null,
|
|
479
|
+
...(s.where !== undefined ? { where: s.where } : {}),
|
|
480
|
+
})),
|
|
481
|
+
}
|
|
482
|
+
: {}),
|
|
483
|
+
...((manifest.mins ?? []).length > 0
|
|
484
|
+
? {
|
|
485
|
+
mins: manifest.mins!.map((e) => ({
|
|
486
|
+
id: e.id,
|
|
487
|
+
of: e.of,
|
|
488
|
+
field: e.valueField,
|
|
489
|
+
by: e.by ?? null,
|
|
490
|
+
...(e.where !== undefined ? { where: e.where } : {}),
|
|
491
|
+
})),
|
|
492
|
+
}
|
|
493
|
+
: {}),
|
|
494
|
+
...((manifest.maxes ?? []).length > 0
|
|
495
|
+
? {
|
|
496
|
+
maxes: manifest.maxes!.map((e) => ({
|
|
497
|
+
id: e.id,
|
|
498
|
+
of: e.of,
|
|
499
|
+
field: e.valueField,
|
|
500
|
+
by: e.by ?? null,
|
|
501
|
+
...(e.where !== undefined ? { where: e.where } : {}),
|
|
502
|
+
})),
|
|
503
|
+
}
|
|
504
|
+
: {}),
|
|
505
|
+
...((manifest.orderedReads ?? []).length > 0
|
|
506
|
+
? {
|
|
507
|
+
orderedReads: manifest.orderedReads!.map((r) => ({
|
|
508
|
+
id: r.id,
|
|
509
|
+
of: r.of,
|
|
510
|
+
orderKey: r.orderKey,
|
|
511
|
+
...(r.orderDesc ? { orderDesc: true as const } : {}),
|
|
512
|
+
...(r.where !== undefined ? { where: r.where } : {}),
|
|
513
|
+
limit: r.limit,
|
|
514
|
+
})),
|
|
515
|
+
}
|
|
516
|
+
: {}),
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
366
520
|
/**
|
|
367
521
|
* Lower a layer's `relationType` decls into the canonical `nomosRelations` wire object (P1) —
|
|
368
522
|
* keys SORTED for byte-determinism, omit-when-empty (the era rule). Multiple decls for one type
|
|
@@ -549,6 +703,8 @@ export function emitUsd(
|
|
|
549
703
|
return {
|
|
550
704
|
path: l.path,
|
|
551
705
|
prims: encodeModuleToPrims(l.path, l.module),
|
|
706
|
+
nomosIdentityManifest: manifest,
|
|
707
|
+
nomosReadProjection: readProjectionForModule(l.module, manifest),
|
|
552
708
|
...(manifest.queries !== undefined ? { queries: manifest.queries } : {}),
|
|
553
709
|
...(manifest.deriveds !== undefined ? { deriveds: manifest.deriveds } : {}),
|
|
554
710
|
...(manifest.combineds !== undefined ? { combineds: manifest.combineds } : {}),
|