@inhuman.tools/plexus-expectations 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +7 -0
- package/README.md +144 -2
- package/dist/driver/ExpectationActor.d.ts +56 -0
- package/dist/driver/ExpectationActor.d.ts.map +1 -0
- package/dist/driver/ExpectationActor.js +210 -0
- package/dist/driver/ExpectationActor.js.map +1 -0
- package/dist/driver/PEWDriver.d.ts +144 -0
- package/dist/driver/PEWDriver.d.ts.map +1 -0
- package/dist/driver/PEWDriver.js +303 -0
- package/dist/driver/PEWDriver.js.map +1 -0
- package/dist/driver/PEWDriverConnection.d.ts +139 -0
- package/dist/driver/PEWDriverConnection.d.ts.map +1 -0
- package/dist/driver/PEWDriverConnection.js +298 -0
- package/dist/driver/PEWDriverConnection.js.map +1 -0
- package/dist/driver/actor-machine.d.ts +28 -0
- package/dist/driver/actor-machine.d.ts.map +1 -0
- package/dist/driver/actor-machine.js +50 -0
- package/dist/driver/actor-machine.js.map +1 -0
- package/dist/driver/connection-miss.d.ts +9 -0
- package/dist/driver/connection-miss.d.ts.map +1 -0
- package/dist/driver/connection-miss.js +13 -0
- package/dist/driver/connection-miss.js.map +1 -0
- package/dist/driver/index.d.ts +6 -0
- package/dist/driver/index.d.ts.map +1 -0
- package/dist/driver/index.js +5 -0
- package/dist/driver/index.js.map +1 -0
- package/dist/error-with-invariant.d.ts +9 -0
- package/dist/error-with-invariant.d.ts.map +1 -0
- package/dist/error-with-invariant.js +12 -0
- package/dist/error-with-invariant.js.map +1 -0
- package/dist/executor/index.d.ts +4 -0
- package/dist/executor/index.d.ts.map +1 -0
- package/dist/executor/index.js +4 -0
- package/dist/executor/index.js.map +1 -0
- package/dist/executor/kernel-intern.d.ts +16 -0
- package/dist/executor/kernel-intern.d.ts.map +1 -0
- package/dist/executor/kernel-intern.js +98 -0
- package/dist/executor/kernel-intern.js.map +1 -0
- package/dist/executor/orchestrator.d.ts +97 -0
- package/dist/executor/orchestrator.d.ts.map +1 -0
- package/dist/executor/orchestrator.js +465 -0
- package/dist/executor/orchestrator.js.map +1 -0
- package/dist/executor/types.d.ts +8 -0
- package/dist/executor/types.d.ts.map +1 -0
- package/dist/executor/types.js +2 -0
- package/dist/executor/types.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas/connection.d.ts +4 -0
- package/dist/schemas/connection.d.ts.map +1 -0
- package/dist/schemas/connection.js +4 -0
- package/dist/schemas/connection.js.map +1 -0
- package/dist/schemas/index.d.ts +2 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +2 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/shared/attention.d.ts +27 -0
- package/dist/shared/attention.d.ts.map +1 -0
- package/dist/shared/attention.js +116 -0
- package/dist/shared/attention.js.map +1 -0
- package/dist/shared/control.d.ts +4 -0
- package/dist/shared/control.d.ts.map +1 -0
- package/dist/shared/control.js +2 -0
- package/dist/shared/control.js.map +1 -0
- package/dist/shared/index.d.ts +7 -0
- package/dist/shared/index.d.ts.map +1 -0
- package/dist/shared/index.js +7 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/shared/map-field-awareness.d.ts +19 -0
- package/dist/shared/map-field-awareness.d.ts.map +1 -0
- package/dist/shared/map-field-awareness.js +58 -0
- package/dist/shared/map-field-awareness.js.map +1 -0
- package/dist/shared/models/ApprovableExpectation.d.ts +15 -0
- package/dist/shared/models/ApprovableExpectation.d.ts.map +1 -0
- package/dist/shared/models/ApprovableExpectation.js +138 -0
- package/dist/shared/models/ApprovableExpectation.js.map +1 -0
- package/dist/shared/models/ConnectionSpec.d.ts +44 -0
- package/dist/shared/models/ConnectionSpec.d.ts.map +1 -0
- package/dist/shared/models/ConnectionSpec.js +180 -0
- package/dist/shared/models/ConnectionSpec.js.map +1 -0
- package/dist/shared/models/DriverSpec.d.ts +45 -0
- package/dist/shared/models/DriverSpec.d.ts.map +1 -0
- package/dist/shared/models/DriverSpec.js +221 -0
- package/dist/shared/models/DriverSpec.js.map +1 -0
- package/dist/shared/models/IntrospectingPlexusModel.d.ts +8 -0
- package/dist/shared/models/IntrospectingPlexusModel.d.ts.map +1 -0
- package/dist/shared/models/IntrospectingPlexusModel.js +48 -0
- package/dist/shared/models/IntrospectingPlexusModel.js.map +1 -0
- package/dist/shared/models/Job.d.ts +85 -0
- package/dist/shared/models/Job.d.ts.map +1 -0
- package/dist/shared/models/Job.js +261 -0
- package/dist/shared/models/Job.js.map +1 -0
- package/dist/shared/models/ProgrammaticJob.d.ts +6 -0
- package/dist/shared/models/ProgrammaticJob.d.ts.map +1 -0
- package/dist/shared/models/ProgrammaticJob.js +80 -0
- package/dist/shared/models/ProgrammaticJob.js.map +1 -0
- package/dist/shared/models/WorkContainer.d.ts +36 -0
- package/dist/shared/models/WorkContainer.d.ts.map +1 -0
- package/dist/shared/models/WorkContainer.js +140 -0
- package/dist/shared/models/WorkContainer.js.map +1 -0
- package/dist/shared/models/WorkExpectation.d.ts +63 -0
- package/dist/shared/models/WorkExpectation.d.ts.map +1 -0
- package/dist/shared/models/WorkExpectation.js +290 -0
- package/dist/shared/models/WorkExpectation.js.map +1 -0
- package/dist/shared/models/WorkExpectationWithIntrospection.d.ts +4 -0
- package/dist/shared/models/WorkExpectationWithIntrospection.d.ts.map +1 -0
- package/dist/shared/models/WorkExpectationWithIntrospection.js +28 -0
- package/dist/shared/models/WorkExpectationWithIntrospection.js.map +1 -0
- package/dist/shared/models/index.d.ts +7 -0
- package/dist/shared/models/index.d.ts.map +1 -0
- package/dist/shared/models/index.js +6 -0
- package/dist/shared/models/index.js.map +1 -0
- package/dist/shared/models/jsonRecord.d.ts +14 -0
- package/dist/shared/models/jsonRecord.d.ts.map +1 -0
- package/dist/shared/models/jsonRecord.js +46 -0
- package/dist/shared/models/jsonRecord.js.map +1 -0
- package/dist/shared/resolution.d.ts +39 -0
- package/dist/shared/resolution.d.ts.map +1 -0
- package/dist/shared/resolution.js +124 -0
- package/dist/shared/resolution.js.map +1 -0
- package/dist/shared/revalidation-demand.d.ts +27 -0
- package/dist/shared/revalidation-demand.d.ts.map +1 -0
- package/dist/shared/revalidation-demand.js +116 -0
- package/dist/shared/revalidation-demand.js.map +1 -0
- package/dist/shared/role.d.ts +12 -0
- package/dist/shared/role.d.ts.map +1 -0
- package/dist/shared/role.js +21 -0
- package/dist/shared/role.js.map +1 -0
- package/dist/shared/types.d.ts +43 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +12 -0
- package/dist/shared/types.js.map +1 -0
- package/package.json +79 -3
- package/src/driver/ExpectationActor.ts +244 -0
- package/src/driver/PEWDriver.ts +302 -0
- package/src/driver/actor-machine.ts +54 -0
- package/src/driver/connection-miss.ts +13 -0
- package/src/driver/index.ts +5 -0
- package/src/executor/index.ts +3 -0
- package/src/executor/kernel-intern.ts +121 -0
- package/src/executor/orchestrator.ts +463 -0
- package/src/executor/types.ts +3 -0
- package/src/index.ts +1 -0
- package/src/schemas/connection.ts +4 -0
- package/src/schemas/index.ts +1 -0
- package/src/shared/control.ts +4 -0
- package/src/shared/index.ts +6 -0
- package/src/shared/map-field-awareness.ts +65 -0
- package/src/shared/models/ConnectionSpec.ts +84 -0
- package/src/shared/models/DriverSpec.ts +102 -0
- package/src/shared/models/Job.ts +192 -0
- package/src/shared/models/WorkContainer.ts +90 -0
- package/src/shared/models/WorkExpectation.ts +229 -0
- package/src/shared/models/index.ts +6 -0
- package/src/shared/models/jsonRecord.ts +49 -0
- package/src/shared/revalidation-demand.ts +121 -0
- package/src/shared/role.ts +32 -0
- package/src/shared/types.ts +57 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAML connection inventory. Settings ride a private synced bag; the public
|
|
3
|
+
* face is {@link configuration} (deep-readonly get, whole-bag set).
|
|
4
|
+
* Secrets never live here. PEW process facts are `readyState` / `error`
|
|
5
|
+
* plus {@link live}. YAML file fill does not touch process facts.
|
|
6
|
+
*/
|
|
7
|
+
import { PlexusModel, syncing } from "@here.build/plexus";
|
|
8
|
+
import { computed } from "mobx";
|
|
9
|
+
|
|
10
|
+
import type { ReadyState, SpecCatalog } from "../types.js";
|
|
11
|
+
import { DriverSpec } from "./DriverSpec.js";
|
|
12
|
+
import { readJsonRecord, writeJsonRecord, type LiveFacts } from "./jsonRecord.js";
|
|
13
|
+
|
|
14
|
+
export type ConfigurationAtom = string | number | boolean | null;
|
|
15
|
+
export type ConfigurationValue =
|
|
16
|
+
| ConfigurationAtom
|
|
17
|
+
| readonly ConfigurationValue[]
|
|
18
|
+
| { readonly [key: string]: ConfigurationValue };
|
|
19
|
+
export type ConnectionConfiguration = { readonly [key: string]: ConfigurationValue };
|
|
20
|
+
|
|
21
|
+
@syncing("@inhuman.tools/plexus-expectations:ConnectionSpec")
|
|
22
|
+
export class ConnectionSpec extends PlexusModel<SpecCatalog | null> {
|
|
23
|
+
get id(): string {
|
|
24
|
+
return (this.parentFieldKey as string | null) ?? "";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Only `failed` is a problem. */
|
|
28
|
+
get issue(): "failed" | false {
|
|
29
|
+
return this.readyState === "failed" ? "failed" : false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** YAML `driver:` resolves to this entity. */
|
|
33
|
+
@syncing accessor driver!: DriverSpec;
|
|
34
|
+
@syncing accessor name: string = "";
|
|
35
|
+
@syncing accessor annotation: string = "";
|
|
36
|
+
@syncing.record private accessor configurationMap: Record<string, string> = {};
|
|
37
|
+
|
|
38
|
+
/** PEW instance self-report. `null` = never ensured. YAML fill does not touch this. */
|
|
39
|
+
@syncing accessor readyState: ReadyState = null;
|
|
40
|
+
@syncing accessor error: string = "";
|
|
41
|
+
@syncing.record private accessor liveMap: Record<string, string> = {};
|
|
42
|
+
|
|
43
|
+
/** Frozen through; assign the whole bag to write. */
|
|
44
|
+
@computed
|
|
45
|
+
get configuration(): ConnectionConfiguration {
|
|
46
|
+
return readJsonRecord(this.configurationMap) as ConnectionConfiguration;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
set configuration(settings: Readonly<Record<string, unknown>> | undefined) {
|
|
50
|
+
this.#writeConfiguration(settings);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@syncing.action
|
|
54
|
+
#writeConfiguration(settings: Readonly<Record<string, unknown>> | undefined): void {
|
|
55
|
+
writeJsonRecord(this.configurationMap, settings);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@computed
|
|
59
|
+
get live(): LiveFacts {
|
|
60
|
+
return readJsonRecord(this.liveMap) as LiveFacts;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
set live(next: Readonly<Record<string, unknown>> | undefined) {
|
|
64
|
+
this.#writeLive(next);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@syncing.action
|
|
68
|
+
#writeLive(next: Readonly<Record<string, unknown>> | undefined): void {
|
|
69
|
+
writeJsonRecord(this.liveMap, next);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Process self-report. YAML fill does not call this. */
|
|
73
|
+
@syncing.action
|
|
74
|
+
stampProcess(next: {
|
|
75
|
+
readonly readyState: ReadyState;
|
|
76
|
+
readonly error: string;
|
|
77
|
+
readonly live: Readonly<Record<string, unknown>> | undefined;
|
|
78
|
+
}): void {
|
|
79
|
+
this.readyState = next.readyState;
|
|
80
|
+
this.error = next.error;
|
|
81
|
+
this.live = next.live;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advertised driver inventory. `can` is a plexus list — file tokens, no load required.
|
|
3
|
+
* PEW process facts are `readyState` / `error` / `offersJson` plus {@link live}.
|
|
4
|
+
* YAML file fill does not touch process facts.
|
|
5
|
+
*/
|
|
6
|
+
import { PlexusModel, syncing } from "@here.build/plexus";
|
|
7
|
+
import { computed } from "mobx";
|
|
8
|
+
import invariant from "tiny-invariant";
|
|
9
|
+
|
|
10
|
+
import type { ReadyState, SpecCatalog } from "../types.js";
|
|
11
|
+
import { readJsonRecord, writeJsonRecord, type LiveFacts } from "./jsonRecord.js";
|
|
12
|
+
|
|
13
|
+
/** How a new connection of this driver is offered, before the class loads. */
|
|
14
|
+
export type DriverMultiplicity = "add-by-default" | "add-as-secondary";
|
|
15
|
+
|
|
16
|
+
@syncing("@inhuman.tools/plexus-expectations:DriverSpec")
|
|
17
|
+
export class DriverSpec extends PlexusModel<SpecCatalog | null> {
|
|
18
|
+
@syncing accessor name: string = "";
|
|
19
|
+
@syncing accessor description: string = "";
|
|
20
|
+
@syncing accessor importPath: string = "";
|
|
21
|
+
/** Expectation kind names this driver handles. Not tool names. */
|
|
22
|
+
@syncing.list accessor abilityClaim: string[] = [];
|
|
23
|
+
@syncing accessor multiplicity: DriverMultiplicity = "add-as-secondary";
|
|
24
|
+
|
|
25
|
+
/** Module-load self-report. `null` = never loaded. YAML fill does not touch this. */
|
|
26
|
+
@syncing accessor readyState: ReadyState = null;
|
|
27
|
+
@syncing accessor error: string = "";
|
|
28
|
+
/** Offer list JSON. Null = no offers. */
|
|
29
|
+
@syncing private accessor offersJson: string | null = null;
|
|
30
|
+
@syncing.record private accessor liveMap: Record<string, string> = {};
|
|
31
|
+
|
|
32
|
+
@computed
|
|
33
|
+
get live(): LiveFacts {
|
|
34
|
+
return readJsonRecord(this.liveMap) as LiveFacts;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
set live(next: Readonly<Record<string, unknown>> | undefined) {
|
|
38
|
+
this.#writeLive(next);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@syncing.action
|
|
42
|
+
#writeLive(next: Readonly<Record<string, unknown>> | undefined): void {
|
|
43
|
+
writeJsonRecord(this.liveMap, next);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Module-load self-report. Omitted `offersJson` / `live` stay
|
|
48
|
+
* (failure must not wipe a previous load).
|
|
49
|
+
*/
|
|
50
|
+
@syncing.action
|
|
51
|
+
stampLoad(next: {
|
|
52
|
+
readonly readyState: ReadyState;
|
|
53
|
+
readonly error: string;
|
|
54
|
+
readonly offersJson?: string | null;
|
|
55
|
+
readonly live?: Readonly<Record<string, unknown>>;
|
|
56
|
+
}): void {
|
|
57
|
+
this.readyState = next.readyState;
|
|
58
|
+
this.error = next.error;
|
|
59
|
+
if (next.offersJson !== undefined) this.offersJson = next.offersJson;
|
|
60
|
+
if (next.live !== undefined) this.live = next.live;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Class-bootstrap offers. Null, blank, and the string `"null"` are no offers.
|
|
65
|
+
* A non-string cell or malformed JSON throws: a silent [] hides a driver defect.
|
|
66
|
+
*/
|
|
67
|
+
@computed
|
|
68
|
+
get offers(): readonly object[] {
|
|
69
|
+
return readDriverOffers(this.offersJson);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
set offers(next: readonly object[] | null) {
|
|
73
|
+
this.#writeOffers(next);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@syncing.action
|
|
77
|
+
#writeOffers(next: readonly object[] | null): void {
|
|
78
|
+
this.offersJson = next == null ? null : JSON.stringify(next);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function readDriverOffers(raw: unknown): object[] {
|
|
83
|
+
if (raw == null) return [];
|
|
84
|
+
invariant(typeof raw === "string", "harness/kernel/offers: offersJson must be a JSON string");
|
|
85
|
+
const text = raw.trim();
|
|
86
|
+
if (text === "" || text === "null") return [];
|
|
87
|
+
let parsed: unknown;
|
|
88
|
+
try {
|
|
89
|
+
parsed = JSON.parse(text);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`harness/kernel/offers: offersJson is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
invariant(Array.isArray(parsed), "harness/kernel/offers: offersJson must be a JSON array of offer objects");
|
|
96
|
+
for (const item of parsed) {
|
|
97
|
+
invariant(typeof item === "object", "harness/kernel/offers: offersJson entries must be objects");
|
|
98
|
+
invariant(item !== null, "harness/kernel/offers: offersJson entries cannot be null");
|
|
99
|
+
invariant(!Array.isArray(item), "harness/kernel/offers: offersJson entries cannot be arrays");
|
|
100
|
+
}
|
|
101
|
+
return parsed as object[];
|
|
102
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* COLLECTORS ARE NOT `.work`. Collectors expand until `walkBoundary`.
|
|
3
|
+
* Job is a WorkContainer, not a WorkExpectation.
|
|
4
|
+
*
|
|
5
|
+
* `orchestrationResult` is the guard's boolean. `orchestrated` is computed
|
|
6
|
+
* (`!== null`). `cancelled` is the cancel fact. Do not assign `orchestrated`.
|
|
7
|
+
*
|
|
8
|
+
* Await settles on `result`, not on this object. `result` stays null until
|
|
9
|
+
* `endedAtMs` is set, then this class fulfills `undefined`. `cancelled` alone
|
|
10
|
+
* does not settle. A subclass names its value and rejection by overriding `result`.
|
|
11
|
+
*/
|
|
12
|
+
import { syncing } from "@here.build/plexus";
|
|
13
|
+
import { computed, when } from "mobx";
|
|
14
|
+
import invariant from "tiny-invariant";
|
|
15
|
+
|
|
16
|
+
import { WorkContainer } from "./WorkContainer.js";
|
|
17
|
+
|
|
18
|
+
export type JobResolution<TValue, TReason> =
|
|
19
|
+
{ readonly status: "fulfilled"; readonly value: TValue } | { readonly status: "rejected"; readonly reason: TReason };
|
|
20
|
+
|
|
21
|
+
export type JobKind = "command" | "agent" | "bot" | "workflow";
|
|
22
|
+
|
|
23
|
+
/** Runtime that owns a job's effect. */
|
|
24
|
+
export type JobEffectOwner = "session-runtime" | "tool-runtime" | "scheme-runtime" | "agent-calls" | "honor";
|
|
25
|
+
|
|
26
|
+
/** What a restart does with a job whose outcome it cannot see. */
|
|
27
|
+
export type UnknownOutcomeRecovery = "resume-queued" | "resume-inner" | "unknown-no-replay";
|
|
28
|
+
|
|
29
|
+
@syncing("@inhuman.tools/plexus-expectations:Job")
|
|
30
|
+
export abstract class Job<TValue = unknown, TReason = unknown> extends WorkContainer implements Promise<TValue> {
|
|
31
|
+
#pending: Promise<TValue> | null = null;
|
|
32
|
+
|
|
33
|
+
/** Not this instantiation — a parent holds jobs with other fulfill and reject types. */
|
|
34
|
+
@syncing.child.list accessor children: Job<unknown, unknown>[] = [];
|
|
35
|
+
@syncing accessor background: boolean = false;
|
|
36
|
+
@syncing accessor walkBoundary: boolean = false;
|
|
37
|
+
/** Guard return. `null` = phase 1 not done. */
|
|
38
|
+
@syncing accessor orchestrationResult: true | false | null = null;
|
|
39
|
+
/** Cancel fact. `false → true`. Not a deny and not a human approval. */
|
|
40
|
+
@syncing accessor cancelled: boolean = false;
|
|
41
|
+
/** This job's own work has finalized (ok or not). */
|
|
42
|
+
@syncing accessor completed: boolean = false;
|
|
43
|
+
/**
|
|
44
|
+
* Crash ending: the license guard or the executor body threw. Empty means
|
|
45
|
+
* no crash. The message is the fact; the host stamps a receipt from it and
|
|
46
|
+
* the job completes, so whoever joins the job is released with an outcome.
|
|
47
|
+
*/
|
|
48
|
+
@syncing accessor crash: string = "";
|
|
49
|
+
/**
|
|
50
|
+
* Ordering as data: jobs that must be released before this one is claimed
|
|
51
|
+
* (license and body). Refs to earlier siblings, never a cycle. The
|
|
52
|
+
* orchestrator honours it on every pass, so it holds across a restart.
|
|
53
|
+
*/
|
|
54
|
+
@syncing.list accessor after: Job<unknown, unknown>[] = [];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Seal + end clock, in one update: `result` keys on `endedAtMs`, so a
|
|
58
|
+
* crash between the two writes would leave a completed job unawaitable.
|
|
59
|
+
* Deny-without-BODY leaves `startedAtMs` null.
|
|
60
|
+
*/
|
|
61
|
+
@syncing.action
|
|
62
|
+
markComplete(now = Date.now()): void {
|
|
63
|
+
if (!this.completed) this.completed = true;
|
|
64
|
+
this.markEnded(now);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Replace {@link after}. Call before the job becomes reachable. */
|
|
68
|
+
@syncing.action
|
|
69
|
+
stampAfter(jobs: readonly Job<unknown, unknown>[]): void {
|
|
70
|
+
this.after.splice(0, this.after.length, ...jobs);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Released for a successor: done, cancelled, or a background job that has
|
|
75
|
+
* started. A denied or failed predecessor is done too; order is not success.
|
|
76
|
+
*/
|
|
77
|
+
get released(): boolean {
|
|
78
|
+
return this.completed || this.cancelled || (this.orchestrated && this.background);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Every {@link after} predecessor is released. */
|
|
82
|
+
@computed
|
|
83
|
+
get afterReleased(): boolean {
|
|
84
|
+
return this.after.every((job) => job.released);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Parent `child` when it is not already in `children`. Does not enqueue or wait. */
|
|
88
|
+
registerJob<T extends Job<unknown, unknown>>(child: T): T {
|
|
89
|
+
if (!this.children.includes(child)) this.children.push(child);
|
|
90
|
+
return child;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@computed
|
|
94
|
+
get orchestrated(): boolean {
|
|
95
|
+
return this.orchestrationResult !== null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Not one call. Agent, bot, shell, and program override. */
|
|
99
|
+
get kind(): JobKind {
|
|
100
|
+
return "workflow";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Not the background flag. Call, program, and shell override. */
|
|
104
|
+
get isTrackedBackground(): boolean {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Not the wire name. Call, program, and shell override. */
|
|
109
|
+
get object(): string {
|
|
110
|
+
return "";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Effect contract for this class. Not synced. An abstract intermediate
|
|
115
|
+
* declares none, so every concrete kind has to state its own.
|
|
116
|
+
*/
|
|
117
|
+
abstract readonly owner: JobEffectOwner;
|
|
118
|
+
abstract readonly intent: string;
|
|
119
|
+
abstract readonly checkpoint: string;
|
|
120
|
+
abstract readonly dispatch: string;
|
|
121
|
+
abstract readonly settlement: string;
|
|
122
|
+
abstract readonly cancel: string;
|
|
123
|
+
abstract readonly unknownRecovery: UnknownOutcomeRecovery;
|
|
124
|
+
abstract readonly unknownNote: string;
|
|
125
|
+
abstract readonly intentRequired: boolean;
|
|
126
|
+
|
|
127
|
+
@computed
|
|
128
|
+
get jobs(): Job<unknown, unknown>[] {
|
|
129
|
+
const out: Job<unknown, unknown>[] = [];
|
|
130
|
+
for (const child of this.children) {
|
|
131
|
+
if (!(child instanceof Job)) continue;
|
|
132
|
+
out.push(child);
|
|
133
|
+
if (!child.walkBoundary) out.push(...child.jobs);
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@computed
|
|
139
|
+
get foregroundJobs(): Job<unknown, unknown>[] {
|
|
140
|
+
return this.jobs.filter((node) => !node.background);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@computed
|
|
144
|
+
get backgroundJobs(): Job<unknown, unknown>[] {
|
|
145
|
+
return this.jobs.filter((node) => node.background);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@computed
|
|
149
|
+
get result(): JobResolution<TValue, TReason> | null {
|
|
150
|
+
if (this.endedAtMs == null) return null;
|
|
151
|
+
return { status: "fulfilled", value: undefined as TValue };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@computed
|
|
155
|
+
get work(): readonly WorkContainer[] {
|
|
156
|
+
return this.children;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
then<TFulfilled = TValue, TRejected = never>(
|
|
160
|
+
onfulfilled?: ((value: TValue) => TFulfilled | PromiseLike<TFulfilled>) | null,
|
|
161
|
+
onrejected?: ((reason: TReason) => TRejected | PromiseLike<TRejected>) | null,
|
|
162
|
+
): Promise<TFulfilled | TRejected> {
|
|
163
|
+
return this.#settled().then(onfulfilled, onrejected);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
catch<TRejected = never>(
|
|
167
|
+
onrejected?: ((reason: TReason) => TRejected | PromiseLike<TRejected>) | null,
|
|
168
|
+
): Promise<TValue | TRejected> {
|
|
169
|
+
return this.#settled().catch(onrejected);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
finally(onfinally?: (() => void) | null): Promise<TValue> {
|
|
173
|
+
return this.#settled().finally(onfinally);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
get [Symbol.toStringTag](): string {
|
|
177
|
+
return "Job";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#settled(): Promise<TValue> {
|
|
181
|
+
if (this.#pending != null) return this.#pending;
|
|
182
|
+
const deliver = (): TValue => {
|
|
183
|
+
const resolution = this.result;
|
|
184
|
+
invariant(resolution != null, "Job settled without result");
|
|
185
|
+
if (resolution.status === "rejected") throw resolution.reason;
|
|
186
|
+
return resolution.value;
|
|
187
|
+
};
|
|
188
|
+
this.#pending =
|
|
189
|
+
this.result != null ? Promise.resolve().then(deliver) : when(() => this.result != null).then(deliver);
|
|
190
|
+
return this.#pending;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work graph node. `work` may be a synced list or a computed getter.
|
|
3
|
+
* Walking `.work` from the document root is the whole out-reachable set.
|
|
4
|
+
*
|
|
5
|
+
* Brand is on this class — do not duck-type `.work`.
|
|
6
|
+
*
|
|
7
|
+
* Thenable. The fulfillment is not `this` — a thenable that resolves to itself
|
|
8
|
+
* never settles. This class settles when `endedAtMs` is set, fulfills
|
|
9
|
+
* `undefined`, and does not reject. {@link WorkExpectation} fulfills its
|
|
10
|
+
* outcome. {@link Job} fulfills or rejects from `result`. The promise value
|
|
11
|
+
* here is `unknown` so both stay containers.
|
|
12
|
+
*/
|
|
13
|
+
import { PlexusModel, syncing } from "@here.build/plexus";
|
|
14
|
+
import { when } from "mobx";
|
|
15
|
+
|
|
16
|
+
export const workContainer: unique symbol = Symbol.for("work:container") as never;
|
|
17
|
+
|
|
18
|
+
/** Base promise value. An expectation narrows it to the outcome; a job narrows it per class. */
|
|
19
|
+
export type WorkFulfillment = unknown;
|
|
20
|
+
|
|
21
|
+
@syncing("@inhuman.tools/plexus-expectations:WorkContainer")
|
|
22
|
+
export abstract class WorkContainer extends PlexusModel implements Promise<WorkFulfillment> {
|
|
23
|
+
readonly [workContainer] = true as const;
|
|
24
|
+
#settledPromise: Promise<WorkFulfillment> | null = null;
|
|
25
|
+
|
|
26
|
+
/** Epoch ms. Write-once. BODY / actor start — not mint, not the guard. */
|
|
27
|
+
@syncing accessor startedAtMs: number | null = null;
|
|
28
|
+
/** Epoch ms. Write-once. Job complete / PEW succeed|fail. This class's thenable settles on this. */
|
|
29
|
+
@syncing accessor endedAtMs: number | null = null;
|
|
30
|
+
|
|
31
|
+
abstract readonly work: readonly WorkContainer[];
|
|
32
|
+
|
|
33
|
+
markStarted(now = Date.now()): void {
|
|
34
|
+
if (this.startedAtMs != null) return;
|
|
35
|
+
this.startedAtMs = now;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
markEnded(now = Date.now()): void {
|
|
39
|
+
if (this.endedAtMs != null) return;
|
|
40
|
+
this.endedAtMs = now;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Live: `now - startedAtMs`. Sealed: `endedAtMs - startedAtMs`. Null if start was never stamped. */
|
|
44
|
+
durationMs(now = Date.now()): number | null {
|
|
45
|
+
if (this.startedAtMs == null || !Number.isFinite(this.startedAtMs)) return null;
|
|
46
|
+
const end =
|
|
47
|
+
this.endedAtMs != null && Number.isFinite(this.endedAtMs) ? this.endedAtMs : now;
|
|
48
|
+
return Math.max(0, end - this.startedAtMs);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
then<TFulfilled = WorkFulfillment, TRejected = never>(
|
|
52
|
+
onfulfilled?: ((value: WorkFulfillment) => TFulfilled | PromiseLike<TFulfilled>) | null,
|
|
53
|
+
onrejected?: ((reason: unknown) => TRejected | PromiseLike<TRejected>) | null,
|
|
54
|
+
): Promise<TFulfilled | TRejected> {
|
|
55
|
+
return this.#settled().then(onfulfilled, onrejected);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
catch<TRejected = never>(
|
|
59
|
+
onrejected?: ((reason: unknown) => TRejected | PromiseLike<TRejected>) | null,
|
|
60
|
+
): Promise<WorkFulfillment | TRejected> {
|
|
61
|
+
return this.#settled().catch(onrejected);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
finally(onfinally?: (() => void) | null): Promise<WorkFulfillment> {
|
|
65
|
+
return this.#settled().finally(onfinally);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get [Symbol.toStringTag](): string {
|
|
69
|
+
return "WorkContainer";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#settled(): Promise<WorkFulfillment> {
|
|
73
|
+
if (this.#settledPromise != null) return this.#settledPromise;
|
|
74
|
+
const fulfill = (): WorkFulfillment => undefined;
|
|
75
|
+
this.#settledPromise =
|
|
76
|
+
this.endedAtMs != null ? Promise.resolve(fulfill()) : when(() => this.endedAtMs != null).then(fulfill);
|
|
77
|
+
return this.#settledPromise;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Preorder walk of `.work`, cycle-safe. `item === container` stops a leaf `[this]` from recursing. */
|
|
82
|
+
export function* walkWork(container: WorkContainer, seen: Set<WorkContainer> = new Set()): Generator<WorkContainer> {
|
|
83
|
+
for (const item of container.work) {
|
|
84
|
+
if (seen.has(item)) continue;
|
|
85
|
+
seen.add(item);
|
|
86
|
+
yield item;
|
|
87
|
+
if (item === container) continue;
|
|
88
|
+
yield* walkWork(item, seen);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { type PlexusAwareness, syncing } from "@here.build/plexus";
|
|
2
|
+
import { docPlexus } from "@here.build/plexus/internals";
|
|
3
|
+
import { comparer, computed, observable, reaction, when } from "mobx";
|
|
4
|
+
import invariant from "tiny-invariant";
|
|
5
|
+
|
|
6
|
+
import { type EndCause, type ExpectationOutcome } from "../control.js";
|
|
7
|
+
import { roleOf } from "../role.js";
|
|
8
|
+
import type { SessionAwareness, SessionPlexus } from "../types.js";
|
|
9
|
+
import { Job } from "./Job.js";
|
|
10
|
+
import { WorkContainer } from "./WorkContainer.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Awareness: empty string occupies the role map without a report frame.
|
|
14
|
+
* Retract drops the uuid. Not a durable outcome.
|
|
15
|
+
*/
|
|
16
|
+
export const ROLE_LANE_OCCUPIED = "";
|
|
17
|
+
|
|
18
|
+
@syncing("@inhuman.tools/plexus-expectations:WorkExpectation")
|
|
19
|
+
export abstract class WorkExpectation<TResult = unknown, TReport = unknown>
|
|
20
|
+
extends WorkContainer
|
|
21
|
+
implements Promise<ExpectationOutcome>
|
|
22
|
+
{
|
|
23
|
+
/**
|
|
24
|
+
* Kernel catalog key this work rides. Required at construction.
|
|
25
|
+
* Plexus materialization passes a symbol through unchanged.
|
|
26
|
+
*/
|
|
27
|
+
@syncing
|
|
28
|
+
accessor connectionId!: string;
|
|
29
|
+
|
|
30
|
+
constructor(init: { readonly connectionId: string } | symbol) {
|
|
31
|
+
if (typeof init !== "symbol") {
|
|
32
|
+
invariant(
|
|
33
|
+
typeof init.connectionId === "string" && init.connectionId !== "",
|
|
34
|
+
"WorkExpectation.connectionId is required",
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
super(init);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@syncing
|
|
41
|
+
accessor outcome: ExpectationOutcome | null = null;
|
|
42
|
+
|
|
43
|
+
@syncing
|
|
44
|
+
accessor endCause: EndCause | null = null;
|
|
45
|
+
|
|
46
|
+
@syncing
|
|
47
|
+
accessor endDetail: string | null = null;
|
|
48
|
+
|
|
49
|
+
/** Durable last frame as JSON. `"null"` when the work ended without a report. */
|
|
50
|
+
@syncing
|
|
51
|
+
accessor outcomeReport: string | null = null;
|
|
52
|
+
|
|
53
|
+
/** Last live role-map frame this process saw. Retract does not clear it. */
|
|
54
|
+
@observable.ref private accessor lastProgress: TReport | null = null;
|
|
55
|
+
#progressCacheStarted = false;
|
|
56
|
+
#settledPromise: Promise<ExpectationOutcome> | null = null;
|
|
57
|
+
|
|
58
|
+
get plexus(): SessionPlexus | null {
|
|
59
|
+
const doc = this.__doc__;
|
|
60
|
+
if (!doc) return null;
|
|
61
|
+
const plexus = docPlexus.get(doc);
|
|
62
|
+
if (!plexus) return null;
|
|
63
|
+
return plexus as SessionPlexus;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get awareness(): PlexusAwareness<SessionAwareness> | null {
|
|
67
|
+
const hub = this.plexus?.awareness ?? null;
|
|
68
|
+
if (hub != null) this.#ensureProgressCache();
|
|
69
|
+
return hub;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// MobX: arm on the first `awareness` read so a later untracked read still
|
|
73
|
+
// sees the last frame. Dispose on done — a sealed lane is frozen; do not
|
|
74
|
+
// keep a reaction per expectation for the life of the document.
|
|
75
|
+
#ensureProgressCache(): void {
|
|
76
|
+
if (this.#progressCacheStarted) return;
|
|
77
|
+
this.#progressCacheStarted = true;
|
|
78
|
+
reaction(
|
|
79
|
+
() => ({ done: this.isDone, frame: this.#liveProgressFrame() }),
|
|
80
|
+
({ done, frame }, _previous, self) => {
|
|
81
|
+
if (frame !== null) this.lastProgress = frame;
|
|
82
|
+
if (done) self.dispose();
|
|
83
|
+
},
|
|
84
|
+
{ fireImmediately: true, equals: comparer.structural },
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#liveProgressFrame(): TReport | null {
|
|
89
|
+
const hub = this.awareness;
|
|
90
|
+
if (hub == null) return null;
|
|
91
|
+
const frame = roleOf(hub, this.uuid);
|
|
92
|
+
if (frame == null || frame === ROLE_LANE_OCCUPIED) return null;
|
|
93
|
+
return frame as TReport;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Live progress as the fulfiller tells it. Awareness retract does not drop the last frame. */
|
|
97
|
+
@computed
|
|
98
|
+
get progressAwareness(): TReport | null {
|
|
99
|
+
return this.#liveProgressFrame() ?? this.lastProgress;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Occupancy on the role map. Not a durable outcome. */
|
|
103
|
+
@computed
|
|
104
|
+
get workerPresent(): boolean {
|
|
105
|
+
const hub = this.awareness;
|
|
106
|
+
if (hub == null) return false;
|
|
107
|
+
const value = roleOf(hub, this.uuid);
|
|
108
|
+
return value !== undefined && value !== null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
get isDone(): boolean {
|
|
112
|
+
return this.outcome != null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Leaf. PEW does not parent PEW. */
|
|
116
|
+
get work(): readonly WorkExpectation[] {
|
|
117
|
+
return [this];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Settles when {@link isDone}. Fulfills with {@link outcome} for both
|
|
122
|
+
* `successful` and `failed` — failure is an outcome, not a rejection.
|
|
123
|
+
* The fulfillment cannot be `this`: a thenable that resolves to itself
|
|
124
|
+
* never settles (Promise unwraps `then` forever).
|
|
125
|
+
*/
|
|
126
|
+
then<TFulfilled = ExpectationOutcome, TRejected = never>(
|
|
127
|
+
onfulfilled?: ((value: ExpectationOutcome) => TFulfilled | PromiseLike<TFulfilled>) | null,
|
|
128
|
+
onrejected?: ((reason: unknown) => TRejected | PromiseLike<TRejected>) | null,
|
|
129
|
+
): Promise<TFulfilled | TRejected> {
|
|
130
|
+
return this.#settled().then(onfulfilled, onrejected);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
catch<TRejected = never>(
|
|
134
|
+
onrejected?: ((reason: unknown) => TRejected | PromiseLike<TRejected>) | null,
|
|
135
|
+
): Promise<ExpectationOutcome | TRejected> {
|
|
136
|
+
return this.#settled().catch(onrejected);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
finally(onfinally?: (() => void) | null): Promise<ExpectationOutcome> {
|
|
140
|
+
return this.#settled().finally(onfinally);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
get [Symbol.toStringTag](): string {
|
|
144
|
+
return "WorkExpectation";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#settled(): Promise<ExpectationOutcome> {
|
|
148
|
+
if (this.#settledPromise != null) return this.#settledPromise;
|
|
149
|
+
this.#settledPromise = this.isDone
|
|
150
|
+
? Promise.resolve(this.#sealedOutcome())
|
|
151
|
+
: when(() => this.isDone).then(() => this.#sealedOutcome());
|
|
152
|
+
return this.#settledPromise;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#sealedOutcome(): ExpectationOutcome {
|
|
156
|
+
invariant(this.outcome != null, "WorkExpectation settled without outcome");
|
|
157
|
+
return this.outcome;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
applySettlement(_result: TResult): void {}
|
|
161
|
+
|
|
162
|
+
#parseOutcomeReport(): TReport | null {
|
|
163
|
+
if (this.outcomeReport == null) return null;
|
|
164
|
+
try {
|
|
165
|
+
return JSON.parse(this.outcomeReport) as TReport | null;
|
|
166
|
+
} catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Durable outcome frame if written, else last progress seen.
|
|
173
|
+
* {@link ROLE_LANE_OCCUPIED} is not a frame. Retract keeps the last frame.
|
|
174
|
+
*/
|
|
175
|
+
@computed
|
|
176
|
+
get report(): TReport | null {
|
|
177
|
+
if (this.outcomeReport != null) return this.#parseOutcomeReport();
|
|
178
|
+
return this.progressAwareness;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@syncing.action
|
|
182
|
+
succeed(cause: EndCause, detail: string, outcomeReport: string, settlement?: { readonly result: TResult }): boolean {
|
|
183
|
+
if (this.isDone) return false;
|
|
184
|
+
if (settlement !== undefined) {
|
|
185
|
+
try {
|
|
186
|
+
this.applySettlement(settlement.result);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
this.outcome = "failed";
|
|
189
|
+
this.endCause = cause;
|
|
190
|
+
this.endDetail = `apply_error: ${String(error)}`;
|
|
191
|
+
this.outcomeReport = outcomeReport;
|
|
192
|
+
this.markEnded();
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
this.outcome = "successful";
|
|
197
|
+
this.endCause = cause;
|
|
198
|
+
this.endDetail = detail;
|
|
199
|
+
this.outcomeReport = outcomeReport;
|
|
200
|
+
this.markEnded();
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
@syncing.action
|
|
205
|
+
fail(cause: EndCause, detail: string, outcomeReport: string): boolean {
|
|
206
|
+
if (this.isDone) return false;
|
|
207
|
+
this.outcome = "failed";
|
|
208
|
+
this.endCause = cause;
|
|
209
|
+
this.endDetail = detail;
|
|
210
|
+
this.outcomeReport = outcomeReport;
|
|
211
|
+
this.markEnded();
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Open work whose parent sealed — honor does not start these. */
|
|
217
|
+
export function isTreeOrphan(node: WorkExpectation): boolean {
|
|
218
|
+
if (node.isDone) return false;
|
|
219
|
+
const parent = node.parent;
|
|
220
|
+
if (parent === null) return false;
|
|
221
|
+
if (parent instanceof WorkExpectation) return parent.isDone;
|
|
222
|
+
if (parent instanceof Job) return parent.completed;
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Admits every WorkExpectation instantiation. */
|
|
227
|
+
export type AnyWorkExpectation = WorkExpectation<any, any>;
|
|
228
|
+
export type ResultOf<E extends AnyWorkExpectation> = Parameters<E["applySettlement"]>[0];
|
|
229
|
+
export type ReportOf<E extends AnyWorkExpectation> = Exclude<E["report"], null>;
|