@ai-matrx/alchemy 0.9.1 → 0.10.1
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/CHANGELOG.md +33 -0
- package/README.md +1 -0
- package/dist/actions.cjs +1 -1
- package/dist/actions.cjs.map +1 -1
- package/dist/actions.d.cts +166 -2
- package/dist/actions.d.ts +166 -2
- package/dist/actions.js +1 -1
- package/dist/actions.js.map +1 -1
- package/dist/core.cjs +1 -1
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +143 -14
- package/dist/core.d.ts +143 -14
- package/dist/core.js +2 -2
- package/dist/core.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +143 -14
- package/dist/index.d.ts +143 -14
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/menu.d.cts +153 -1
- package/dist/menu.d.ts +153 -1
- package/dist/operate-server.d.cts +5 -1
- package/dist/operate-server.d.ts +5 -1
- package/dist/operate.cjs +1 -1
- package/dist/operate.cjs.map +1 -1
- package/dist/operate.d.cts +143 -14
- package/dist/operate.d.ts +143 -14
- package/dist/operate.js +2 -2
- package/dist/operate.js.map +1 -1
- package/dist/ports.cjs.map +1 -1
- package/dist/ports.d.cts +154 -2
- package/dist/ports.d.ts +154 -2
- package/dist/react-host.d.cts +153 -1
- package/dist/react-host.d.ts +153 -1
- package/dist/react-overflow.d.cts +153 -1
- package/dist/react-overflow.d.ts +153 -1
- package/package.json +1 -1
package/dist/actions.d.cts
CHANGED
|
@@ -250,6 +250,148 @@ interface ClickTarget {
|
|
|
250
250
|
host?: unknown;
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Operate — the format and delivery contract (Matrx Alchemy CONTRACT §2.3, ALC-16).
|
|
255
|
+
*
|
|
256
|
+
* A format is an ADAPTER: `supports` is synchronous and loads nothing; `load` dynamically
|
|
257
|
+
* imports the engine the first time the format is chosen (print for rich-html/html/pdf/docx/
|
|
258
|
+
* epub, SheetJS for xlsx). Delivery accepts only a SEALED artifact, so the bytes previewed are
|
|
259
|
+
* the bytes copied, downloaded or sent.
|
|
260
|
+
*/
|
|
261
|
+
|
|
262
|
+
/** A reference to a declared write target (resolved by the one write door, ALC-17). */
|
|
263
|
+
interface WriteTargetRef {
|
|
264
|
+
surfaceName?: string;
|
|
265
|
+
target: string;
|
|
266
|
+
item?: {
|
|
267
|
+
itemType: string;
|
|
268
|
+
identity: Record<string, string>;
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Every write returns one (CONTRACT §2.3; the door is `createWriteDoor`, ALC-17). An applied
|
|
273
|
+
* receipt may carry a `warning` with its `fix` (rule 10: a person's kind mismatch warns, never refuses).
|
|
274
|
+
* A refusal's `reason` is a `WriteRefusalReason` (`./door`).
|
|
275
|
+
*/
|
|
276
|
+
type Receipt = {
|
|
277
|
+
status: "applied";
|
|
278
|
+
to: WriteTargetRef;
|
|
279
|
+
sentence: string;
|
|
280
|
+
warning?: string;
|
|
281
|
+
fix?: string;
|
|
282
|
+
} | {
|
|
283
|
+
status: "queued";
|
|
284
|
+
to: WriteTargetRef;
|
|
285
|
+
sentence: string;
|
|
286
|
+
remedy: string;
|
|
287
|
+
} | {
|
|
288
|
+
status: "refused";
|
|
289
|
+
to: WriteTargetRef;
|
|
290
|
+
reason: string;
|
|
291
|
+
sentence: string;
|
|
292
|
+
remedy: string;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* THE ONE WRITE DOOR (Matrx Alchemy ALC-17; Applets CONTRACTS §6 "receipts through the one write door").
|
|
297
|
+
*
|
|
298
|
+
* Every write — a person's edit, a menu row, a destination send, an agent, an applet, an automation,
|
|
299
|
+
* an Action projected from a write target — lands through `door.write`. The door:
|
|
300
|
+
*
|
|
301
|
+
* 1. resolves the DECLARED write target (the surface's own, then its ancestors'); an undeclared
|
|
302
|
+
* target is refused by name;
|
|
303
|
+
* 2. enforces the mode: `stream` takes ops from the target's `streamOps`; ops on any other target
|
|
304
|
+
* need `patchable` (+ `updatesValue`); a value on a `stream` target is refused;
|
|
305
|
+
* 3. checks the kind (rule 10): a person's mismatch WARNS (the receipt is still applied, with a
|
|
306
|
+
* `warning` and a `fix`); an agent's, applet's or automation's malformed write is REFUSED;
|
|
307
|
+
* 4. applies the policy: manual → a person only; ask → a person's own act is the approval, anyone
|
|
308
|
+
* else asks through `approvals`; queued → filed through `review`; auto → applies;
|
|
309
|
+
* 5. applies through the handler: the mounted page's LIVE handler wins over the HEADLESS one;
|
|
310
|
+
* neither → refused `unapplicable` ("open <surface> to apply it");
|
|
311
|
+
* 6. turns stale / missing record / broken protected span into "cannot apply" with a one-click
|
|
312
|
+
* remedy (rule 11).
|
|
313
|
+
*
|
|
314
|
+
* Every outcome is a `Receipt` carrying the system's own sentence. `write` never throws.
|
|
315
|
+
*
|
|
316
|
+
* Who counts as a person: `person`, `menu` and `destination` (a destination send is a person's own
|
|
317
|
+
* click on "Send to …"). `agent`, `applet` and `automation` are not.
|
|
318
|
+
*
|
|
319
|
+
* Open-risk ruling — stream ops into an entity target: an entity write is ONE atomic save. Ops reach
|
|
320
|
+
* an entity target only when it is declared `patchable` (the ops are handed to the handler in one
|
|
321
|
+
* call and commit as one save); a non-patchable entity target refuses ops with the remedy "stream
|
|
322
|
+
* into the editor, then save". Op-by-op persistence into a record does not exist.
|
|
323
|
+
*/
|
|
324
|
+
|
|
325
|
+
/** Who is writing. `menu` and `destination` are a person's own act. */
|
|
326
|
+
type WriteCaller = "person" | "agent" | "applet" | "automation" | "destination" | "menu";
|
|
327
|
+
/** One text op for a `stream` (or `patchable`) target — the WidgetHandle op set. */
|
|
328
|
+
interface WriteOp {
|
|
329
|
+
op: StreamOp;
|
|
330
|
+
/** The text the op inserts or replaces with. */
|
|
331
|
+
text?: string;
|
|
332
|
+
/** What `replace` / `insert_before` / `insert_after` / `patch` anchor on. */
|
|
333
|
+
find?: string;
|
|
334
|
+
}
|
|
335
|
+
interface WriteRequest {
|
|
336
|
+
surfaceName: string;
|
|
337
|
+
/** A write target name; `"<itemType>.<name>"` names an item's target. */
|
|
338
|
+
target: string;
|
|
339
|
+
itemType?: string;
|
|
340
|
+
/** Which item, by its declared identity values. Required for an item target. */
|
|
341
|
+
itemIdentity?: Record<string, string>;
|
|
342
|
+
/** A whole value. Exactly one of `value` / `ops`. */
|
|
343
|
+
value?: unknown;
|
|
344
|
+
/** Text ops (stream / patchable targets). */
|
|
345
|
+
ops?: readonly WriteOp[];
|
|
346
|
+
by: WriteCaller;
|
|
347
|
+
runId?: string;
|
|
348
|
+
}
|
|
349
|
+
/** What a handler receives: the request plus the declared target it resolved to. */
|
|
350
|
+
interface WriteApplyRequest {
|
|
351
|
+
surfaceName: string;
|
|
352
|
+
/** The surface that declares the target (an ancestor when inherited). */
|
|
353
|
+
declaredOn: string;
|
|
354
|
+
target: WriteTarget;
|
|
355
|
+
item?: {
|
|
356
|
+
itemType: string;
|
|
357
|
+
identity: Record<string, string>;
|
|
358
|
+
};
|
|
359
|
+
value?: unknown;
|
|
360
|
+
ops?: readonly WriteOp[];
|
|
361
|
+
by: WriteCaller;
|
|
362
|
+
runId?: string;
|
|
363
|
+
}
|
|
364
|
+
/** Rule 11: the three "cannot apply" signals. */
|
|
365
|
+
type CannotApply = "stale" | "missing-record" | "broken-span";
|
|
366
|
+
/**
|
|
367
|
+
* A handler applies the write. Return nothing (or `{ status: "applied" }`) when it landed, or a
|
|
368
|
+
* `cannotApply` signal; THROWING an error whose `code` is a `CannotApply` signal means the same.
|
|
369
|
+
*/
|
|
370
|
+
type WriteHandlerResult = void | {
|
|
371
|
+
status: "applied";
|
|
372
|
+
sentence?: string;
|
|
373
|
+
} | {
|
|
374
|
+
status: "cannot-apply";
|
|
375
|
+
cannotApply: CannotApply;
|
|
376
|
+
sentence?: string;
|
|
377
|
+
remedy?: string;
|
|
378
|
+
};
|
|
379
|
+
type WriteHandler = (request: WriteApplyRequest, signal: AbortSignal) => WriteHandlerResult | Promise<WriteHandlerResult>;
|
|
380
|
+
/** Files a `queued` write for an organization's review. */
|
|
381
|
+
interface WriteReviewPort {
|
|
382
|
+
file(request: WriteApplyRequest): Promise<{
|
|
383
|
+
sentence?: string;
|
|
384
|
+
remedy?: string;
|
|
385
|
+
} | void>;
|
|
386
|
+
}
|
|
387
|
+
interface WriteDoor {
|
|
388
|
+
write(request: WriteRequest, signal?: AbortSignal): Promise<Receipt>;
|
|
389
|
+
/** A feature's write handler that works with no page mounted. Refuses a second one for the same target. */
|
|
390
|
+
registerHeadless(surfaceName: string, targetName: string, handler: WriteHandler): () => void;
|
|
391
|
+
/** A mounted page's handler; wins over the headless one while registered. Returns unregister. */
|
|
392
|
+
registerLive(surfaceName: string, targetName: string, handler: WriteHandler): () => void;
|
|
393
|
+
}
|
|
394
|
+
|
|
253
395
|
/**
|
|
254
396
|
* Surface Actions — the shapes (Applets CONTRACTS §6, FROZEN; exported here as a superset).
|
|
255
397
|
*
|
|
@@ -336,6 +478,8 @@ interface ActionInfo {
|
|
|
336
478
|
target?: string;
|
|
337
479
|
updatesValue?: string;
|
|
338
480
|
itemType?: string;
|
|
481
|
+
/** Where it came from. "write-target" Actions go through the write door when one is bound (ALC-17). */
|
|
482
|
+
source?: ActionOrigin["source"];
|
|
339
483
|
}
|
|
340
484
|
interface ActionContext {
|
|
341
485
|
runId: string;
|
|
@@ -350,6 +494,8 @@ interface ActionError {
|
|
|
350
494
|
message: string;
|
|
351
495
|
retryable: boolean;
|
|
352
496
|
diagnostic?: string;
|
|
497
|
+
/** Superset: the one-click remedy a write door refusal names. */
|
|
498
|
+
remedy?: string;
|
|
353
499
|
}
|
|
354
500
|
/** CONTRACTS §0 `Result<T>`. */
|
|
355
501
|
type ActionResult<T = unknown> = {
|
|
@@ -424,7 +570,7 @@ interface SurfacePort {
|
|
|
424
570
|
* PP-01a `window` (host-rendered windows for a live preparation session)
|
|
425
571
|
* PP-13a `transferKnobs` (organization default recipes)
|
|
426
572
|
*
|
|
427
|
-
*
|
|
573
|
+
* ALC-17 `door` (the one write door), `review` (queued writes)
|
|
428
574
|
*
|
|
429
575
|
* ── Absent-port semantics ───────────────────────────────────────────────────
|
|
430
576
|
*
|
|
@@ -460,6 +606,10 @@ interface SurfacePort {
|
|
|
460
606
|
* approvals absent → a policy "ask" Action started by an agent, applet or automation is refused
|
|
461
607
|
* ("needs a person's approval"); a person's own click is the approval
|
|
462
608
|
* (`runs: "navigate"` goes through `overlay.navigate`; overlay absent → refused)
|
|
609
|
+
* door absent → write-target Actions dispatch to `pageActions` as before, and
|
|
610
|
+
* `Delivery.send` is refused with a remedy; bound → every such write goes
|
|
611
|
+
* through it and returns its receipt
|
|
612
|
+
* review absent → a write to a `queued` target is refused ("review isn't available here")
|
|
463
613
|
*
|
|
464
614
|
* Types only: this entry has no runtime code.
|
|
465
615
|
*/
|
|
@@ -613,6 +763,10 @@ interface AlchemyHostPorts {
|
|
|
613
763
|
pageActions?: PageActionPort;
|
|
614
764
|
/** Asks a person, in place, to approve an `ask` Action that an agent, applet or automation started. */
|
|
615
765
|
approvals?: ApprovalPort;
|
|
766
|
+
/** `createWriteDoor(...)`: every write to a declared target lands here and returns a receipt. */
|
|
767
|
+
door?: WriteDoor;
|
|
768
|
+
/** Files a write to a `queued` target for an organization's review. */
|
|
769
|
+
review?: WriteReviewPort;
|
|
616
770
|
}
|
|
617
771
|
interface ServerActionPort {
|
|
618
772
|
run(request: ServerActionRunRequest): Promise<ServerActionRunResponse>;
|
|
@@ -1146,15 +1300,25 @@ declare function resolveActionChain(chain: readonly ActionChainLink[], options?:
|
|
|
1146
1300
|
* A port the host did not bind means that capability is ABSENT: the run is
|
|
1147
1301
|
* refused with a sentence, never stubbed (ports rule).
|
|
1148
1302
|
*
|
|
1303
|
+
* Write targets (ALC-17): an Action projected from a declared write target (`source: "write-target"`)
|
|
1304
|
+
* goes through `ports.door.write` when a door is bound — the door applies the target's own policy
|
|
1305
|
+
* (manual / ask / queued / auto) and its receipt becomes the result (`applied` / `queued` → ok with
|
|
1306
|
+
* the receipt; refused → an error whose code is the refusal reason and whose `remedy` is the door's).
|
|
1307
|
+
* Without a door it dispatches to `pageActions` as before.
|
|
1308
|
+
*
|
|
1149
1309
|
* Never throws: every outcome is a `Result` whose error message is the
|
|
1150
1310
|
* system's own sentence.
|
|
1151
1311
|
*/
|
|
1152
1312
|
|
|
1153
|
-
type SurfaceActionPorts = Pick<AlchemyHostPorts, "diagnostics" | "serverActions" | "mandates" | "pageActions" | "overlay" | "approvals">;
|
|
1313
|
+
type SurfaceActionPorts = Pick<AlchemyHostPorts, "diagnostics" | "serverActions" | "mandates" | "pageActions" | "overlay" | "approvals" | "door">;
|
|
1154
1314
|
interface RunSurfaceActionOptions {
|
|
1155
1315
|
runId?: string;
|
|
1156
1316
|
viewerId?: string | null;
|
|
1157
1317
|
signal?: AbortSignal;
|
|
1318
|
+
/** Which item a write-target Action writes into (by its declared identity values). */
|
|
1319
|
+
itemIdentity?: Record<string, string>;
|
|
1320
|
+
/** Text edits for a stream or patchable write target, instead of the input as the whole value. */
|
|
1321
|
+
ops?: readonly WriteOp[];
|
|
1158
1322
|
}
|
|
1159
1323
|
/** Fill `{param}` placeholders from the input object. Returns the names it could not fill. */
|
|
1160
1324
|
declare function fillActionTarget(template: string, input: unknown): {
|
package/dist/actions.d.ts
CHANGED
|
@@ -250,6 +250,148 @@ interface ClickTarget {
|
|
|
250
250
|
host?: unknown;
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Operate — the format and delivery contract (Matrx Alchemy CONTRACT §2.3, ALC-16).
|
|
255
|
+
*
|
|
256
|
+
* A format is an ADAPTER: `supports` is synchronous and loads nothing; `load` dynamically
|
|
257
|
+
* imports the engine the first time the format is chosen (print for rich-html/html/pdf/docx/
|
|
258
|
+
* epub, SheetJS for xlsx). Delivery accepts only a SEALED artifact, so the bytes previewed are
|
|
259
|
+
* the bytes copied, downloaded or sent.
|
|
260
|
+
*/
|
|
261
|
+
|
|
262
|
+
/** A reference to a declared write target (resolved by the one write door, ALC-17). */
|
|
263
|
+
interface WriteTargetRef {
|
|
264
|
+
surfaceName?: string;
|
|
265
|
+
target: string;
|
|
266
|
+
item?: {
|
|
267
|
+
itemType: string;
|
|
268
|
+
identity: Record<string, string>;
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Every write returns one (CONTRACT §2.3; the door is `createWriteDoor`, ALC-17). An applied
|
|
273
|
+
* receipt may carry a `warning` with its `fix` (rule 10: a person's kind mismatch warns, never refuses).
|
|
274
|
+
* A refusal's `reason` is a `WriteRefusalReason` (`./door`).
|
|
275
|
+
*/
|
|
276
|
+
type Receipt = {
|
|
277
|
+
status: "applied";
|
|
278
|
+
to: WriteTargetRef;
|
|
279
|
+
sentence: string;
|
|
280
|
+
warning?: string;
|
|
281
|
+
fix?: string;
|
|
282
|
+
} | {
|
|
283
|
+
status: "queued";
|
|
284
|
+
to: WriteTargetRef;
|
|
285
|
+
sentence: string;
|
|
286
|
+
remedy: string;
|
|
287
|
+
} | {
|
|
288
|
+
status: "refused";
|
|
289
|
+
to: WriteTargetRef;
|
|
290
|
+
reason: string;
|
|
291
|
+
sentence: string;
|
|
292
|
+
remedy: string;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* THE ONE WRITE DOOR (Matrx Alchemy ALC-17; Applets CONTRACTS §6 "receipts through the one write door").
|
|
297
|
+
*
|
|
298
|
+
* Every write — a person's edit, a menu row, a destination send, an agent, an applet, an automation,
|
|
299
|
+
* an Action projected from a write target — lands through `door.write`. The door:
|
|
300
|
+
*
|
|
301
|
+
* 1. resolves the DECLARED write target (the surface's own, then its ancestors'); an undeclared
|
|
302
|
+
* target is refused by name;
|
|
303
|
+
* 2. enforces the mode: `stream` takes ops from the target's `streamOps`; ops on any other target
|
|
304
|
+
* need `patchable` (+ `updatesValue`); a value on a `stream` target is refused;
|
|
305
|
+
* 3. checks the kind (rule 10): a person's mismatch WARNS (the receipt is still applied, with a
|
|
306
|
+
* `warning` and a `fix`); an agent's, applet's or automation's malformed write is REFUSED;
|
|
307
|
+
* 4. applies the policy: manual → a person only; ask → a person's own act is the approval, anyone
|
|
308
|
+
* else asks through `approvals`; queued → filed through `review`; auto → applies;
|
|
309
|
+
* 5. applies through the handler: the mounted page's LIVE handler wins over the HEADLESS one;
|
|
310
|
+
* neither → refused `unapplicable` ("open <surface> to apply it");
|
|
311
|
+
* 6. turns stale / missing record / broken protected span into "cannot apply" with a one-click
|
|
312
|
+
* remedy (rule 11).
|
|
313
|
+
*
|
|
314
|
+
* Every outcome is a `Receipt` carrying the system's own sentence. `write` never throws.
|
|
315
|
+
*
|
|
316
|
+
* Who counts as a person: `person`, `menu` and `destination` (a destination send is a person's own
|
|
317
|
+
* click on "Send to …"). `agent`, `applet` and `automation` are not.
|
|
318
|
+
*
|
|
319
|
+
* Open-risk ruling — stream ops into an entity target: an entity write is ONE atomic save. Ops reach
|
|
320
|
+
* an entity target only when it is declared `patchable` (the ops are handed to the handler in one
|
|
321
|
+
* call and commit as one save); a non-patchable entity target refuses ops with the remedy "stream
|
|
322
|
+
* into the editor, then save". Op-by-op persistence into a record does not exist.
|
|
323
|
+
*/
|
|
324
|
+
|
|
325
|
+
/** Who is writing. `menu` and `destination` are a person's own act. */
|
|
326
|
+
type WriteCaller = "person" | "agent" | "applet" | "automation" | "destination" | "menu";
|
|
327
|
+
/** One text op for a `stream` (or `patchable`) target — the WidgetHandle op set. */
|
|
328
|
+
interface WriteOp {
|
|
329
|
+
op: StreamOp;
|
|
330
|
+
/** The text the op inserts or replaces with. */
|
|
331
|
+
text?: string;
|
|
332
|
+
/** What `replace` / `insert_before` / `insert_after` / `patch` anchor on. */
|
|
333
|
+
find?: string;
|
|
334
|
+
}
|
|
335
|
+
interface WriteRequest {
|
|
336
|
+
surfaceName: string;
|
|
337
|
+
/** A write target name; `"<itemType>.<name>"` names an item's target. */
|
|
338
|
+
target: string;
|
|
339
|
+
itemType?: string;
|
|
340
|
+
/** Which item, by its declared identity values. Required for an item target. */
|
|
341
|
+
itemIdentity?: Record<string, string>;
|
|
342
|
+
/** A whole value. Exactly one of `value` / `ops`. */
|
|
343
|
+
value?: unknown;
|
|
344
|
+
/** Text ops (stream / patchable targets). */
|
|
345
|
+
ops?: readonly WriteOp[];
|
|
346
|
+
by: WriteCaller;
|
|
347
|
+
runId?: string;
|
|
348
|
+
}
|
|
349
|
+
/** What a handler receives: the request plus the declared target it resolved to. */
|
|
350
|
+
interface WriteApplyRequest {
|
|
351
|
+
surfaceName: string;
|
|
352
|
+
/** The surface that declares the target (an ancestor when inherited). */
|
|
353
|
+
declaredOn: string;
|
|
354
|
+
target: WriteTarget;
|
|
355
|
+
item?: {
|
|
356
|
+
itemType: string;
|
|
357
|
+
identity: Record<string, string>;
|
|
358
|
+
};
|
|
359
|
+
value?: unknown;
|
|
360
|
+
ops?: readonly WriteOp[];
|
|
361
|
+
by: WriteCaller;
|
|
362
|
+
runId?: string;
|
|
363
|
+
}
|
|
364
|
+
/** Rule 11: the three "cannot apply" signals. */
|
|
365
|
+
type CannotApply = "stale" | "missing-record" | "broken-span";
|
|
366
|
+
/**
|
|
367
|
+
* A handler applies the write. Return nothing (or `{ status: "applied" }`) when it landed, or a
|
|
368
|
+
* `cannotApply` signal; THROWING an error whose `code` is a `CannotApply` signal means the same.
|
|
369
|
+
*/
|
|
370
|
+
type WriteHandlerResult = void | {
|
|
371
|
+
status: "applied";
|
|
372
|
+
sentence?: string;
|
|
373
|
+
} | {
|
|
374
|
+
status: "cannot-apply";
|
|
375
|
+
cannotApply: CannotApply;
|
|
376
|
+
sentence?: string;
|
|
377
|
+
remedy?: string;
|
|
378
|
+
};
|
|
379
|
+
type WriteHandler = (request: WriteApplyRequest, signal: AbortSignal) => WriteHandlerResult | Promise<WriteHandlerResult>;
|
|
380
|
+
/** Files a `queued` write for an organization's review. */
|
|
381
|
+
interface WriteReviewPort {
|
|
382
|
+
file(request: WriteApplyRequest): Promise<{
|
|
383
|
+
sentence?: string;
|
|
384
|
+
remedy?: string;
|
|
385
|
+
} | void>;
|
|
386
|
+
}
|
|
387
|
+
interface WriteDoor {
|
|
388
|
+
write(request: WriteRequest, signal?: AbortSignal): Promise<Receipt>;
|
|
389
|
+
/** A feature's write handler that works with no page mounted. Refuses a second one for the same target. */
|
|
390
|
+
registerHeadless(surfaceName: string, targetName: string, handler: WriteHandler): () => void;
|
|
391
|
+
/** A mounted page's handler; wins over the headless one while registered. Returns unregister. */
|
|
392
|
+
registerLive(surfaceName: string, targetName: string, handler: WriteHandler): () => void;
|
|
393
|
+
}
|
|
394
|
+
|
|
253
395
|
/**
|
|
254
396
|
* Surface Actions — the shapes (Applets CONTRACTS §6, FROZEN; exported here as a superset).
|
|
255
397
|
*
|
|
@@ -336,6 +478,8 @@ interface ActionInfo {
|
|
|
336
478
|
target?: string;
|
|
337
479
|
updatesValue?: string;
|
|
338
480
|
itemType?: string;
|
|
481
|
+
/** Where it came from. "write-target" Actions go through the write door when one is bound (ALC-17). */
|
|
482
|
+
source?: ActionOrigin["source"];
|
|
339
483
|
}
|
|
340
484
|
interface ActionContext {
|
|
341
485
|
runId: string;
|
|
@@ -350,6 +494,8 @@ interface ActionError {
|
|
|
350
494
|
message: string;
|
|
351
495
|
retryable: boolean;
|
|
352
496
|
diagnostic?: string;
|
|
497
|
+
/** Superset: the one-click remedy a write door refusal names. */
|
|
498
|
+
remedy?: string;
|
|
353
499
|
}
|
|
354
500
|
/** CONTRACTS §0 `Result<T>`. */
|
|
355
501
|
type ActionResult<T = unknown> = {
|
|
@@ -424,7 +570,7 @@ interface SurfacePort {
|
|
|
424
570
|
* PP-01a `window` (host-rendered windows for a live preparation session)
|
|
425
571
|
* PP-13a `transferKnobs` (organization default recipes)
|
|
426
572
|
*
|
|
427
|
-
*
|
|
573
|
+
* ALC-17 `door` (the one write door), `review` (queued writes)
|
|
428
574
|
*
|
|
429
575
|
* ── Absent-port semantics ───────────────────────────────────────────────────
|
|
430
576
|
*
|
|
@@ -460,6 +606,10 @@ interface SurfacePort {
|
|
|
460
606
|
* approvals absent → a policy "ask" Action started by an agent, applet or automation is refused
|
|
461
607
|
* ("needs a person's approval"); a person's own click is the approval
|
|
462
608
|
* (`runs: "navigate"` goes through `overlay.navigate`; overlay absent → refused)
|
|
609
|
+
* door absent → write-target Actions dispatch to `pageActions` as before, and
|
|
610
|
+
* `Delivery.send` is refused with a remedy; bound → every such write goes
|
|
611
|
+
* through it and returns its receipt
|
|
612
|
+
* review absent → a write to a `queued` target is refused ("review isn't available here")
|
|
463
613
|
*
|
|
464
614
|
* Types only: this entry has no runtime code.
|
|
465
615
|
*/
|
|
@@ -613,6 +763,10 @@ interface AlchemyHostPorts {
|
|
|
613
763
|
pageActions?: PageActionPort;
|
|
614
764
|
/** Asks a person, in place, to approve an `ask` Action that an agent, applet or automation started. */
|
|
615
765
|
approvals?: ApprovalPort;
|
|
766
|
+
/** `createWriteDoor(...)`: every write to a declared target lands here and returns a receipt. */
|
|
767
|
+
door?: WriteDoor;
|
|
768
|
+
/** Files a write to a `queued` target for an organization's review. */
|
|
769
|
+
review?: WriteReviewPort;
|
|
616
770
|
}
|
|
617
771
|
interface ServerActionPort {
|
|
618
772
|
run(request: ServerActionRunRequest): Promise<ServerActionRunResponse>;
|
|
@@ -1146,15 +1300,25 @@ declare function resolveActionChain(chain: readonly ActionChainLink[], options?:
|
|
|
1146
1300
|
* A port the host did not bind means that capability is ABSENT: the run is
|
|
1147
1301
|
* refused with a sentence, never stubbed (ports rule).
|
|
1148
1302
|
*
|
|
1303
|
+
* Write targets (ALC-17): an Action projected from a declared write target (`source: "write-target"`)
|
|
1304
|
+
* goes through `ports.door.write` when a door is bound — the door applies the target's own policy
|
|
1305
|
+
* (manual / ask / queued / auto) and its receipt becomes the result (`applied` / `queued` → ok with
|
|
1306
|
+
* the receipt; refused → an error whose code is the refusal reason and whose `remedy` is the door's).
|
|
1307
|
+
* Without a door it dispatches to `pageActions` as before.
|
|
1308
|
+
*
|
|
1149
1309
|
* Never throws: every outcome is a `Result` whose error message is the
|
|
1150
1310
|
* system's own sentence.
|
|
1151
1311
|
*/
|
|
1152
1312
|
|
|
1153
|
-
type SurfaceActionPorts = Pick<AlchemyHostPorts, "diagnostics" | "serverActions" | "mandates" | "pageActions" | "overlay" | "approvals">;
|
|
1313
|
+
type SurfaceActionPorts = Pick<AlchemyHostPorts, "diagnostics" | "serverActions" | "mandates" | "pageActions" | "overlay" | "approvals" | "door">;
|
|
1154
1314
|
interface RunSurfaceActionOptions {
|
|
1155
1315
|
runId?: string;
|
|
1156
1316
|
viewerId?: string | null;
|
|
1157
1317
|
signal?: AbortSignal;
|
|
1318
|
+
/** Which item a write-target Action writes into (by its declared identity values). */
|
|
1319
|
+
itemIdentity?: Record<string, string>;
|
|
1320
|
+
/** Text edits for a stream or patchable write target, instead of the input as the whole value. */
|
|
1321
|
+
ops?: readonly WriteOp[];
|
|
1158
1322
|
}
|
|
1159
1323
|
/** Fill `{param}` placeholders from the input object. Returns the names it could not fill. */
|
|
1160
1324
|
declare function fillActionTarget(template: string, input: unknown): {
|
package/dist/actions.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var UNIVERSAL_VERBS=["copy","cut","paste","undo","redo","find"];var ACTION_CATEGORIES=["feedback","copy","export","save","edit","share","creator","app","listen","study","ask","ai","admin","clipboard","history","surface-info","applet"];var DuplicateActionError=class extends Error{actionId;providers;constructor(actionId,first,second){super(first===second?`Action "${actionId}" is yielded twice by provider "${first}". Ids are unique; the second was refused.`:`Action "${actionId}" is registered by provider "${first}" and again by "${second}". Ids are unique; "${second}"'s copy was refused.`),this.name="DuplicateActionError",this.actionId=actionId,this.providers=[first,second]}},DuplicateProviderError=class extends Error{providerId;constructor(providerId){super(`Action provider "${providerId}" is already registered. Unregister it first; nothing was overwritten.`),this.name="DuplicateProviderError",this.providerId=providerId}},VERBS=new Set(UNIVERSAL_VERBS);function capture(ports,error,detail){try{ports.diagnostics.capture(error,{area:"actions",detail})}catch{}}function computeEligibility(action,target,options){let restrict=options.restrict;if(target.excludedActionIds.has(action.id))return{status:"absent"};if(restrict?.exclude?.includes(action.id))return{status:"absent"};if(restrict?.categories&&!restrict.categories.includes(action.category))return{status:"absent"};if(action.writes&&action.writes.length>0){if(target.readOnly)return{status:"absent"};let writable=new Set(target.writable.map(w=>w.name));if(!action.writes.some(name=>writable.has(name)))return{status:"absent"}}if(action.requiresAuth&&!target.auth.authenticated&&!options.authResumable)return{status:"absent"};let own=action.eligible(target);return own.status==="unavailable"?own.sentence.trim()?own:{status:"absent"}:own.status!=="unavailable-verb"?own:action.category!=="clipboard"||!VERBS.has(own.verb)?{status:"contract-break",reason:`Action "${action.id}" answered unavailable-verb but only the six universal verbs (category "clipboard") may grey; it is absent instead.`}:own.sentence.trim()?own:{status:"contract-break",reason:`Universal verb "${action.id}" greyed without a sentence; a greyed verb must say why.`}}function createActionRegistry(options){let ports=options.ports,entries=new Map,listeners=new Set,notify=()=>{for(let l of[...listeners])l()};return{register(provider){if(entries.has(provider.id))throw new DuplicateProviderError(provider.id);let declared=provider.declaredIds?.()??[],own=new Set;for(let id of declared){if(own.has(id))throw new DuplicateActionError(id,provider.id,provider.id);own.add(id);for(let[otherId,other]of entries)if(other.declared.includes(id))throw new DuplicateActionError(id,otherId,provider.id)}let entry={provider,declared};return entries.set(provider.id,entry),notify(),()=>{entries.get(provider.id)===entry&&(entries.delete(provider.id),notify())}},async resolve(target,restrict){let authResumable=!!ports.auth,seen=new Map,out=[];for(let[providerId,{provider}]of[...entries]){let actions;try{actions=await provider.actions(target)}catch(error){capture(ports,error,{providerId,stage:"actions"});continue}for(let raw of actions){let first=seen.get(raw.id);if(first!==void 0){capture(ports,new DuplicateActionError(raw.id,first,providerId),{actionId:raw.id});continue}seen.set(raw.id,providerId);let action=raw.provider===providerId?raw:{...raw,provider:providerId},eligibility;try{eligibility=computeEligibility(action,target,{restrict:restrict??{},authResumable})}catch(error){capture(ports,error,{actionId:action.id,stage:"eligible"});continue}if(eligibility.status==="contract-break"){capture(ports,new Error(eligibility.reason),{actionId:action.id,stage:"eligible"});continue}eligibility.status!=="absent"&&out.push({action,eligibility})}}return out},providers(){return[...entries.keys()]},subscribe(listener){return listeners.add(listener),()=>{listeners.delete(listener)}}}}function staticActionProvider(id,actions,tier="T0"){let list=actions.map(a=>a.provider===id?a:{...a,provider:id});return{id,tier,declaredIds:()=>list.map(a=>a.id),actions:()=>list}}var alwaysEligible=()=>({status:"available"});var DEFAULT_SENSITIVITY=Object.freeze({exportable:!0,classification:"ordinary",includedByDefault:!0});function resolveSensitivity(value){return{exportable:value.sensitivity?.exportable??DEFAULT_SENSITIVITY.exportable,classification:value.sensitivity?.classification??DEFAULT_SENSITIVITY.classification,includedByDefault:value.sensitivity?.includedByDefault??DEFAULT_SENSITIVITY.includedByDefault}}function toJson(value){if(!(value===void 0||typeof value=="function"||typeof value=="symbol")){if(value===null||typeof value=="string"||typeof value=="boolean")return value;if(typeof value=="number")return Number.isFinite(value)?value:null;if(typeof value=="bigint")return value.toString();if(value instanceof Date)return value.toISOString();if(Array.isArray(value))return value.map(v=>toJson(v)??null);if(typeof value=="object"){let out={};for(let[k,v]of Object.entries(value)){let j=toJson(v);j!==void 0&&(out[k]=j)}return out}}}function projectValues(raw,declared,pathPrefix=""){let byName=new Map(declared.map(d=>[d.name,d])),values={},omissions=[];for(let[name,value]of Object.entries(raw)){let path=pathPrefix?`${pathPrefix}.${name}`:name,decl=byName.get(name);if(!decl){omissions.push({path,reason:"filtered"});continue}let sensitivity=resolveSensitivity(decl);if(sensitivity.classification!=="ordinary"){omissions.push({path,reason:"excluded",classification:sensitivity.classification});continue}if(!sensitivity.exportable){omissions.push({path,reason:"excluded"});continue}let json=toJson(value);json!==void 0&&(values[name]=json)}return{values,omissions}}function createProjectedReads(source){return{async values(signal){if(!source.surface||!source.declaration)return{values:{},omissions:[]};let raw=await source.surface.readValues(signal),screenValues=source.declaration.values;return projectValues(raw,screenValues)},async item(signal){let item=source.item;if(!item||!source.declaration)return null;let type=source.declaration.itemTypes?.find(t=>t.name===item.itemType);if(!type)return{values:{},omissions:[{path:item.itemType,reason:"filtered"}]};let raw=await item.readValues(signal);return projectValues(raw,type.values,item.itemType)},async live(signal){return source.surface?.readLive?source.surface.readLive(signal):{}}}}function qualifyValueKey(itemType,name){return itemType?`${itemType}.${name}`:name}function domainOf(surfaceName){let slash=surfaceName.indexOf("/");return slash>0?surfaceName.slice(0,slash):surfaceName}function createClickTarget(input={}){let declaration=input.declaration??null,item=input.item??null,valueKeys=new Set,restricted=!1,writable=[];if(declaration){for(let value of declaration.values){valueKeys.add(value.name);let s=resolveSensitivity(value);(s.classification!=="ordinary"||!s.exportable)&&(restricted=!0)}let itemType=item?declaration.itemTypes?.find(t=>t.name===item.itemType):void 0;if(itemType){for(let value of itemType.values){valueKeys.add(qualifyValueKey(itemType.name,value.name));let s=resolveSensitivity(value);(s.classification!=="ordinary"||!s.exportable)&&(restricted=!0)}for(let t of itemType.writeTargets??[])writable.push(t)}for(let t of declaration.writeTargets??[])(!t.itemType||t.itemType===item?.itemType)&&writable.push(t)}let readOnly=input.readOnly??item?.readOnly??!1,surfaceName=input.surface?.surfaceName??declaration?.surfaceName??null,scopeChain=input.scopeChain??(surfaceName?["global",domainOf(surfaceName),surfaceName]:["global"]);return{surface:input.surface??null,declaration,valueKeys,scopeChain,excludedActionIds:new Set(input.excludedActionIds??[]),item,level:input.level??item?.level??null,selection:input.selection??null,payloadKinds:input.payloadKinds??[],writable:readOnly?[]:input.writable??writable,readOnly,restricted,organizationId:input.organizationId??null,auth:{authenticated:input.auth?.authenticated??!1,admin:input.auth?.admin??!1,creator:input.auth?.creator??!1},host:input.host}}async function runAction(action,target,options){let{ports}=options;if(action.requiresAuth&&!target.auth.authenticated)return ports.auth?(ports.auth.signInThenResume({actionId:action.id,contentFingerprint:options.contentFingerprint??action.id}),{status:"auth-pending"}):(ports.diagnostics.capture(new Error(`requiresAuth action "${action.id}" ran signed out with no auth port`),{area:"actions",detail:{actionId:action.id}}),{status:"failed",error:new Error("sign-in unavailable")});let reads=createProjectedReads(options.source??{surface:null,declaration:target.declaration,item:null}),signal=options.signal??new AbortController().signal;try{return{status:"ran",outcome:await action.run(target,{reads,ports,signal})}}catch(error){ports.diagnostics.capture(error,{area:"actions",detail:{actionId:action.id,stage:"run"}});let label=typeof action.label=="string"?action.label:action.id;return ports.notify?.error(`${label} did not finish.`,"Try again; if it keeps failing, report it from the feedback button."),{status:"failed",error}}}var DEFAULT_MAX_INHERITANCE_DEPTH=3;var APPLET_CATEGORY="applet",DEFAULT_TRIGGERED_BY=["person","agent","applet"],ACTION_CALLERS=["person","agent","applet","automation"],ACTION_RUNS=["page","server","mandate","navigate"],ACTION_POLICIES=["ask","auto","manual"];function compact(value){return Object.fromEntries(Object.entries(value).filter(([,v])=>v!==void 0))}var POLICY_OF_APPLY={manual:"manual",ask:"ask",auto:"auto",queued:"ask"};function writeTargetToAction(target){let applyPolicy=target.applyPolicy??"manual";return compact({name:target.name,label:target.label,description:target.description,input:target.valueKind??{jsonSchema:target.valueType==="document"?{}:{type:target.valueType}},runs:"page",triggeredBy:[...DEFAULT_TRIGGERED_BY],policy:POLICY_OF_APPLY[applyPolicy],updatesValue:target.updatesValue,mode:target.mode,patchable:target.patchable,approvalComparison:target.approvalComparison,streamOps:target.streamOps?[...target.streamOps]:void 0,destination:target.destination,groupKey:target.group,itemType:target.itemType,applyPolicy:target.applyPolicy,valueType:target.valueType,valueKind:target.valueKind,sortOrder:target.sortOrder,origin:{source:"write-target",declaredBy:"code"}})}function clientToolToAction(row){return compact({name:row.name,label:row.label,description:row.description,input:{jsonSchema:row.input_schema??{}},runs:"page",triggeredBy:["agent"],policy:row.mode==="draft"?"ask":"auto",mode:row.mode,origin:compact({source:"client-tool",surfaceName:row.surface_name,id:row.id,declaredBy:row.declared_by})})}function surfaceActionRowToAction(row){return compact({name:row.name,category:row.category,label:row.label,description:row.description,input:row.input??void 0,output:row.output??void 0,runs:row.runs,target:row.target??void 0,triggeredBy:[...row.triggered_by],policy:row.policy,updatesValue:row.updates_value??void 0,itemType:row.item_type||void 0,sortOrder:row.sort_order,origin:compact({source:"surface-action",surfaceName:row.surface_name,surfaceId:row.surface_id,id:row.id,declaredBy:row.declared_by,codeRef:row.code_ref??void 0})})}function toActionInfo(declaration,declaredOn){return compact({name:declaration.name,category:declaration.category??APPLET_CATEGORY,label:declaration.label,description:declaration.description,runs:declaration.runs,policy:declaration.policy,triggeredBy:[...declaration.triggeredBy],input:declaration.input,output:declaration.output,declaredOn,target:declaration.target,updatesValue:declaration.updatesValue,itemType:declaration.itemType})}function resolveActionChain(chain,options={}){let maxDepth=options.maxDepth??DEFAULT_MAX_INHERITANCE_DEPTH,names=chain.map(link=>link.surfaceName),seen=new Set;for(let name of names){if(seen.has(name))throw new Error(`[actions] inheritance CYCLE detected at "${name}". Fix the surface chain: ${names.join(" \u2192 ")}`);seen.add(name)}if(chain.length-1>maxDepth)throw new Error(`[actions] inheritance chain for "${names[names.length-1]}" exceeds depth ${maxDepth}: ${names.join(" \u2192 ")}`);let byName=new Map;for(let link of chain){let own=new Set;for(let action of link.actions){if(own.has(action.name))throw new Error(`[actions] surface "${link.surfaceName}" declares Action "${action.name}" twice. Names are unique; nothing was overwritten.`);own.add(action.name),byName.set(action.name,toActionInfo(action,link.surfaceName))}}return[...byName.values()]}function fail(code,message,retryable=!1,diagnostic){return{ok:!1,error:diagnostic?{code,message,retryable,diagnostic}:{code,message,retryable}}}function messageOf(error){return error instanceof Error&&error.message.trim()?error.message:typeof error=="string"&&error.trim()?error:"The run failed without saying why."}function newRunId(){let c=globalThis.crypto;return c?.randomUUID?c.randomUUID():`run-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,10)}`}function fillActionTarget(template,input){let source=input&&typeof input=="object"&&!Array.isArray(input)?input:{},missing=[];return{value:template.replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g,(_,key)=>{let v=source[key];return v==null||v===""||typeof v=="object"?(missing.push(key),""):encodeURIComponent(String(v))}),missing}}function fillName(template,input){let filled=fillActionTarget(template,input);return{value:decodeURIComponent(filled.value),missing:filled.missing}}function toJson2(value){return value===void 0?null:JSON.parse(JSON.stringify(value))}function refuseCaller(info,by){return info.triggeredBy.includes(by)?info.policy==="manual"&&by!=="person"?{code:"manual_only",message:`"${info.label}" (${info.name}) runs only when a person starts it.`,retryable:!1}:null:{code:"caller_not_allowed",message:`"${info.label}" (${info.name}) can't be started by ${CALLER_WORDS[by]}; it allows ${info.triggeredBy.join(", ")||"no one"}.`,retryable:!1}}var CALLER_WORDS={person:"a person",agent:"an agent",applet:"an applet",automation:"an automation"};function absent(info,capability){return fail("capability_absent",`"${info.label}" can't run here: ${capability} isn't available on this page.`)}async function runSurfaceAction(info,input,by,ports,options={}){let refusal=refuseCaller(info,by);if(refusal)return{ok:!1,error:refusal};let context={runId:options.runId??newRunId(),by,viewerId:options.viewerId??null,surfaceName:info.declaredOn,policy:info.policy},capture2=(error,stage)=>{try{ports.diagnostics.capture(error,{area:"actions",detail:{action:info.name,runs:info.runs,stage,runId:context.runId}})}catch{}};try{if(info.policy==="ask"&&by!=="person"&&info.runs!=="server"){if(!ports.approvals)return fail("needs_approval",`"${info.label}" needs a person's approval, and there is no one to ask here.`);if(!await ports.approvals.ask({action:info.name,label:info.label,description:info.description,input:toJson2(input),by,surfaceName:info.declaredOn}))return fail("declined",`"${info.label}" was not approved, so it did not run.`)}switch(info.runs){case"server":{if(!ports.serverActions)return absent(info,"server actions");let filled=fillName(info.target??info.name,input);if(filled.missing.length)return fail("input_missing",`"${info.label}" needs ${filled.missing.join(", ")}.`);let response=await ports.serverActions.run({name:filled.value,input:toJson2(input),by,surface_name:info.declaredOn,policy:info.policy});return response.status==="refused"?fail("refused",response.message):{ok:!0,data:response}}case"mandate":{if(!ports.mandates)return absent(info,"running a job");if(!info.target)return fail("target_missing",`"${info.label}" names no job to run.`);let filled=fillName(info.target,input);if(filled.missing.length)return fail("input_missing",`"${info.label}" needs ${filled.missing.join(", ")}.`);let record=input&&typeof input=="object"&&!Array.isArray(input)?input:{},variables=/\{[A-Za-z_]/.test(info.target)?record.variables&&typeof record.variables=="object"?toJson2(record.variables):{}:toJson2(record),userInput=typeof record.userInput=="string"?record.userInput:void 0;return{ok:!0,data:await ports.mandates.run({alias:filled.value,variables,...userInput!==void 0?{userInput}:{},by,policy:info.policy})}}case"navigate":{if(!ports.overlay)return absent(info,"navigation");let filled=fillActionTarget(info.target??"",input);if(filled.missing.length)return fail("input_missing",`"${info.label}" needs ${filled.missing.join(", ")}.`);let to=info.target&&/^\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(info.target)?decodeURIComponent(filled.value):filled.value;return to?(ports.overlay.navigate(to),{ok:!0,data:{to}}):fail("target_missing",`"${info.label}" names no page to open.`)}case"page":return ports.pageActions?await ports.pageActions.dispatch(info.name,input,context):absent(info,"this page's actions");default:{let runs=info.runs;return fail("runs_unknown",`"${info.label}" runs as "${String(runs)}", which nothing here knows how to run.`)}}}catch(error){return capture2(error,"run"),fail("run_failed",messageOf(error),!0)}}export{ACTION_CALLERS,ACTION_CATEGORIES,ACTION_POLICIES,ACTION_RUNS,APPLET_CATEGORY,DEFAULT_TRIGGERED_BY,DuplicateActionError,DuplicateProviderError,UNIVERSAL_VERBS,alwaysEligible,clientToolToAction,computeEligibility,createActionRegistry,createClickTarget,createProjectedReads,fillActionTarget,projectValues,refuseCaller,resolveActionChain,runAction,runSurfaceAction,staticActionProvider,surfaceActionRowToAction,toActionInfo,writeTargetToAction};
|
|
1
|
+
var UNIVERSAL_VERBS=["copy","cut","paste","undo","redo","find"];var ACTION_CATEGORIES=["feedback","copy","export","save","edit","share","creator","app","listen","study","ask","ai","admin","clipboard","history","surface-info","applet"];var DuplicateActionError=class extends Error{actionId;providers;constructor(actionId,first,second){super(first===second?`Action "${actionId}" is yielded twice by provider "${first}". Ids are unique; the second was refused.`:`Action "${actionId}" is registered by provider "${first}" and again by "${second}". Ids are unique; "${second}"'s copy was refused.`),this.name="DuplicateActionError",this.actionId=actionId,this.providers=[first,second]}},DuplicateProviderError=class extends Error{providerId;constructor(providerId){super(`Action provider "${providerId}" is already registered. Unregister it first; nothing was overwritten.`),this.name="DuplicateProviderError",this.providerId=providerId}},VERBS=new Set(UNIVERSAL_VERBS);function capture(ports,error,detail){try{ports.diagnostics.capture(error,{area:"actions",detail})}catch{}}function computeEligibility(action,target,options){let restrict=options.restrict;if(target.excludedActionIds.has(action.id))return{status:"absent"};if(restrict?.exclude?.includes(action.id))return{status:"absent"};if(restrict?.categories&&!restrict.categories.includes(action.category))return{status:"absent"};if(action.writes&&action.writes.length>0){if(target.readOnly)return{status:"absent"};let writable=new Set(target.writable.map(w=>w.name));if(!action.writes.some(name=>writable.has(name)))return{status:"absent"}}if(action.requiresAuth&&!target.auth.authenticated&&!options.authResumable)return{status:"absent"};let own=action.eligible(target);return own.status==="unavailable"?own.sentence.trim()?own:{status:"absent"}:own.status!=="unavailable-verb"?own:action.category!=="clipboard"||!VERBS.has(own.verb)?{status:"contract-break",reason:`Action "${action.id}" answered unavailable-verb but only the six universal verbs (category "clipboard") may grey; it is absent instead.`}:own.sentence.trim()?own:{status:"contract-break",reason:`Universal verb "${action.id}" greyed without a sentence; a greyed verb must say why.`}}function createActionRegistry(options){let ports=options.ports,entries=new Map,listeners=new Set,notify=()=>{for(let l of[...listeners])l()};return{register(provider){if(entries.has(provider.id))throw new DuplicateProviderError(provider.id);let declared=provider.declaredIds?.()??[],own=new Set;for(let id of declared){if(own.has(id))throw new DuplicateActionError(id,provider.id,provider.id);own.add(id);for(let[otherId,other]of entries)if(other.declared.includes(id))throw new DuplicateActionError(id,otherId,provider.id)}let entry={provider,declared};return entries.set(provider.id,entry),notify(),()=>{entries.get(provider.id)===entry&&(entries.delete(provider.id),notify())}},async resolve(target,restrict){let authResumable=!!ports.auth,seen=new Map,out=[];for(let[providerId,{provider}]of[...entries]){let actions;try{actions=await provider.actions(target)}catch(error){capture(ports,error,{providerId,stage:"actions"});continue}for(let raw of actions){let first=seen.get(raw.id);if(first!==void 0){capture(ports,new DuplicateActionError(raw.id,first,providerId),{actionId:raw.id});continue}seen.set(raw.id,providerId);let action=raw.provider===providerId?raw:{...raw,provider:providerId},eligibility;try{eligibility=computeEligibility(action,target,{restrict:restrict??{},authResumable})}catch(error){capture(ports,error,{actionId:action.id,stage:"eligible"});continue}if(eligibility.status==="contract-break"){capture(ports,new Error(eligibility.reason),{actionId:action.id,stage:"eligible"});continue}eligibility.status!=="absent"&&out.push({action,eligibility})}}return out},providers(){return[...entries.keys()]},subscribe(listener){return listeners.add(listener),()=>{listeners.delete(listener)}}}}function staticActionProvider(id,actions,tier="T0"){let list=actions.map(a=>a.provider===id?a:{...a,provider:id});return{id,tier,declaredIds:()=>list.map(a=>a.id),actions:()=>list}}var alwaysEligible=()=>({status:"available"});var DEFAULT_SENSITIVITY=Object.freeze({exportable:!0,classification:"ordinary",includedByDefault:!0});function resolveSensitivity(value){return{exportable:value.sensitivity?.exportable??DEFAULT_SENSITIVITY.exportable,classification:value.sensitivity?.classification??DEFAULT_SENSITIVITY.classification,includedByDefault:value.sensitivity?.includedByDefault??DEFAULT_SENSITIVITY.includedByDefault}}function toJson(value){if(!(value===void 0||typeof value=="function"||typeof value=="symbol")){if(value===null||typeof value=="string"||typeof value=="boolean")return value;if(typeof value=="number")return Number.isFinite(value)?value:null;if(typeof value=="bigint")return value.toString();if(value instanceof Date)return value.toISOString();if(Array.isArray(value))return value.map(v=>toJson(v)??null);if(typeof value=="object"){let out={};for(let[k,v]of Object.entries(value)){let j=toJson(v);j!==void 0&&(out[k]=j)}return out}}}function projectValues(raw,declared,pathPrefix=""){let byName=new Map(declared.map(d=>[d.name,d])),values={},omissions=[];for(let[name,value]of Object.entries(raw)){let path=pathPrefix?`${pathPrefix}.${name}`:name,decl=byName.get(name);if(!decl){omissions.push({path,reason:"filtered"});continue}let sensitivity=resolveSensitivity(decl);if(sensitivity.classification!=="ordinary"){omissions.push({path,reason:"excluded",classification:sensitivity.classification});continue}if(!sensitivity.exportable){omissions.push({path,reason:"excluded"});continue}let json=toJson(value);json!==void 0&&(values[name]=json)}return{values,omissions}}function createProjectedReads(source){return{async values(signal){if(!source.surface||!source.declaration)return{values:{},omissions:[]};let raw=await source.surface.readValues(signal),screenValues=source.declaration.values;return projectValues(raw,screenValues)},async item(signal){let item=source.item;if(!item||!source.declaration)return null;let type=source.declaration.itemTypes?.find(t=>t.name===item.itemType);if(!type)return{values:{},omissions:[{path:item.itemType,reason:"filtered"}]};let raw=await item.readValues(signal);return projectValues(raw,type.values,item.itemType)},async live(signal){return source.surface?.readLive?source.surface.readLive(signal):{}}}}function qualifyValueKey(itemType,name){return itemType?`${itemType}.${name}`:name}function domainOf(surfaceName){let slash=surfaceName.indexOf("/");return slash>0?surfaceName.slice(0,slash):surfaceName}function createClickTarget(input={}){let declaration=input.declaration??null,item=input.item??null,valueKeys=new Set,restricted=!1,writable=[];if(declaration){for(let value of declaration.values){valueKeys.add(value.name);let s=resolveSensitivity(value);(s.classification!=="ordinary"||!s.exportable)&&(restricted=!0)}let itemType=item?declaration.itemTypes?.find(t=>t.name===item.itemType):void 0;if(itemType){for(let value of itemType.values){valueKeys.add(qualifyValueKey(itemType.name,value.name));let s=resolveSensitivity(value);(s.classification!=="ordinary"||!s.exportable)&&(restricted=!0)}for(let t of itemType.writeTargets??[])writable.push(t)}for(let t of declaration.writeTargets??[])(!t.itemType||t.itemType===item?.itemType)&&writable.push(t)}let readOnly=input.readOnly??item?.readOnly??!1,surfaceName=input.surface?.surfaceName??declaration?.surfaceName??null,scopeChain=input.scopeChain??(surfaceName?["global",domainOf(surfaceName),surfaceName]:["global"]);return{surface:input.surface??null,declaration,valueKeys,scopeChain,excludedActionIds:new Set(input.excludedActionIds??[]),item,level:input.level??item?.level??null,selection:input.selection??null,payloadKinds:input.payloadKinds??[],writable:readOnly?[]:input.writable??writable,readOnly,restricted,organizationId:input.organizationId??null,auth:{authenticated:input.auth?.authenticated??!1,admin:input.auth?.admin??!1,creator:input.auth?.creator??!1},host:input.host}}async function runAction(action,target,options){let{ports}=options;if(action.requiresAuth&&!target.auth.authenticated)return ports.auth?(ports.auth.signInThenResume({actionId:action.id,contentFingerprint:options.contentFingerprint??action.id}),{status:"auth-pending"}):(ports.diagnostics.capture(new Error(`requiresAuth action "${action.id}" ran signed out with no auth port`),{area:"actions",detail:{actionId:action.id}}),{status:"failed",error:new Error("sign-in unavailable")});let reads=createProjectedReads(options.source??{surface:null,declaration:target.declaration,item:null}),signal=options.signal??new AbortController().signal;try{return{status:"ran",outcome:await action.run(target,{reads,ports,signal})}}catch(error){ports.diagnostics.capture(error,{area:"actions",detail:{actionId:action.id,stage:"run"}});let label=typeof action.label=="string"?action.label:action.id;return ports.notify?.error(`${label} did not finish.`,"Try again; if it keeps failing, report it from the feedback button."),{status:"failed",error}}}var DEFAULT_MAX_INHERITANCE_DEPTH=3;var APPLET_CATEGORY="applet",DEFAULT_TRIGGERED_BY=["person","agent","applet"],ACTION_CALLERS=["person","agent","applet","automation"],ACTION_RUNS=["page","server","mandate","navigate"],ACTION_POLICIES=["ask","auto","manual"];function compact(value){return Object.fromEntries(Object.entries(value).filter(([,v])=>v!==void 0))}var POLICY_OF_APPLY={manual:"manual",ask:"ask",auto:"auto",queued:"ask"};function writeTargetToAction(target){let applyPolicy=target.applyPolicy??"manual";return compact({name:target.name,label:target.label,description:target.description,input:target.valueKind??{jsonSchema:target.valueType==="document"?{}:{type:target.valueType}},runs:"page",triggeredBy:[...DEFAULT_TRIGGERED_BY],policy:POLICY_OF_APPLY[applyPolicy],updatesValue:target.updatesValue,mode:target.mode,patchable:target.patchable,approvalComparison:target.approvalComparison,streamOps:target.streamOps?[...target.streamOps]:void 0,destination:target.destination,groupKey:target.group,itemType:target.itemType,applyPolicy:target.applyPolicy,valueType:target.valueType,valueKind:target.valueKind,sortOrder:target.sortOrder,origin:{source:"write-target",declaredBy:"code"}})}function clientToolToAction(row){return compact({name:row.name,label:row.label,description:row.description,input:{jsonSchema:row.input_schema??{}},runs:"page",triggeredBy:["agent"],policy:row.mode==="draft"?"ask":"auto",mode:row.mode,origin:compact({source:"client-tool",surfaceName:row.surface_name,id:row.id,declaredBy:row.declared_by})})}function surfaceActionRowToAction(row){return compact({name:row.name,category:row.category,label:row.label,description:row.description,input:row.input??void 0,output:row.output??void 0,runs:row.runs,target:row.target??void 0,triggeredBy:[...row.triggered_by],policy:row.policy,updatesValue:row.updates_value??void 0,itemType:row.item_type||void 0,sortOrder:row.sort_order,origin:compact({source:"surface-action",surfaceName:row.surface_name,surfaceId:row.surface_id,id:row.id,declaredBy:row.declared_by,codeRef:row.code_ref??void 0})})}function toActionInfo(declaration,declaredOn){return compact({name:declaration.name,category:declaration.category??APPLET_CATEGORY,label:declaration.label,description:declaration.description,runs:declaration.runs,policy:declaration.policy,triggeredBy:[...declaration.triggeredBy],input:declaration.input,output:declaration.output,declaredOn,target:declaration.target,updatesValue:declaration.updatesValue,itemType:declaration.itemType,source:declaration.origin?.source})}function resolveActionChain(chain,options={}){let maxDepth=options.maxDepth??DEFAULT_MAX_INHERITANCE_DEPTH,names=chain.map(link=>link.surfaceName),seen=new Set;for(let name of names){if(seen.has(name))throw new Error(`[actions] inheritance CYCLE detected at "${name}". Fix the surface chain: ${names.join(" \u2192 ")}`);seen.add(name)}if(chain.length-1>maxDepth)throw new Error(`[actions] inheritance chain for "${names[names.length-1]}" exceeds depth ${maxDepth}: ${names.join(" \u2192 ")}`);let byName=new Map;for(let link of chain){let own=new Set;for(let action of link.actions){if(own.has(action.name))throw new Error(`[actions] surface "${link.surfaceName}" declares Action "${action.name}" twice. Names are unique; nothing was overwritten.`);own.add(action.name),byName.set(action.name,toActionInfo(action,link.surfaceName))}}return[...byName.values()]}function fail(code,message,retryable=!1,diagnostic){return{ok:!1,error:diagnostic?{code,message,retryable,diagnostic}:{code,message,retryable}}}function messageOf(error){return error instanceof Error&&error.message.trim()?error.message:typeof error=="string"&&error.trim()?error:"The run failed without saying why."}function newRunId(){let c=globalThis.crypto;return c?.randomUUID?c.randomUUID():`run-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,10)}`}function fillActionTarget(template,input){let source=input&&typeof input=="object"&&!Array.isArray(input)?input:{},missing=[];return{value:template.replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g,(_,key)=>{let v=source[key];return v==null||v===""||typeof v=="object"?(missing.push(key),""):encodeURIComponent(String(v))}),missing}}function fillName(template,input){let filled=fillActionTarget(template,input);return{value:decodeURIComponent(filled.value),missing:filled.missing}}function toJson2(value){return value===void 0?null:JSON.parse(JSON.stringify(value))}function refuseCaller(info,by){return info.triggeredBy.includes(by)?info.policy==="manual"&&by!=="person"?{code:"manual_only",message:`"${info.label}" (${info.name}) runs only when a person starts it.`,retryable:!1}:null:{code:"caller_not_allowed",message:`"${info.label}" (${info.name}) can't be started by ${CALLER_WORDS[by]}; it allows ${info.triggeredBy.join(", ")||"no one"}.`,retryable:!1}}var CALLER_WORDS={person:"a person",agent:"an agent",applet:"an applet",automation:"an automation"};function absent(info,capability){return fail("capability_absent",`"${info.label}" can't run here: ${capability} isn't available on this page.`)}async function runSurfaceAction(info,input,by,ports,options={}){let refusal=refuseCaller(info,by);if(refusal)return{ok:!1,error:refusal};let context={runId:options.runId??newRunId(),by,viewerId:options.viewerId??null,surfaceName:info.declaredOn,policy:info.policy},capture2=(error,stage)=>{try{ports.diagnostics.capture(error,{area:"actions",detail:{action:info.name,runs:info.runs,stage,runId:context.runId}})}catch{}};try{if(info.source==="write-target"&&ports.door){let receipt=await ports.door.write({surfaceName:info.declaredOn,target:info.name,...info.itemType?{itemType:info.itemType}:{},...options.itemIdentity?{itemIdentity:options.itemIdentity}:{},...options.ops?{ops:options.ops}:{value:input},by,runId:context.runId},options.signal);return receipt.status==="refused"?{ok:!1,error:{code:receipt.reason,message:receipt.sentence,retryable:receipt.reason==="failed",remedy:receipt.remedy}}:{ok:!0,data:receipt}}if(info.policy==="ask"&&by!=="person"&&info.runs!=="server"){if(!ports.approvals)return fail("needs_approval",`"${info.label}" needs a person's approval, and there is no one to ask here.`);if(!await ports.approvals.ask({action:info.name,label:info.label,description:info.description,input:toJson2(input),by,surfaceName:info.declaredOn}))return fail("declined",`"${info.label}" was not approved, so it did not run.`)}switch(info.runs){case"server":{if(!ports.serverActions)return absent(info,"server actions");let filled=fillName(info.target??info.name,input);if(filled.missing.length)return fail("input_missing",`"${info.label}" needs ${filled.missing.join(", ")}.`);let response=await ports.serverActions.run({name:filled.value,input:toJson2(input),by,surface_name:info.declaredOn,policy:info.policy});return response.status==="refused"?fail("refused",response.message):{ok:!0,data:response}}case"mandate":{if(!ports.mandates)return absent(info,"running a job");if(!info.target)return fail("target_missing",`"${info.label}" names no job to run.`);let filled=fillName(info.target,input);if(filled.missing.length)return fail("input_missing",`"${info.label}" needs ${filled.missing.join(", ")}.`);let record=input&&typeof input=="object"&&!Array.isArray(input)?input:{},variables=/\{[A-Za-z_]/.test(info.target)?record.variables&&typeof record.variables=="object"?toJson2(record.variables):{}:toJson2(record),userInput=typeof record.userInput=="string"?record.userInput:void 0;return{ok:!0,data:await ports.mandates.run({alias:filled.value,variables,...userInput!==void 0?{userInput}:{},by,policy:info.policy})}}case"navigate":{if(!ports.overlay)return absent(info,"navigation");let filled=fillActionTarget(info.target??"",input);if(filled.missing.length)return fail("input_missing",`"${info.label}" needs ${filled.missing.join(", ")}.`);let to=info.target&&/^\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(info.target)?decodeURIComponent(filled.value):filled.value;return to?(ports.overlay.navigate(to),{ok:!0,data:{to}}):fail("target_missing",`"${info.label}" names no page to open.`)}case"page":return ports.pageActions?await ports.pageActions.dispatch(info.name,input,context):absent(info,"this page's actions");default:{let runs=info.runs;return fail("runs_unknown",`"${info.label}" runs as "${String(runs)}", which nothing here knows how to run.`)}}}catch(error){return capture2(error,"run"),fail("run_failed",messageOf(error),!0)}}export{ACTION_CALLERS,ACTION_CATEGORIES,ACTION_POLICIES,ACTION_RUNS,APPLET_CATEGORY,DEFAULT_TRIGGERED_BY,DuplicateActionError,DuplicateProviderError,UNIVERSAL_VERBS,alwaysEligible,clientToolToAction,computeEligibility,createActionRegistry,createClickTarget,createProjectedReads,fillActionTarget,projectValues,refuseCaller,resolveActionChain,runAction,runSurfaceAction,staticActionProvider,surfaceActionRowToAction,toActionInfo,writeTargetToAction};
|
|
2
2
|
//# sourceMappingURL=actions.js.map
|