@inhuman.tools/plexus-expectations 0.0.0-stage → 0.2.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.
Files changed (128) hide show
  1. package/LICENSE.md +7 -0
  2. package/README.md +144 -2
  3. package/dist/driver/ExpectationActor.d.ts +56 -0
  4. package/dist/driver/ExpectationActor.d.ts.map +1 -0
  5. package/dist/driver/ExpectationActor.js +210 -0
  6. package/dist/driver/ExpectationActor.js.map +1 -0
  7. package/dist/driver/PEWDriver.d.ts +144 -0
  8. package/dist/driver/PEWDriver.d.ts.map +1 -0
  9. package/dist/driver/PEWDriver.js +303 -0
  10. package/dist/driver/PEWDriver.js.map +1 -0
  11. package/dist/driver/actor-machine.d.ts +28 -0
  12. package/dist/driver/actor-machine.d.ts.map +1 -0
  13. package/dist/driver/actor-machine.js +50 -0
  14. package/dist/driver/actor-machine.js.map +1 -0
  15. package/dist/driver/connection-miss.d.ts +9 -0
  16. package/dist/driver/connection-miss.d.ts.map +1 -0
  17. package/dist/driver/connection-miss.js +13 -0
  18. package/dist/driver/connection-miss.js.map +1 -0
  19. package/dist/driver/index.d.ts +6 -0
  20. package/dist/driver/index.d.ts.map +1 -0
  21. package/dist/driver/index.js +5 -0
  22. package/dist/driver/index.js.map +1 -0
  23. package/dist/executor/index.d.ts +4 -0
  24. package/dist/executor/index.d.ts.map +1 -0
  25. package/dist/executor/index.js +4 -0
  26. package/dist/executor/index.js.map +1 -0
  27. package/dist/executor/kernel-intern.d.ts +16 -0
  28. package/dist/executor/kernel-intern.d.ts.map +1 -0
  29. package/dist/executor/kernel-intern.js +98 -0
  30. package/dist/executor/kernel-intern.js.map +1 -0
  31. package/dist/executor/orchestrator.d.ts +97 -0
  32. package/dist/executor/orchestrator.d.ts.map +1 -0
  33. package/dist/executor/orchestrator.js +465 -0
  34. package/dist/executor/orchestrator.js.map +1 -0
  35. package/dist/executor/types.d.ts +8 -0
  36. package/dist/executor/types.d.ts.map +1 -0
  37. package/dist/executor/types.js +2 -0
  38. package/dist/executor/types.js.map +1 -0
  39. package/dist/index.d.ts +2 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +2 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/schemas/connection.d.ts +4 -0
  44. package/dist/schemas/connection.d.ts.map +1 -0
  45. package/dist/schemas/connection.js +4 -0
  46. package/dist/schemas/connection.js.map +1 -0
  47. package/dist/schemas/index.d.ts +2 -0
  48. package/dist/schemas/index.d.ts.map +1 -0
  49. package/dist/schemas/index.js +2 -0
  50. package/dist/schemas/index.js.map +1 -0
  51. package/dist/shared/control.d.ts +4 -0
  52. package/dist/shared/control.d.ts.map +1 -0
  53. package/dist/shared/control.js +2 -0
  54. package/dist/shared/control.js.map +1 -0
  55. package/dist/shared/index.d.ts +7 -0
  56. package/dist/shared/index.d.ts.map +1 -0
  57. package/dist/shared/index.js +7 -0
  58. package/dist/shared/index.js.map +1 -0
  59. package/dist/shared/map-field-awareness.d.ts +19 -0
  60. package/dist/shared/map-field-awareness.d.ts.map +1 -0
  61. package/dist/shared/map-field-awareness.js +58 -0
  62. package/dist/shared/map-field-awareness.js.map +1 -0
  63. package/dist/shared/models/ConnectionSpec.d.ts +44 -0
  64. package/dist/shared/models/ConnectionSpec.d.ts.map +1 -0
  65. package/dist/shared/models/ConnectionSpec.js +180 -0
  66. package/dist/shared/models/ConnectionSpec.js.map +1 -0
  67. package/dist/shared/models/DriverSpec.d.ts +45 -0
  68. package/dist/shared/models/DriverSpec.d.ts.map +1 -0
  69. package/dist/shared/models/DriverSpec.js +221 -0
  70. package/dist/shared/models/DriverSpec.js.map +1 -0
  71. package/dist/shared/models/Job.d.ts +85 -0
  72. package/dist/shared/models/Job.d.ts.map +1 -0
  73. package/dist/shared/models/Job.js +261 -0
  74. package/dist/shared/models/Job.js.map +1 -0
  75. package/dist/shared/models/WorkContainer.d.ts +36 -0
  76. package/dist/shared/models/WorkContainer.d.ts.map +1 -0
  77. package/dist/shared/models/WorkContainer.js +140 -0
  78. package/dist/shared/models/WorkContainer.js.map +1 -0
  79. package/dist/shared/models/WorkExpectation.d.ts +63 -0
  80. package/dist/shared/models/WorkExpectation.d.ts.map +1 -0
  81. package/dist/shared/models/WorkExpectation.js +290 -0
  82. package/dist/shared/models/WorkExpectation.js.map +1 -0
  83. package/dist/shared/models/index.d.ts +7 -0
  84. package/dist/shared/models/index.d.ts.map +1 -0
  85. package/dist/shared/models/index.js +6 -0
  86. package/dist/shared/models/index.js.map +1 -0
  87. package/dist/shared/models/jsonRecord.d.ts +14 -0
  88. package/dist/shared/models/jsonRecord.d.ts.map +1 -0
  89. package/dist/shared/models/jsonRecord.js +46 -0
  90. package/dist/shared/models/jsonRecord.js.map +1 -0
  91. package/dist/shared/revalidation-demand.d.ts +27 -0
  92. package/dist/shared/revalidation-demand.d.ts.map +1 -0
  93. package/dist/shared/revalidation-demand.js +116 -0
  94. package/dist/shared/revalidation-demand.js.map +1 -0
  95. package/dist/shared/role.d.ts +12 -0
  96. package/dist/shared/role.d.ts.map +1 -0
  97. package/dist/shared/role.js +21 -0
  98. package/dist/shared/role.js.map +1 -0
  99. package/dist/shared/types.d.ts +43 -0
  100. package/dist/shared/types.d.ts.map +1 -0
  101. package/dist/shared/types.js +12 -0
  102. package/dist/shared/types.js.map +1 -0
  103. package/package.json +79 -3
  104. package/src/driver/ExpectationActor.ts +244 -0
  105. package/src/driver/PEWDriver.ts +302 -0
  106. package/src/driver/actor-machine.ts +54 -0
  107. package/src/driver/connection-miss.ts +13 -0
  108. package/src/driver/index.ts +5 -0
  109. package/src/executor/index.ts +3 -0
  110. package/src/executor/kernel-intern.ts +121 -0
  111. package/src/executor/orchestrator.ts +463 -0
  112. package/src/executor/types.ts +3 -0
  113. package/src/index.ts +1 -0
  114. package/src/schemas/connection.ts +4 -0
  115. package/src/schemas/index.ts +1 -0
  116. package/src/shared/control.ts +4 -0
  117. package/src/shared/index.ts +6 -0
  118. package/src/shared/map-field-awareness.ts +65 -0
  119. package/src/shared/models/ConnectionSpec.ts +84 -0
  120. package/src/shared/models/DriverSpec.ts +102 -0
  121. package/src/shared/models/Job.ts +192 -0
  122. package/src/shared/models/WorkContainer.ts +90 -0
  123. package/src/shared/models/WorkExpectation.ts +229 -0
  124. package/src/shared/models/index.ts +6 -0
  125. package/src/shared/models/jsonRecord.ts +49 -0
  126. package/src/shared/revalidation-demand.ts +121 -0
  127. package/src/shared/role.ts +32 -0
  128. package/src/shared/types.ts +57 -0
package/LICENSE.md ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2026 Merkle Bonsai
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,145 @@
1
- # Temporary Holding Version
1
+ # PEW — Plexus Expectation Workflow
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Package:** `@inhuman.tools/plexus-expectations`
4
+
5
+ Durable work on Plexus documents. One noun for open work — **Expectation** (harness docs: **PEW/Expectation**) — demand with an
6
+ outcome. The entity itself is the whole API: the claim-owner host writes PEW/Expectation bodies (Client queues the named action);
7
+ observers read it; Actors honor it; the process holding the live PEW/Connection starts or declines it.
8
+
9
+ This README is the contract.
10
+
11
+ ---
12
+
13
+ ## Three entry points, three roles
14
+
15
+ | Import | Role | Contents |
16
+ | ------------------------------------ | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
17
+ | `@inhuman.tools/plexus-expectations` | **Vocabulary** — any process holding the doc (claim-owner host, observers, UIs) | `WorkExpectation`, `Job`, `WorkContainer`, `ConnectionSpec`, `DriverSpec`, outcome type |
18
+ | `…/driver` | **Fulfillment** — PEW/Driver packages | `PEWDriver`, `ExpectationActor`, bootstrap contexts |
19
+ | `…/executor` | **Supervision** — the process that has the live connections | `Orchestrator` (PEW honor + Job executors; plus everything in `/driver`) |
20
+
21
+ Observers and claim-owner hosts never import `/executor`; driver packages never need it either.
22
+
23
+ ---
24
+
25
+ ## The entity is the API
26
+
27
+ Live `report` is the scan; settlement is the `outcome` / `outcomeReport` write on the entity; the next read is instant.
28
+
29
+ Everything scoped to a unit of work lives on the `Expectation` model — there is no client
30
+ object, no session parameter, and a cross-session mistake is unrepresentable (the entity locates
31
+ its own document, plexus, and PEW/Awareness):
32
+
33
+ | Face | Surface |
34
+ | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
35
+ | **Author** (claim-owner host, not Client) | `new Kind({ …fields })` — the subclass fields ARE the request |
36
+ | **Observer** | `await E` (settles with `"successful"` / `"failed"` when `isDone`; `failed` fulfills), `E.outcome` (`null` / `successful` / `failed`), `E.workerPresent`, `E.report` (outcome frame or live awareness), `E.endCause` / `E.endDetail`; catalog liveness is process-local on the spec entities' `.readyState` / `.live` |
37
+ | **Actor** | `this.expectation` (the request), `this.signal`, `admitTermination()`, `report()` / `complete()` / `fail()`; `ConnectionNotReady` / `DemandIncomplete` from `run()` settle fail, not crash |
38
+ | **Connection** | leftover attach PEW is honored; new work is kicked from origin (`complete` / Job BODY interned methods); catalog drivers load themselves; `PEWDriver.handle` remains the leftover door |
39
+
40
+ Role occupancy is one `role:expectations` map keyed by uuid. A role write requires a
41
+ doc-homed entity — occupancy not propagated over awareness is no occupancy, so
42
+ touching the role map on an unhomed entity throws loudly rather than publishing
43
+ into the void.
44
+
45
+ ### Outcome
46
+
47
+ ```text
48
+ null = not done
49
+ successful = done as expected
50
+ failed = done otherwise (error, crash, cancel)
51
+ ```
52
+
53
+ There is no expectation state machine and no durable `running`. "Someone is working
54
+ to satisfy this" is actor presence (`workerPresent` / the role map), gone when the
55
+ teller goes. Last progress this process saw stays on `progressAwareness` after the
56
+ uuid retracts. The writers `succeed` / `fail` are first-writer-wins (`fail` with
57
+ `endCause: cancel` is how cancel is recorded). `connection` on the expectation and
58
+ `driver` on the connection are required — a missing field is a host bug, not an
59
+ outcome. Every ending carries `endCause` (`settled` | `cancel` | `crash`) and
60
+ `outcomeReport`.
61
+
62
+ Cancel is host-owned: `admitTermination()` on a live actor, or `fail("cancel", …)` when
63
+ nobody is working. No acks, no epochs, no kernel hop.
64
+
65
+ ### Endings are observed, never imposed
66
+
67
+ There is no fold. Endings are `succeed` / `fail` writes — the actor on its own settlement,
68
+ or host `admitTermination` (a settlement captured before admission outranks the cancel).
69
+ The actor keeps one reaction on its entity's `isDone`: however the ending arrived — local
70
+ settlement, admitted cancel, a peer's write — the process duties are identical: abort the
71
+ signal, leave. Children of a done parent are skipped by honor (`isTreeOrphan`), not ended
72
+ by a cascade from the parent's stack frame.
73
+
74
+ ---
75
+
76
+ ## Extending: kinds and drivers
77
+
78
+ **Work vocabulary is global.** A product ships `Expectation` subclasses; the `@syncing` registry
79
+ tag IS the kind — derived, never declared. The subclass is the single type carrier:
80
+
81
+ ```ts
82
+ @syncing("myapp:tool_call")
83
+ class ToolCallExpectation extends Expectation<ToolResult, ToolReport> {
84
+ @syncing accessor name = "";
85
+ @syncing accessor argsJson = "{}";
86
+
87
+ override applySettlement(result: ToolResult): void {
88
+ /* typed product-field writes */
89
+ }
90
+ }
91
+ ```
92
+
93
+ `ResultOf` / `ReportOf` extract the shapes.
94
+
95
+ **PEW/Driver is claim, not invent — and the claim lives on the catalog, never on code.**
96
+ `DriverSpec.can: string[]` is the durable claim surface ("this driver handles these
97
+ kinds") — expectation model names, elevated through the process-global `@syncing` class registry.
98
+ Dynamically loaded driver code does not get to declare its own authority.
99
+
100
+ **The loaded class is `PEWDriver`. The instance is the connection.** `DriverSpec` and `ConnectionSpec` stay the inventory rows. A driver module's default export is
101
+ a `PEWDriver` subclass: the static side is code identity (`pewBrand`, `offers`),
102
+ each instance is one live connection (its own gated `bootstrap()`), and `resolve(expectation)`
103
+ mints the actor — which may be a connection-specialized `ExpectationActor` subclass holding the
104
+ live runtime directly:
105
+
106
+ ```ts
107
+ export default class MyDriverConnection extends PEWDriver<ToolCallExpectation> {
108
+ static override configurationSchema = z.object({ token: z.string() });
109
+ static offers = [];
110
+
111
+ override async bootstrap() {
112
+ /* per-connection load */
113
+ }
114
+ protected override resolve(expectation: ToolCallExpectation) {
115
+ return new MyActor(expectation, this /* live runtime */);
116
+ }
117
+ }
118
+ ```
119
+
120
+ Process facts are `readyState` / `error` / `offers`. Driver extras live on `state`
121
+ (`@observable.shallow override accessor state = { … }`); the orchestrator mirrors
122
+ them onto `spec.live`. Clients render. A concrete class that should not share the
123
+ base extras object declares its own `static state`. Reporting only (no machinery
124
+ branches on it; instance bootstrap truth is the gate); `ensureBootstrapStarted` /
125
+ `failReady` write the token and siblings.
126
+
127
+ Settings enter through `configurationSchema` (Zod), secrets through
128
+ `secretsSchema`. The constructor takes them as separate arguments;
129
+ `parseConfiguration` / `parseSecrets` are the same doors without `new`.
130
+ The live `secrets` view may be replaced after construct (reauth).
131
+
132
+ Bootstrap contexts stay minimal (`{signal, secrets}`); a host hands drivers more by extending
133
+ the context object structurally — there is no ports registry.
134
+
135
+ **The seam.** `Orchestrator` is the boundary between the plexus plane and execution: it notices
136
+ open work, maps the expectation's connection to a catalog row ensures the row's driver class and connection instance
137
+ (memoized; a rejected memo IS the sticky failure), and hands the expectation to
138
+ `connection.handle` — where
139
+ everything about executing it lives. Mint is not a claim: `resolve()` throwing
140
+ breaks the connection (the work stays unspawned). The actor machine still
141
+ requires `START` before `run()`; that is process-local, not a durable claim.
142
+
143
+ ## License
144
+
145
+ [MIT](./LICENSE.md).
@@ -0,0 +1,56 @@
1
+ import { ErrorWithInvariant } from "@inhuman.tools/error-with-invariant";
2
+ import type { PEWDriver } from "./PEWDriver.js";
3
+ import type { Settlement } from "../executor/types.js";
4
+ import { type AnyWorkExpectation, type ReportOf, type ResultOf } from "../shared/models/WorkExpectation.js";
5
+ /**
6
+ * Thrown from `run()` — `start()` settles the work as fail, not crash.
7
+ *
8
+ * Two kinds: {@link ConnectionNotReady} (fulfiller not live), {@link DemandIncomplete}
9
+ * (required field missing on the expectation). Not this family: cancel, applySettlement,
10
+ * report JSON crash, HTTP/tool `fail(reason)`.
11
+ */
12
+ export declare abstract class ExpectationInvariant extends ErrorWithInvariant {
13
+ name: string;
14
+ constructor(message: string);
15
+ }
16
+ /** Honor started; the fulfiller is not live. */
17
+ export declare class ConnectionNotReady extends ExpectationInvariant {
18
+ readonly connection: string;
19
+ constructor(connection: string);
20
+ }
21
+ /** Required field missing on the expectation body. */
22
+ export declare class DemandIncomplete extends ExpectationInvariant {
23
+ readonly field: string;
24
+ constructor(field: string);
25
+ }
26
+ /**
27
+ * One execution of one expectation, bound to the process that has the live connection.
28
+ *
29
+ * ENDINGS ARE OBSERVED, NOT IMPOSED. The actor writes durable endings only at
30
+ * two authority points — its own settlement (`#settle` / `#crash`) and host
31
+ * admission (`admitTermination`). Everything that must happen BECAUSE the
32
+ * work ended (abort the signal, leave) runs in the done reaction — so an
33
+ * ending that arrives from anywhere (a peer's write synced in, host policy)
34
+ * stops this actor exactly like its own. Child endings are supervision's
35
+ * job (the tree-orphan rule), never a cascade from here.
36
+ */
37
+ export declare abstract class ExpectationActor<E extends AnyWorkExpectation = AnyWorkExpectation> {
38
+ #private;
39
+ readonly expectation: E;
40
+ readonly driverConnection: PEWDriver<E>;
41
+ readonly signal: AbortSignal;
42
+ readonly settled: Promise<Settlement<ResultOf<E>>>;
43
+ constructor(expectation: E, driverConnection: PEWDriver<E>);
44
+ get started(): boolean;
45
+ get done(): boolean;
46
+ start(): void;
47
+ protected abstract run(): void | Promise<void>;
48
+ abort(reason?: unknown): void;
49
+ protected report(frame: ReportOf<E>): void;
50
+ protected complete(result: ResultOf<E>): void;
51
+ protected fail(reason: unknown): void;
52
+ /** Host cancel. A settlement captured while running outranks the cancel. */
53
+ admitTermination(): void;
54
+ leave(): void;
55
+ }
56
+ //# sourceMappingURL=ExpectationActor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpectationActor.d.ts","sourceRoot":"","sources":["../../src/driver/ExpectationActor.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AAMzE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,QAAQ,EACb,KAAK,QAAQ,EAEd,MAAM,qCAAqC,CAAC;AAG7C;;;;;;GAMG;AACH,8BAAsB,oBAAqB,SAAQ,kBAAkB;IAC1D,IAAI,SAAyB;gBAE1B,OAAO,EAAE,MAAM;CAG5B;AAED,gDAAgD;AAChD,qBAAa,kBAAmB,SAAQ,oBAAoB;IAC9C,QAAQ,CAAC,UAAU,EAAE,MAAM;gBAAlB,UAAU,EAAE,MAAM;CAGxC;AAED,sDAAsD;AACtD,qBAAa,gBAAiB,SAAQ,oBAAoB;IAC5C,QAAQ,CAAC,KAAK,EAAE,MAAM;gBAAb,KAAK,EAAE,MAAM;CAGnC;AAED;;;;;;;;;;GAUG;AACH,8BAAsB,gBAAgB,CAAC,CAAC,SAAS,kBAAkB,GAAG,kBAAkB;;aAcpE,WAAW,EAAE,CAAC;aACd,gBAAgB,EAAE,SAAS,CAAC,CAAC,CAAC;IAZhD,QAAQ,CAAC,MAAM,EAAE,WAAW,CAA2B;IAKvD,SAAgB,OAAO,EAAE,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBAMxC,WAAW,EAAE,CAAC,EACd,gBAAgB,EAAE,SAAS,CAAC,CAAC,CAAC;IAgChD,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,IAAI,IAAI,IAAI,OAAO,CAElB;IAED,KAAK,IAAI,IAAI;IAsCb,SAAS,CAAC,QAAQ,CAAC,GAAG,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAE9C,KAAK,CAAC,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI;IAI7B,SAAS,CAAC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI;IAa1C,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI;IAI7C,SAAS,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IAmDrC,4EAA4E;IAC5E,gBAAgB,IAAI,IAAI;IAQxB,KAAK,IAAI,IAAI;CASd"}
@@ -0,0 +1,210 @@
1
+ import { ErrorWithInvariant } from "@inhuman.tools/error-with-invariant";
2
+ import { reaction, runInAction } from "mobx";
3
+ import invariant from "tiny-invariant";
4
+ import { createActor } from "xstate";
5
+ import { expectationActorMachine } from "./actor-machine.js";
6
+ import { ROLE_LANE_OCCUPIED, } from "../shared/models/WorkExpectation.js";
7
+ import { reportRole, retractRole } from "../shared/role.js";
8
+ /**
9
+ * Thrown from `run()` — `start()` settles the work as fail, not crash.
10
+ *
11
+ * Two kinds: {@link ConnectionNotReady} (fulfiller not live), {@link DemandIncomplete}
12
+ * (required field missing on the expectation). Not this family: cancel, applySettlement,
13
+ * report JSON crash, HTTP/tool `fail(reason)`.
14
+ */
15
+ export class ExpectationInvariant extends ErrorWithInvariant {
16
+ name = this.constructor.name;
17
+ constructor(message) {
18
+ super(message);
19
+ }
20
+ }
21
+ /** Honor started; the fulfiller is not live. */
22
+ export class ConnectionNotReady extends ExpectationInvariant {
23
+ connection;
24
+ constructor(connection) {
25
+ super(`${connection} connection not ready`);
26
+ this.connection = connection;
27
+ }
28
+ }
29
+ /** Required field missing on the expectation body. */
30
+ export class DemandIncomplete extends ExpectationInvariant {
31
+ field;
32
+ constructor(field) {
33
+ super(`demand missing ${field}`);
34
+ this.field = field;
35
+ }
36
+ }
37
+ /**
38
+ * One execution of one expectation, bound to the process that has the live connection.
39
+ *
40
+ * ENDINGS ARE OBSERVED, NOT IMPOSED. The actor writes durable endings only at
41
+ * two authority points — its own settlement (`#settle` / `#crash`) and host
42
+ * admission (`admitTermination`). Everything that must happen BECAUSE the
43
+ * work ended (abort the signal, leave) runs in the done reaction — so an
44
+ * ending that arrives from anywhere (a peer's write synced in, host policy)
45
+ * stops this actor exactly like its own. Child endings are supervision's
46
+ * job (the tree-orphan rule), never a cascade from here.
47
+ */
48
+ export class ExpectationActor {
49
+ expectation;
50
+ driverConnection;
51
+ #hub;
52
+ #controller = new AbortController();
53
+ signal = this.#controller.signal;
54
+ #lastReportJson = null;
55
+ #settlement = null;
56
+ #resolveSettled;
57
+ #rejectSettled;
58
+ settled;
59
+ #phase = createActor(expectationActorMachine).start();
60
+ #laneDisposers = [];
61
+ #left = false;
62
+ constructor(expectation, driverConnection) {
63
+ this.expectation = expectation;
64
+ this.driverConnection = driverConnection;
65
+ this.settled = new Promise((resolve, reject) => {
66
+ this.#resolveSettled = resolve;
67
+ this.#rejectSettled = reject;
68
+ });
69
+ void this.settled.catch(() => { });
70
+ this.signal.addEventListener("abort", () => {
71
+ this.#send("ABORT");
72
+ }, { once: true });
73
+ invariant(expectation.plexus != null, "ExpectationActor requires the expectation to be on a session plexus");
74
+ this.#hub = expectation.plexus.awareness;
75
+ this.#laneDisposers.push(reaction(() => this.expectation.isDone, (done) => {
76
+ if (!done)
77
+ return;
78
+ this.abort("done");
79
+ this.#send("RELEASE");
80
+ this.leave();
81
+ }, { fireImmediately: true }));
82
+ }
83
+ get started() {
84
+ return !this.#phase.getSnapshot().can({ type: "START" });
85
+ }
86
+ get done() {
87
+ return this.#phase.getSnapshot().status === "done";
88
+ }
89
+ start() {
90
+ invariant(this.#phase.getSnapshot().can({ type: "START" }), "double-start attempt");
91
+ this.#send("START");
92
+ runInAction(() => {
93
+ this.expectation.markStarted();
94
+ });
95
+ this.#occupyRoleLane();
96
+ try {
97
+ const result = this.run();
98
+ if (result != null && typeof result.then === "function") {
99
+ result.catch((error) => this.#onRunError(error));
100
+ }
101
+ }
102
+ catch (error) {
103
+ this.#onRunError(error);
104
+ }
105
+ }
106
+ #onRunError(error) {
107
+ if (error instanceof ExpectationInvariant) {
108
+ this.fail(error);
109
+ return;
110
+ }
111
+ this.#crash(error);
112
+ }
113
+ #matches(phase) {
114
+ return this.#phase.getSnapshot().matches(phase);
115
+ }
116
+ #send(type) {
117
+ if (!this.#phase.getSnapshot().can({ type }))
118
+ return;
119
+ this.#phase.send({ type });
120
+ }
121
+ #occupyRoleLane() {
122
+ reportRole(this.#hub, this.expectation.uuid, ROLE_LANE_OCCUPIED);
123
+ }
124
+ abort(reason) {
125
+ this.#controller.abort(reason);
126
+ }
127
+ report(frame) {
128
+ if (!this.#matches("running"))
129
+ return;
130
+ let json;
131
+ try {
132
+ json = JSON.stringify(frame ?? null);
133
+ }
134
+ catch (error) {
135
+ this.#crash(error);
136
+ return;
137
+ }
138
+ this.#lastReportJson = json;
139
+ reportRole(this.#hub, this.expectation.uuid, JSON.parse(json));
140
+ }
141
+ complete(result) {
142
+ this.#settle({ outcome: "complete", result });
143
+ }
144
+ fail(reason) {
145
+ this.#settle({ outcome: "fail", reason });
146
+ }
147
+ #settle(settlement) {
148
+ if (this.done || !this.#phase.getSnapshot().can({ type: "SETTLE" }))
149
+ return;
150
+ const wasRunning = this.#matches("running");
151
+ this.#settlement = settlement;
152
+ this.#resolveSettled(settlement);
153
+ this.#send("SETTLE");
154
+ this.#commitEnding("settle", wasRunning);
155
+ }
156
+ #crash(reason) {
157
+ if (this.done || !this.#phase.getSnapshot().can({ type: "CRASH" }))
158
+ return;
159
+ const wasRunning = this.#matches("running");
160
+ this.#rejectSettled(reason);
161
+ this.#send("CRASH");
162
+ this.#commitEnding("crash", wasRunning, String(reason));
163
+ }
164
+ /**
165
+ * ENDING PRECEDES ABORT. A settlement captured while running (or consulted
166
+ * when admitting cancel) wins. A complete/fail flushed from an abort listener
167
+ * is stored on `#settlement` but not written here.
168
+ */
169
+ #commitEnding(kind, wasRunning, crashDetail) {
170
+ if (this.expectation.isDone)
171
+ return;
172
+ if (this.#settlement !== null && (wasRunning || kind === "cancel")) {
173
+ const settlement = this.#settlement;
174
+ if (settlement.outcome === "complete") {
175
+ this.expectation.succeed("settled", "", this.#frameJson(), { result: settlement.result });
176
+ }
177
+ else {
178
+ this.expectation.fail("settled", settlement.reason instanceof Error ? settlement.reason.message : String(settlement.reason), this.#frameJson());
179
+ }
180
+ return;
181
+ }
182
+ if (kind === "cancel") {
183
+ this.expectation.fail("cancel", "terminate", this.#frameJson());
184
+ return;
185
+ }
186
+ if (!wasRunning)
187
+ return;
188
+ if (kind === "crash") {
189
+ this.expectation.fail("crash", crashDetail ?? "", this.#frameJson());
190
+ }
191
+ }
192
+ /** Host cancel. A settlement captured while running outranks the cancel. */
193
+ admitTermination() {
194
+ this.#commitEnding("cancel", this.#matches("running"));
195
+ }
196
+ #frameJson() {
197
+ return this.#lastReportJson ?? "null";
198
+ }
199
+ leave() {
200
+ if (!this.#left) {
201
+ this.#left = true;
202
+ for (const dispose of this.#laneDisposers)
203
+ dispose();
204
+ this.#laneDisposers = [];
205
+ retractRole(this.#hub, this.expectation.uuid);
206
+ }
207
+ this.driverConnection.release(this.expectation);
208
+ }
209
+ }
210
+ //# sourceMappingURL=ExpectationActor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpectationActor.js","sourceRoot":"","sources":["../../src/driver/ExpectationActor.ts"],"names":[],"mappings":"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","sourcesContent":["import { type PlexusAwareness } from \"@here.build/plexus\";\nimport { ErrorWithInvariant } from \"@inhuman.tools/error-with-invariant\";\nimport { reaction, runInAction } from \"mobx\";\nimport invariant from \"tiny-invariant\";\nimport { createActor } from \"xstate\";\n\nimport { type ActorPhase, type ActorPhaseEvent, expectationActorMachine } from \"./actor-machine.js\";\nimport type { PEWDriver } from \"./PEWDriver.js\";\nimport type { Settlement } from \"../executor/types.js\";\nimport type { SessionAwareness } from \"../shared/index.js\";\nimport {\n type AnyWorkExpectation,\n type ReportOf,\n type ResultOf,\n ROLE_LANE_OCCUPIED,\n} from \"../shared/models/WorkExpectation.js\";\nimport { reportRole, retractRole } from \"../shared/role.js\";\n\n/**\n * Thrown from `run()` — `start()` settles the work as fail, not crash.\n *\n * Two kinds: {@link ConnectionNotReady} (fulfiller not live), {@link DemandIncomplete}\n * (required field missing on the expectation). Not this family: cancel, applySettlement,\n * report JSON crash, HTTP/tool `fail(reason)`.\n */\nexport abstract class ExpectationInvariant extends ErrorWithInvariant {\n override name = this.constructor.name;\n\n constructor(message: string) {\n super(message);\n }\n}\n\n/** Honor started; the fulfiller is not live. */\nexport class ConnectionNotReady extends ExpectationInvariant {\n constructor(readonly connection: string) {\n super(`${connection} connection not ready`);\n }\n}\n\n/** Required field missing on the expectation body. */\nexport class DemandIncomplete extends ExpectationInvariant {\n constructor(readonly field: string) {\n super(`demand missing ${field}`);\n }\n}\n\n/**\n * One execution of one expectation, bound to the process that has the live connection.\n *\n * ENDINGS ARE OBSERVED, NOT IMPOSED. The actor writes durable endings only at\n * two authority points — its own settlement (`#settle` / `#crash`) and host\n * admission (`admitTermination`). Everything that must happen BECAUSE the\n * work ended (abort the signal, leave) runs in the done reaction — so an\n * ending that arrives from anywhere (a peer's write synced in, host policy)\n * stops this actor exactly like its own. Child endings are supervision's\n * job (the tree-orphan rule), never a cascade from here.\n */\nexport abstract class ExpectationActor<E extends AnyWorkExpectation = AnyWorkExpectation> {\n readonly #hub: PlexusAwareness<SessionAwareness>;\n readonly #controller = new AbortController();\n readonly signal: AbortSignal = this.#controller.signal;\n #lastReportJson: string | null = null;\n #settlement: Settlement<ResultOf<E>> | null = null;\n #resolveSettled!: (settlement: Settlement<ResultOf<E>>) => void;\n #rejectSettled!: (reason: unknown) => void;\n public readonly settled: Promise<Settlement<ResultOf<E>>>;\n readonly #phase = createActor(expectationActorMachine).start();\n #laneDisposers: Array<() => void> = [];\n #left = false;\n\n constructor(\n public readonly expectation: E,\n public readonly driverConnection: PEWDriver<E>,\n ) {\n this.settled = new Promise<Settlement<ResultOf<E>>>((resolve, reject) => {\n this.#resolveSettled = resolve;\n this.#rejectSettled = reject;\n });\n void this.settled.catch(() => {});\n\n this.signal.addEventListener(\n \"abort\",\n () => {\n this.#send(\"ABORT\");\n },\n { once: true },\n );\n\n invariant(expectation.plexus != null, \"ExpectationActor requires the expectation to be on a session plexus\");\n this.#hub = expectation.plexus.awareness as PlexusAwareness<SessionAwareness>;\n this.#laneDisposers.push(\n reaction(\n () => this.expectation.isDone,\n (done) => {\n if (!done) return;\n this.abort(\"done\");\n this.#send(\"RELEASE\");\n this.leave();\n },\n { fireImmediately: true },\n ),\n );\n }\n\n get started(): boolean {\n return !this.#phase.getSnapshot().can({ type: \"START\" });\n }\n\n get done(): boolean {\n return this.#phase.getSnapshot().status === \"done\";\n }\n\n start(): void {\n invariant(this.#phase.getSnapshot().can({ type: \"START\" }), \"double-start attempt\");\n this.#send(\"START\");\n runInAction(() => {\n this.expectation.markStarted();\n });\n this.#occupyRoleLane();\n try {\n const result = this.run();\n if (result != null && typeof (result as Promise<void>).then === \"function\") {\n (result as Promise<void>).catch((error) => this.#onRunError(error));\n }\n } catch (error) {\n this.#onRunError(error);\n }\n }\n\n #onRunError(error: unknown): void {\n if (error instanceof ExpectationInvariant) {\n this.fail(error);\n return;\n }\n this.#crash(error);\n }\n\n #matches(phase: ActorPhase): boolean {\n return this.#phase.getSnapshot().matches(phase);\n }\n\n #send(type: ActorPhaseEvent[\"type\"]): void {\n if (!this.#phase.getSnapshot().can({ type })) return;\n this.#phase.send({ type });\n }\n\n #occupyRoleLane(): void {\n reportRole(this.#hub, this.expectation.uuid, ROLE_LANE_OCCUPIED);\n }\n\n protected abstract run(): void | Promise<void>;\n\n abort(reason?: unknown): void {\n this.#controller.abort(reason);\n }\n\n protected report(frame: ReportOf<E>): void {\n if (!this.#matches(\"running\")) return;\n let json: string;\n try {\n json = JSON.stringify(frame ?? null);\n } catch (error) {\n this.#crash(error);\n return;\n }\n this.#lastReportJson = json;\n reportRole(this.#hub, this.expectation.uuid, JSON.parse(json));\n }\n\n protected complete(result: ResultOf<E>): void {\n this.#settle({ outcome: \"complete\", result });\n }\n\n protected fail(reason: unknown): void {\n this.#settle({ outcome: \"fail\", reason });\n }\n\n #settle(settlement: Settlement<ResultOf<E>>): void {\n if (this.done || !this.#phase.getSnapshot().can({ type: \"SETTLE\" })) return;\n const wasRunning = this.#matches(\"running\");\n this.#settlement = settlement;\n this.#resolveSettled(settlement);\n this.#send(\"SETTLE\");\n this.#commitEnding(\"settle\", wasRunning);\n }\n\n #crash(reason: unknown): void {\n if (this.done || !this.#phase.getSnapshot().can({ type: \"CRASH\" })) return;\n const wasRunning = this.#matches(\"running\");\n this.#rejectSettled(reason);\n this.#send(\"CRASH\");\n this.#commitEnding(\"crash\", wasRunning, String(reason));\n }\n\n /**\n * ENDING PRECEDES ABORT. A settlement captured while running (or consulted\n * when admitting cancel) wins. A complete/fail flushed from an abort listener\n * is stored on `#settlement` but not written here.\n */\n #commitEnding(kind: \"settle\" | \"crash\" | \"cancel\", wasRunning: boolean, crashDetail?: string): void {\n if (this.expectation.isDone) return;\n if (this.#settlement !== null && (wasRunning || kind === \"cancel\")) {\n const settlement = this.#settlement;\n if (settlement.outcome === \"complete\") {\n this.expectation.succeed(\"settled\", \"\", this.#frameJson(), { result: settlement.result });\n } else {\n this.expectation.fail(\n \"settled\",\n settlement.reason instanceof Error ? settlement.reason.message : String(settlement.reason),\n this.#frameJson(),\n );\n }\n return;\n }\n if (kind === \"cancel\") {\n this.expectation.fail(\"cancel\", \"terminate\", this.#frameJson());\n return;\n }\n if (!wasRunning) return;\n if (kind === \"crash\") {\n this.expectation.fail(\"crash\", crashDetail ?? \"\", this.#frameJson());\n }\n }\n\n /** Host cancel. A settlement captured while running outranks the cancel. */\n admitTermination(): void {\n this.#commitEnding(\"cancel\", this.#matches(\"running\"));\n }\n\n #frameJson(): string {\n return this.#lastReportJson ?? \"null\";\n }\n\n leave(): void {\n if (!this.#left) {\n this.#left = true;\n for (const dispose of this.#laneDisposers) dispose();\n this.#laneDisposers = [];\n retractRole(this.#hub, this.expectation.uuid);\n }\n this.driverConnection.release(this.expectation);\n }\n}\n"]}
@@ -0,0 +1,144 @@
1
+ import type { PlexusAwareness } from "@here.build/plexus";
2
+ import type { ExpectationActor } from "./ExpectationActor.js";
3
+ import type { AnyWorkExpectation, WorkExpectation } from "../shared/models/WorkExpectation.js";
4
+ import { type RevalidationDemand } from "../shared/revalidation-demand.js";
5
+ import type { ReadyState } from "../shared/types.js";
6
+ /** Duck-typed — drivers may ship a different Zod major than this package. */
7
+ export type ConfigurationSchema = {
8
+ safeParse(data: unknown): {
9
+ readonly success: true;
10
+ readonly data: unknown;
11
+ } | {
12
+ readonly success: false;
13
+ readonly error: {
14
+ readonly issues: ReadonlyArray<{
15
+ readonly message?: string;
16
+ }>;
17
+ };
18
+ };
19
+ };
20
+ /** Process-local. Secrets are never a Plexus field. */
21
+ export type ConnectionSecretsView = {
22
+ readonly revision?: number;
23
+ get(field: string): unknown | undefined;
24
+ /** MCP env_* enumerates deposited fields. */
25
+ all?(): Readonly<Record<string, unknown>> | undefined;
26
+ /** Hosts that persist a refreshed session (reauth) implement this. */
27
+ replace?(secrets: Readonly<Record<string, string>>): void;
28
+ };
29
+ /** Module-graph duplication breaks instanceof; the elevation gate checks this brand too. */
30
+ export declare const PEW_DRIVER_BRAND = "@inhuman.tools/plexus-expectations:PEWDriver@1";
31
+ /** A connection construction the driver already knows will work. */
32
+ export type DriverOffer = {
33
+ readonly name: string;
34
+ readonly description: string;
35
+ readonly live: boolean;
36
+ readonly adoptable: boolean;
37
+ readonly settings: Readonly<Record<string, unknown>>;
38
+ };
39
+ /**
40
+ * Driver extras the orchestrator mirrors onto `spec.live`.
41
+ * Core process facts live on `readyState` / `error` / `offers`.
42
+ */
43
+ export declare function extraLiveFromState(state: object): Record<string, unknown>;
44
+ /**
45
+ * The CLASS is the driver (one loaded module per driver name).
46
+ * The INSTANCE is a live connection (one per connection id).
47
+ *
48
+ * CLAIM DOES NOT LIVE HERE. Which WorkExpectation classes this driver handles
49
+ * lives on the advertised driver record (`can`): dynamically loaded code does
50
+ * not get to declare its own authority.
51
+ *
52
+ * Live state is report only — no machinery branches on it. Instance bootstrap
53
+ * truth is `bootstrapDone` / `bootstrapPending`. Drivers read {@link attention}
54
+ * as the reduced token, never clientIds.
55
+ *
56
+ * YAML doors ({@link configurationSchema} / {@link secretsSchema}) parse
57
+ * without `new`. Secrets stay a live view — the host may replace it (reauth).
58
+ */
59
+ export declare abstract class PEWDriver<E extends AnyWorkExpectation = AnyWorkExpectation, C = Readonly<Record<string, unknown>>> implements AsyncDisposable {
60
+ readonly connectionId: string;
61
+ static readonly pewBrand: string;
62
+ /**
63
+ * YAML settings shape. Default is a passthrough record — subclasses replace
64
+ * this with a real object schema (transforms included). First issue message
65
+ * is what construct / parse throw.
66
+ */
67
+ static readonly configurationSchema: ConfigurationSchema;
68
+ static readonly configurationLabel: string;
69
+ static configurationError(message: string): string;
70
+ /** YAML settings door. Does not construct an instance. */
71
+ static parseConfiguration(settings?: unknown): unknown;
72
+ static readonly secretsSchema: ConfigurationSchema;
73
+ static secretsError(message: string): string;
74
+ /** YAML secrets door. Does not construct an instance. */
75
+ static parseSecrets(secrets?: unknown): unknown;
76
+ /** Shared on the prototype chain — own extras per subclass. */
77
+ static readonly state: Record<string, unknown>;
78
+ /**
79
+ * A plain list, or a getter over observable state when the driver keeps its
80
+ * offers fresh (local detection). The orchestrator mirrors it onto
81
+ * `DriverSpec.offers` with a reaction, which is also what observes it.
82
+ */
83
+ static readonly offers: readonly DriverOffer[];
84
+ /** Kernel-plane occupancy hub. Orchestrator assignment is own-per-class. */
85
+ static awareness: PlexusAwareness | undefined;
86
+ static driverName: string;
87
+ /**
88
+ * A getter, not `@computed` — MobX: the decorator's initializer pins to the
89
+ * declaring class, so subclasses would share one computed.
90
+ */
91
+ static get revalidationDemand(): RevalidationDemand;
92
+ accessor readyState: ReadyState;
93
+ accessor error: string;
94
+ /**
95
+ * MobX: shallow — extras (models, tools, account) are replaced wholesale.
96
+ * Override the accessor with a wider literal to add driver facts.
97
+ */
98
+ accessor state: Record<string, unknown>;
99
+ accessor awareness: PlexusAwareness | undefined;
100
+ get revalidationDemand(): RevalidationDemand;
101
+ constructor(connectionId: string, settings?: unknown, secrets?: ConnectionSecretsView | Readonly<Record<string, unknown>>);
102
+ readonly settings: C;
103
+ /** Live view. The host may replace it (reauth). */
104
+ secrets: ConnectionSecretsView;
105
+ /**
106
+ * Host binds the disk writer for this connection's settings bag.
107
+ * Parsed settings stay readonly; a driver that learns a value overrides this.
108
+ */
109
+ bindSettingsWriter(_write: (patch: Readonly<Record<string, unknown>>) => void): void;
110
+ abstract bootstrap(): Promise<void>;
111
+ bootstrapDone: boolean;
112
+ bootstrapPending: Promise<void> | null;
113
+ /** `resolve()` threw — the connection is broken, not the work. */
114
+ broken: boolean;
115
+ /** Concurrent callers share one promise. Async settlement is its own MobX action. */
116
+ ensureBootstrapStarted(): Promise<void>;
117
+ failReady(error: unknown): void;
118
+ protected abstract resolve(expectation: E): ExpectationActor<E>;
119
+ /** Nothing in PEW calls this; hosts or tests may. */
120
+ [Symbol.asyncDispose](): Promise<void>;
121
+ private readonly actors;
122
+ private readonly liveActors;
123
+ has(expectation: WorkExpectation): boolean;
124
+ resolveExpectation<T extends AnyWorkExpectation>(expectation: T): ExpectationActor<T>;
125
+ release(expectation: WorkExpectation): void;
126
+ /**
127
+ * Logout/drop aborts interned work. Not park. Subclasses abort `#inflight`.
128
+ */
129
+ abortOnDrop(reason: unknown): void;
130
+ /**
131
+ * Honor one demand.
132
+ * `resolve()` throwing is a broken connection, not a failed expectation.
133
+ */
134
+ handle(expectation: WorkExpectation): void;
135
+ }
136
+ /**
137
+ * A concrete `PEWDriver` subclass — constructable, not abstract.
138
+ * `loadDriverModule` returns `unknown`; {@link isDriverConnectionClass} is the gate.
139
+ */
140
+ export type DriverConnectionClass = {
141
+ [K in keyof typeof PEWDriver]: (typeof PEWDriver)[K];
142
+ } & (new (...args: ConstructorParameters<typeof PEWDriver>) => PEWDriver);
143
+ export declare function isDriverConnectionClass(value: unknown): value is DriverConnectionClass;
144
+ //# sourceMappingURL=PEWDriver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PEWDriver.d.ts","sourceRoot":"","sources":["../../src/driver/PEWDriver.ts"],"names":[],"mappings":"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"}