@company-semantics/contracts 90.0.0 → 91.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/package.json +2 -2
- package/src/affordances/README.md +65 -0
- package/src/affordances/__tests__/README.md +30 -0
- package/src/affordances/__tests__/affordances.test.ts +251 -0
- package/src/affordances/affordance.ts +124 -0
- package/src/affordances/args.ts +59 -0
- package/src/affordances/availability.ts +78 -0
- package/src/affordances/ids.ts +45 -0
- package/src/affordances/index.ts +39 -0
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +458 -2
- package/src/generated/openapi-routes.ts +4 -0
- package/src/proactive/README.md +26 -4
- package/src/proactive/__tests__/README.md +10 -0
- package/src/proactive/__tests__/chat-templates.test.ts +8 -0
- package/src/proactive/__tests__/vocabulary.test.ts +11 -0
- package/src/proactive/__tests__/workflow-attention-fixtures.ts +49 -0
- package/src/proactive/__tests__/workflow-attention.test.ts +146 -0
- package/src/proactive/composer.ts +8 -0
- package/src/proactive/facts.ts +36 -0
- package/src/proactive/index.ts +2 -0
- package/src/proactive/kinds.ts +29 -0
- package/src/proactive/registry.ts +22 -0
- package/src/proactive/surfaces.ts +2 -0
- package/src/proactive/templates/README.md +9 -1
- package/src/proactive/templates/index.ts +2 -0
- package/src/proactive/templates/workflow-needs-attention.ts +35 -0
- package/src/root-exports/README.md +1 -0
- package/src/root-exports/delivery.ts +69 -1
- package/src/user-notifications/README.md +8 -0
- package/src/user-notifications/kinds.ts +13 -0
- package/src/user-notifications/lifecycle-registry.ts +10 -0
- package/src/workflows/README.md +44 -6
- package/src/workflows/__tests__/README.md +21 -2
- package/src/workflows/__tests__/affordances-of-outcome.test.ts +96 -0
- package/src/workflows/__tests__/deferred-route.test.ts +167 -0
- package/src/workflows/__tests__/outcome.test.ts +68 -3
- package/src/workflows/affordances-of-outcome.ts +64 -0
- package/src/workflows/deferred-route.ts +242 -0
- package/src/workflows/index.ts +51 -11
- package/src/workflows/outcome-reasons.ts +244 -0
- package/src/workflows/outcome.ts +50 -148
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The workflow-settlement offer rule and its projection (control ADR
|
|
3
|
+
* affordance-system; control ADR-CTRL-540).
|
|
4
|
+
*
|
|
5
|
+
* `affordancesOfReason` is the ONE place a producer reads the frozen offer for
|
|
6
|
+
* an ending (the backend's outcome builder calls it). `affordancesOfOutcome` is
|
|
7
|
+
* the ONE projection every consumer renders from — the settlement message, the
|
|
8
|
+
* proactive chat and the agent's context alike — so the Review control and the
|
|
9
|
+
* offered next steps are one list built one way.
|
|
10
|
+
*/
|
|
11
|
+
import type { Affordance, OfferedAffordance } from "../affordances/affordance";
|
|
12
|
+
import type { WorkflowOutcome, WorkflowOutcomeOf } from "./outcome";
|
|
13
|
+
import {
|
|
14
|
+
DISCOVER_AFFORDANCES_OF_REASON,
|
|
15
|
+
ORG_STRUCTURE_AFFORDANCES_OF_REASON,
|
|
16
|
+
SYNTHESIZE_AFFORDANCES_OF_REASON,
|
|
17
|
+
} from "./outcome-reasons";
|
|
18
|
+
import type { WorkflowType } from "./types";
|
|
19
|
+
|
|
20
|
+
/** Every workflow type's offer table, total over the type union. */
|
|
21
|
+
export const WORKFLOW_AFFORDANCES_OF_REASON = {
|
|
22
|
+
"source-sync/discover": DISCOVER_AFFORDANCES_OF_REASON,
|
|
23
|
+
"source-sync/synthesize": SYNTHESIZE_AFFORDANCES_OF_REASON,
|
|
24
|
+
"org-structure/hris-connect": ORG_STRUCTURE_AFFORDANCES_OF_REASON,
|
|
25
|
+
"org-structure/reanalyze": ORG_STRUCTURE_AFFORDANCES_OF_REASON,
|
|
26
|
+
} as const satisfies {
|
|
27
|
+
readonly [T in WorkflowType]: Record<
|
|
28
|
+
WorkflowOutcomeOf<T>["reason"],
|
|
29
|
+
readonly OfferedAffordance[]
|
|
30
|
+
>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** The frozen offer an ending of one workflow type carries. */
|
|
34
|
+
export function affordancesOfReason<T extends WorkflowType>(
|
|
35
|
+
workflowType: T,
|
|
36
|
+
reason: WorkflowOutcomeOf<T>["reason"],
|
|
37
|
+
): readonly OfferedAffordance[] {
|
|
38
|
+
const table = WORKFLOW_AFFORDANCES_OF_REASON[workflowType] as Record<
|
|
39
|
+
string,
|
|
40
|
+
readonly OfferedAffordance[]
|
|
41
|
+
>;
|
|
42
|
+
return table[reason as string] ?? [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Everything an outcome offers, in render order: the frozen offer, then the
|
|
47
|
+
* `review.open` projected from a `review` disposition. Pure; never persisted.
|
|
48
|
+
*/
|
|
49
|
+
export function affordancesOfOutcome(
|
|
50
|
+
outcome: WorkflowOutcome,
|
|
51
|
+
): readonly Affordance[] {
|
|
52
|
+
const { disposition } = outcome;
|
|
53
|
+
if (disposition.kind !== "review") return [...outcome.affordances];
|
|
54
|
+
return [
|
|
55
|
+
...outcome.affordances,
|
|
56
|
+
{
|
|
57
|
+
id: "review.open",
|
|
58
|
+
args: {
|
|
59
|
+
requestType: disposition.requestType,
|
|
60
|
+
requestId: disposition.requestId,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a settlement goes when there is no launching conversation to write it
|
|
3
|
+
* into (control ADR-CTRL-540 phase 2; control ADR affordance-system).
|
|
4
|
+
*
|
|
5
|
+
* A scheduled rediscovery, an upkeep re-synthesis or a launch nobody docked
|
|
6
|
+
* into a chat settles with no anchor. Its route is decided per REASON, frozen by
|
|
7
|
+
* the backend onto the settlement row when it defers, and never re-derived.
|
|
8
|
+
*
|
|
9
|
+
* THE DOCTRINE (control docs/proactive-delivery.md): a pushed `explained` chat
|
|
10
|
+
* is frozen, so it may contain only material that stays non-privileged after
|
|
11
|
+
* the recipient's authority changes. A settlement told WITHOUT FACTS (its
|
|
12
|
+
* reason only, `facts: null`) plus its affordance ids qualifies — affordances
|
|
13
|
+
* are re-gated per viewer at render and executed by the server. An ending whose
|
|
14
|
+
* telling needs authority-sensitive facts (counts) goes to a `notice` inbox row,
|
|
15
|
+
* which re-renders under the reader's current authority.
|
|
16
|
+
*
|
|
17
|
+
* RULES (pinned by the deferred-route test):
|
|
18
|
+
* - `explained` only for a `failed` or `abandoned` ending that offers at least
|
|
19
|
+
* one affordance. A `produced`, `empty` or `partial` ending is never explained.
|
|
20
|
+
* - `covered` when a domain kind normally announces that ending. Covering is
|
|
21
|
+
* verified, not assumed: the backend checks that the covering occurrence
|
|
22
|
+
* exists for this workflow and epoch at route time, and otherwise routes the
|
|
23
|
+
* `fallback` notice to the stated audience. Nothing is ever said twice, and
|
|
24
|
+
* nothing falls silent because a domain kind declined.
|
|
25
|
+
* - `silent` only for `stopped_by_user`: the person who stopped it acted.
|
|
26
|
+
* - Audiences are policies the backend resolves and freezes at emit; an empty
|
|
27
|
+
* resolved audience routes silent.
|
|
28
|
+
*/
|
|
29
|
+
import { z } from "zod";
|
|
30
|
+
|
|
31
|
+
import type {
|
|
32
|
+
DiscoverWorkflowOutcome,
|
|
33
|
+
HrisConnectWorkflowOutcome,
|
|
34
|
+
ReanalyzeWorkflowOutcome,
|
|
35
|
+
SynthesizeWorkflowOutcome,
|
|
36
|
+
WorkflowOutcome,
|
|
37
|
+
WorkflowOutcomeOf,
|
|
38
|
+
} from "./outcome";
|
|
39
|
+
import type {
|
|
40
|
+
DiscoverOutcomeReason,
|
|
41
|
+
OrgStructureOutcomeReason,
|
|
42
|
+
SynthesizeOutcomeReason,
|
|
43
|
+
} from "./outcome-reasons";
|
|
44
|
+
import type { WorkflowType } from "./types";
|
|
45
|
+
|
|
46
|
+
/** Who a routed settlement is addressed to; resolved and frozen by the backend. */
|
|
47
|
+
export const SETTLEMENT_AUDIENCE_POLICIES = [
|
|
48
|
+
/** The person who launched the workflow, while still an active member. */
|
|
49
|
+
"creator",
|
|
50
|
+
/** The launcher plus the org owner and admins (who manage integrations). */
|
|
51
|
+
"creator_and_owner_admins",
|
|
52
|
+
] as const;
|
|
53
|
+
export type SettlementAudiencePolicy =
|
|
54
|
+
(typeof SETTLEMENT_AUDIENCE_POLICIES)[number];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The proactive kinds that already announce some endings. Literal strings, not
|
|
58
|
+
* an import: workflows never imports proactive (proactive imports workflows).
|
|
59
|
+
* The test pins each one as a real PROACTIVE_EVENT_KIND_IDS member.
|
|
60
|
+
*/
|
|
61
|
+
export const SETTLEMENT_COVERING_KINDS = [
|
|
62
|
+
"sourceSync.review_ready",
|
|
63
|
+
"sourceSync.docs_updated",
|
|
64
|
+
"orgStructure.review_ready",
|
|
65
|
+
] as const;
|
|
66
|
+
export type SettlementCoveringKind = (typeof SETTLEMENT_COVERING_KINDS)[number];
|
|
67
|
+
|
|
68
|
+
/** Why a routed settlement says nothing. Closed. */
|
|
69
|
+
export const SETTLEMENT_SILENT_REASONS = ["stopped_by_person"] as const;
|
|
70
|
+
|
|
71
|
+
export const DEFERRED_ROUTES = [
|
|
72
|
+
"explained",
|
|
73
|
+
"notice",
|
|
74
|
+
"covered",
|
|
75
|
+
"silent",
|
|
76
|
+
] as const;
|
|
77
|
+
export type DeferredRouteKind = (typeof DEFERRED_ROUTES)[number];
|
|
78
|
+
|
|
79
|
+
export const DeferredRouteSchema = z.discriminatedUnion("route", [
|
|
80
|
+
z
|
|
81
|
+
.object({
|
|
82
|
+
route: z.literal("explained"),
|
|
83
|
+
audience: z.enum(SETTLEMENT_AUDIENCE_POLICIES),
|
|
84
|
+
})
|
|
85
|
+
.strict(),
|
|
86
|
+
z
|
|
87
|
+
.object({
|
|
88
|
+
route: z.literal("notice"),
|
|
89
|
+
audience: z.enum(SETTLEMENT_AUDIENCE_POLICIES),
|
|
90
|
+
})
|
|
91
|
+
.strict(),
|
|
92
|
+
z
|
|
93
|
+
.object({
|
|
94
|
+
route: z.literal("covered"),
|
|
95
|
+
by: z.enum(SETTLEMENT_COVERING_KINDS),
|
|
96
|
+
/**
|
|
97
|
+
* Covering is not literal on every path (a domain kind may decline: e.g.
|
|
98
|
+
* sourceSync.review_ready fires on scheduled rediscovery only when new
|
|
99
|
+
* candidates exist). The backend verifies the covering occurrence exists
|
|
100
|
+
* for this workflow/epoch at route time; otherwise it routes the notice.
|
|
101
|
+
*/
|
|
102
|
+
fallback: z.literal("notice"),
|
|
103
|
+
/** Who the fallback notice is addressed to. */
|
|
104
|
+
audience: z.enum(SETTLEMENT_AUDIENCE_POLICIES),
|
|
105
|
+
})
|
|
106
|
+
.strict(),
|
|
107
|
+
z
|
|
108
|
+
.object({
|
|
109
|
+
route: z.literal("silent"),
|
|
110
|
+
why: z.enum(SETTLEMENT_SILENT_REASONS),
|
|
111
|
+
})
|
|
112
|
+
.strict(),
|
|
113
|
+
]);
|
|
114
|
+
export type DeferredRoute = z.infer<typeof DeferredRouteSchema>;
|
|
115
|
+
|
|
116
|
+
const EXPLAINED = {
|
|
117
|
+
route: "explained",
|
|
118
|
+
audience: "creator_and_owner_admins",
|
|
119
|
+
} as const;
|
|
120
|
+
const NOTICE_CREATOR = { route: "notice", audience: "creator" } as const;
|
|
121
|
+
const NOTICE_ADMINS = {
|
|
122
|
+
route: "notice",
|
|
123
|
+
audience: "creator_and_owner_admins",
|
|
124
|
+
} as const;
|
|
125
|
+
const STOPPED = { route: "silent", why: "stopped_by_person" } as const;
|
|
126
|
+
|
|
127
|
+
export const DISCOVER_DEFERRED_ROUTE_OF_REASON = {
|
|
128
|
+
// prettier-ignore
|
|
129
|
+
candidates_found: { route: "covered", by: "sourceSync.review_ready", fallback: "notice", audience: "creator" },
|
|
130
|
+
nothing_found: NOTICE_CREATOR,
|
|
131
|
+
nothing_relevant: NOTICE_CREATOR,
|
|
132
|
+
budget_exhausted: NOTICE_CREATOR,
|
|
133
|
+
file_ceiling: NOTICE_CREATOR,
|
|
134
|
+
stopped_by_user: STOPPED,
|
|
135
|
+
plan_coverage_refused: NOTICE_ADMINS,
|
|
136
|
+
billing_refused: EXPLAINED,
|
|
137
|
+
drive_auth: EXPLAINED,
|
|
138
|
+
drive_unavailable: EXPLAINED,
|
|
139
|
+
no_connection: EXPLAINED,
|
|
140
|
+
source_sync_off: EXPLAINED,
|
|
141
|
+
internal_error: EXPLAINED,
|
|
142
|
+
abandoned: EXPLAINED,
|
|
143
|
+
unrecognized_terminal_state: NOTICE_ADMINS,
|
|
144
|
+
} as const satisfies Record<DiscoverOutcomeReason, DeferredRoute>;
|
|
145
|
+
|
|
146
|
+
export const SYNTHESIZE_DEFERRED_ROUTE_OF_REASON = {
|
|
147
|
+
// prettier-ignore
|
|
148
|
+
updates_ready: { route: "covered", by: "sourceSync.review_ready", fallback: "notice", audience: "creator" },
|
|
149
|
+
// prettier-ignore
|
|
150
|
+
updates_applied: { route: "covered", by: "sourceSync.docs_updated", fallback: "notice", audience: "creator" },
|
|
151
|
+
stopped_by_user: STOPPED,
|
|
152
|
+
paused_billing: EXPLAINED,
|
|
153
|
+
synthesis_incomplete: NOTICE_ADMINS,
|
|
154
|
+
approval_mismatch: NOTICE_ADMINS,
|
|
155
|
+
landing_refused: NOTICE_ADMINS,
|
|
156
|
+
standing_order_refused: NOTICE_ADMINS,
|
|
157
|
+
no_connection: EXPLAINED,
|
|
158
|
+
source_sync_off: EXPLAINED,
|
|
159
|
+
internal_error: NOTICE_ADMINS,
|
|
160
|
+
abandoned: NOTICE_ADMINS,
|
|
161
|
+
unrecognized_terminal_state: NOTICE_ADMINS,
|
|
162
|
+
} as const satisfies Record<SynthesizeOutcomeReason, DeferredRoute>;
|
|
163
|
+
|
|
164
|
+
export const ORG_STRUCTURE_DEFERRED_ROUTE_OF_REASON = {
|
|
165
|
+
// prettier-ignore
|
|
166
|
+
proposal_ready: { route: "covered", by: "orgStructure.review_ready", fallback: "notice", audience: "creator" },
|
|
167
|
+
directory_synced: NOTICE_CREATOR,
|
|
168
|
+
analysis_already_running: NOTICE_CREATOR,
|
|
169
|
+
proposal_blocked: NOTICE_ADMINS,
|
|
170
|
+
// `empty`, so never explained, even though it offers a directory sync.
|
|
171
|
+
no_sync_run: NOTICE_ADMINS,
|
|
172
|
+
no_persons: NOTICE_CREATOR,
|
|
173
|
+
reporting_not_keyed: NOTICE_CREATOR,
|
|
174
|
+
stopped_by_user: STOPPED,
|
|
175
|
+
sync_failed: EXPLAINED,
|
|
176
|
+
snapshot_too_large: NOTICE_ADMINS,
|
|
177
|
+
model_failed: EXPLAINED,
|
|
178
|
+
run_too_large: NOTICE_ADMINS,
|
|
179
|
+
placement_failed: EXPLAINED,
|
|
180
|
+
internal_error: EXPLAINED,
|
|
181
|
+
abandoned: EXPLAINED,
|
|
182
|
+
unrecognized_terminal_state: NOTICE_ADMINS,
|
|
183
|
+
} as const satisfies Record<OrgStructureOutcomeReason, DeferredRoute>;
|
|
184
|
+
|
|
185
|
+
/** Every workflow type's route table, total over the type union. */
|
|
186
|
+
export const WORKFLOW_DEFERRED_ROUTE_OF_REASON = {
|
|
187
|
+
"source-sync/discover": DISCOVER_DEFERRED_ROUTE_OF_REASON,
|
|
188
|
+
"source-sync/synthesize": SYNTHESIZE_DEFERRED_ROUTE_OF_REASON,
|
|
189
|
+
"org-structure/hris-connect": ORG_STRUCTURE_DEFERRED_ROUTE_OF_REASON,
|
|
190
|
+
"org-structure/reanalyze": ORG_STRUCTURE_DEFERRED_ROUTE_OF_REASON,
|
|
191
|
+
} as const satisfies {
|
|
192
|
+
readonly [T in WorkflowType]: Record<
|
|
193
|
+
WorkflowOutcomeOf<T>["reason"],
|
|
194
|
+
DeferredRoute
|
|
195
|
+
>;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
type ExplainedReasonOf<T> = {
|
|
199
|
+
[R in keyof T]: T[R] extends { readonly route: "explained" } ? R : never;
|
|
200
|
+
}[keyof T];
|
|
201
|
+
|
|
202
|
+
export type ExplainedDiscoverReason = ExplainedReasonOf<
|
|
203
|
+
typeof DISCOVER_DEFERRED_ROUTE_OF_REASON
|
|
204
|
+
>;
|
|
205
|
+
export type ExplainedSynthesizeReason = ExplainedReasonOf<
|
|
206
|
+
typeof SYNTHESIZE_DEFERRED_ROUTE_OF_REASON
|
|
207
|
+
>;
|
|
208
|
+
export type ExplainedOrgStructureReason = ExplainedReasonOf<
|
|
209
|
+
typeof ORG_STRUCTURE_DEFERRED_ROUTE_OF_REASON
|
|
210
|
+
>;
|
|
211
|
+
|
|
212
|
+
type FactFree<O, R> = Omit<O, "reason" | "facts"> & {
|
|
213
|
+
readonly reason: R;
|
|
214
|
+
readonly facts: null;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/** An outcome a pushed chat may carry: an explained reason, facts stripped. */
|
|
218
|
+
export type ExplainedWorkflowOutcome =
|
|
219
|
+
| FactFree<DiscoverWorkflowOutcome, ExplainedDiscoverReason>
|
|
220
|
+
| FactFree<SynthesizeWorkflowOutcome, ExplainedSynthesizeReason>
|
|
221
|
+
| FactFree<HrisConnectWorkflowOutcome, ExplainedOrgStructureReason>
|
|
222
|
+
| FactFree<ReanalyzeWorkflowOutcome, ExplainedOrgStructureReason>;
|
|
223
|
+
|
|
224
|
+
/** The route an outcome takes when it has no launching conversation. */
|
|
225
|
+
export function deferredRouteOf(outcome: WorkflowOutcome): DeferredRoute {
|
|
226
|
+
const table = WORKFLOW_DEFERRED_ROUTE_OF_REASON[
|
|
227
|
+
outcome.workflowType
|
|
228
|
+
] as Record<string, DeferredRoute>;
|
|
229
|
+
return table[outcome.reason] as DeferredRoute;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The fact-free outcome a pushed chat may carry, or null when the ending's
|
|
234
|
+
* route is not `explained`. Stripping facts keeps the outcome schema-valid
|
|
235
|
+
* (`facts` is nullable) and leaves the frozen affordances untouched.
|
|
236
|
+
*/
|
|
237
|
+
export function toExplainedOutcome(
|
|
238
|
+
outcome: WorkflowOutcome,
|
|
239
|
+
): ExplainedWorkflowOutcome | null {
|
|
240
|
+
if (deferredRouteOf(outcome).route !== "explained") return null;
|
|
241
|
+
return { ...outcome, facts: null } as ExplainedWorkflowOutcome;
|
|
242
|
+
}
|
package/src/workflows/index.ts
CHANGED
|
@@ -14,32 +14,72 @@ export { ANALYSIS_WORKFLOW_TYPES, isWorkflowType } from "./types";
|
|
|
14
14
|
export type {
|
|
15
15
|
DiscoverOutcomeFacts,
|
|
16
16
|
DiscoverOutcomeReason,
|
|
17
|
-
DiscoverWorkflowOutcome,
|
|
18
|
-
HrisConnectWorkflowOutcome,
|
|
19
17
|
OrgStructureOutcomeFacts,
|
|
20
18
|
OrgStructureOutcomeReason,
|
|
21
|
-
ReanalyzeWorkflowOutcome,
|
|
22
19
|
SynthesizeOutcomeFacts,
|
|
23
20
|
SynthesizeOutcomeReason,
|
|
21
|
+
WorkflowOutcomeKind,
|
|
22
|
+
} from "./outcome-reasons";
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
DISCOVER_AFFORDANCES_OF_REASON,
|
|
26
|
+
DISCOVER_OUTCOME_KIND_OF_REASON,
|
|
27
|
+
DiscoverOutcomeFactsSchema,
|
|
28
|
+
ORG_STRUCTURE_AFFORDANCES_OF_REASON,
|
|
29
|
+
ORG_STRUCTURE_OUTCOME_KIND_OF_REASON,
|
|
30
|
+
OrgStructureOutcomeFactsSchema,
|
|
31
|
+
SYNTHESIZE_AFFORDANCES_OF_REASON,
|
|
32
|
+
SYNTHESIZE_OUTCOME_KIND_OF_REASON,
|
|
33
|
+
SynthesizeOutcomeFactsSchema,
|
|
34
|
+
WORKFLOW_OUTCOME_KINDS,
|
|
35
|
+
} from "./outcome-reasons";
|
|
36
|
+
|
|
37
|
+
export type {
|
|
38
|
+
DiscoverWorkflowOutcome,
|
|
39
|
+
HrisConnectWorkflowOutcome,
|
|
40
|
+
ReanalyzeWorkflowOutcome,
|
|
24
41
|
SynthesizeWorkflowOutcome,
|
|
25
42
|
WorkflowOutcome,
|
|
26
43
|
WorkflowOutcomeDisposition,
|
|
27
|
-
WorkflowOutcomeKind,
|
|
28
44
|
WorkflowOutcomeOf,
|
|
29
45
|
} from "./outcome";
|
|
30
46
|
|
|
31
47
|
export {
|
|
32
|
-
DISCOVER_OUTCOME_KIND_OF_REASON,
|
|
33
|
-
DiscoverOutcomeFactsSchema,
|
|
34
48
|
DiscoverWorkflowOutcomeSchema,
|
|
35
49
|
HrisConnectWorkflowOutcomeSchema,
|
|
36
|
-
ORG_STRUCTURE_OUTCOME_KIND_OF_REASON,
|
|
37
|
-
OrgStructureOutcomeFactsSchema,
|
|
38
50
|
ReanalyzeWorkflowOutcomeSchema,
|
|
39
|
-
SYNTHESIZE_OUTCOME_KIND_OF_REASON,
|
|
40
|
-
SynthesizeOutcomeFactsSchema,
|
|
41
51
|
SynthesizeWorkflowOutcomeSchema,
|
|
42
|
-
WORKFLOW_OUTCOME_KINDS,
|
|
43
52
|
WorkflowOutcomeDispositionSchema,
|
|
44
53
|
WorkflowOutcomeSchema,
|
|
45
54
|
} from "./outcome";
|
|
55
|
+
|
|
56
|
+
export {
|
|
57
|
+
WORKFLOW_AFFORDANCES_OF_REASON,
|
|
58
|
+
affordancesOfOutcome,
|
|
59
|
+
affordancesOfReason,
|
|
60
|
+
} from "./affordances-of-outcome";
|
|
61
|
+
|
|
62
|
+
export type {
|
|
63
|
+
DeferredRoute,
|
|
64
|
+
DeferredRouteKind,
|
|
65
|
+
ExplainedDiscoverReason,
|
|
66
|
+
ExplainedOrgStructureReason,
|
|
67
|
+
ExplainedSynthesizeReason,
|
|
68
|
+
ExplainedWorkflowOutcome,
|
|
69
|
+
SettlementAudiencePolicy,
|
|
70
|
+
SettlementCoveringKind,
|
|
71
|
+
} from "./deferred-route";
|
|
72
|
+
|
|
73
|
+
export {
|
|
74
|
+
DEFERRED_ROUTES,
|
|
75
|
+
DISCOVER_DEFERRED_ROUTE_OF_REASON,
|
|
76
|
+
DeferredRouteSchema,
|
|
77
|
+
ORG_STRUCTURE_DEFERRED_ROUTE_OF_REASON,
|
|
78
|
+
SETTLEMENT_AUDIENCE_POLICIES,
|
|
79
|
+
SETTLEMENT_COVERING_KINDS,
|
|
80
|
+
SETTLEMENT_SILENT_REASONS,
|
|
81
|
+
SYNTHESIZE_DEFERRED_ROUTE_OF_REASON,
|
|
82
|
+
WORKFLOW_DEFERRED_ROUTE_OF_REASON,
|
|
83
|
+
deferredRouteOf,
|
|
84
|
+
toExplainedOutcome,
|
|
85
|
+
} from "./deferred-route";
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-type vocabulary of a workflow outcome (control ADR-CTRL-540): the
|
|
3
|
+
* shared `kind` list and, for every workflow type, its closed `reason` union,
|
|
4
|
+
* the `kind` each reason fixes, the affordances each reason offers, and the
|
|
5
|
+
* typed `facts` it reports.
|
|
6
|
+
*
|
|
7
|
+
* Tables only. `./outcome.ts` builds the outcome schemas from them and states
|
|
8
|
+
* the invariants they uphold (per-type and closed, kind and offer as functions
|
|
9
|
+
* of reason, facts are counts).
|
|
10
|
+
*
|
|
11
|
+
* An ending is one row in a `*_OUTCOME_KIND_OF_REASON` table and one in the
|
|
12
|
+
* matching `*_AFFORDANCES_OF_REASON` table, whose `satisfies` refuses a
|
|
13
|
+
* missing row.
|
|
14
|
+
*/
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
|
|
17
|
+
import type { OfferedAffordance } from "../affordances/affordance";
|
|
18
|
+
|
|
19
|
+
/** The shape of an ending, shared by every workflow type. */
|
|
20
|
+
export const WORKFLOW_OUTCOME_KINDS = [
|
|
21
|
+
/** The run produced what it is for. */
|
|
22
|
+
"produced",
|
|
23
|
+
/** The run finished and produced nothing. */
|
|
24
|
+
"empty",
|
|
25
|
+
/** The run finished short of full coverage (a budget or ceiling). */
|
|
26
|
+
"partial",
|
|
27
|
+
/** A person stopped the run; Continue may begin a new execution epoch. */
|
|
28
|
+
"stopped",
|
|
29
|
+
/** The run failed. */
|
|
30
|
+
"failed",
|
|
31
|
+
/** The run went silent and the reconciler closed it. */
|
|
32
|
+
"abandoned",
|
|
33
|
+
/** A terminal transition the classifier did not recognize. */
|
|
34
|
+
"unknown",
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
37
|
+
export type WorkflowOutcomeKind = (typeof WORKFLOW_OUTCOME_KINDS)[number];
|
|
38
|
+
|
|
39
|
+
const Count = z.number().int().nonnegative();
|
|
40
|
+
|
|
41
|
+
// The affordances an ending may offer, named once for the tables below.
|
|
42
|
+
const FIND_FILES = { id: "sourceSync.findFiles" } as const;
|
|
43
|
+
const CONNECT_GOOGLE_DRIVE = { id: "integrations.connectGoogleDrive" } as const;
|
|
44
|
+
const ENABLE_SOURCE_SYNC = { id: "sourceSync.enable" } as const;
|
|
45
|
+
const OPEN_BILLING = { id: "billing.open" } as const;
|
|
46
|
+
const REANALYZE_STRUCTURE = { id: "orgStructure.reanalyze" } as const;
|
|
47
|
+
const RESYNC_DIRECTORY = { id: "hris.resyncDirectory" } as const;
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// source-sync/discover
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
/** Why a Drive discovery run ended the way it did. */
|
|
54
|
+
export const DISCOVER_OUTCOME_KIND_OF_REASON = {
|
|
55
|
+
candidates_found: "produced",
|
|
56
|
+
nothing_found: "empty",
|
|
57
|
+
nothing_relevant: "empty",
|
|
58
|
+
budget_exhausted: "partial",
|
|
59
|
+
file_ceiling: "partial",
|
|
60
|
+
stopped_by_user: "stopped",
|
|
61
|
+
plan_coverage_refused: "failed",
|
|
62
|
+
billing_refused: "failed",
|
|
63
|
+
drive_auth: "failed",
|
|
64
|
+
drive_unavailable: "failed",
|
|
65
|
+
no_connection: "failed",
|
|
66
|
+
source_sync_off: "failed",
|
|
67
|
+
internal_error: "failed",
|
|
68
|
+
abandoned: "abandoned",
|
|
69
|
+
unrecognized_terminal_state: "unknown",
|
|
70
|
+
} as const satisfies Record<string, WorkflowOutcomeKind>;
|
|
71
|
+
|
|
72
|
+
export type DiscoverOutcomeReason =
|
|
73
|
+
keyof typeof DISCOVER_OUTCOME_KIND_OF_REASON;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* What each discovery ending offers (control ADR affordance-system). TOTAL: an
|
|
77
|
+
* empty list is a decision that no grounded action changes that ending — a
|
|
78
|
+
* re-run of the same search is not "broadening" it, and nothing raises a budget.
|
|
79
|
+
*/
|
|
80
|
+
export const DISCOVER_AFFORDANCES_OF_REASON = {
|
|
81
|
+
candidates_found: [],
|
|
82
|
+
nothing_found: [],
|
|
83
|
+
nothing_relevant: [],
|
|
84
|
+
budget_exhausted: [],
|
|
85
|
+
file_ceiling: [],
|
|
86
|
+
stopped_by_user: [],
|
|
87
|
+
plan_coverage_refused: [],
|
|
88
|
+
billing_refused: [OPEN_BILLING],
|
|
89
|
+
drive_auth: [CONNECT_GOOGLE_DRIVE],
|
|
90
|
+
drive_unavailable: [FIND_FILES],
|
|
91
|
+
no_connection: [CONNECT_GOOGLE_DRIVE],
|
|
92
|
+
source_sync_off: [ENABLE_SOURCE_SYNC],
|
|
93
|
+
internal_error: [FIND_FILES],
|
|
94
|
+
abandoned: [FIND_FILES],
|
|
95
|
+
unrecognized_terminal_state: [],
|
|
96
|
+
} as const satisfies Record<
|
|
97
|
+
DiscoverOutcomeReason,
|
|
98
|
+
readonly OfferedAffordance[]
|
|
99
|
+
>;
|
|
100
|
+
|
|
101
|
+
/** The counts a discovery run reports at its end. */
|
|
102
|
+
export const DiscoverOutcomeFactsSchema = z
|
|
103
|
+
.object({
|
|
104
|
+
probesTotal: Count,
|
|
105
|
+
probesDone: Count,
|
|
106
|
+
filesSeen: Count,
|
|
107
|
+
filesShown: Count,
|
|
108
|
+
filesHidden: Count,
|
|
109
|
+
filesDropped: Count,
|
|
110
|
+
coveragePercent: z.number().min(0).max(100).nullable(),
|
|
111
|
+
})
|
|
112
|
+
.strict();
|
|
113
|
+
|
|
114
|
+
export type DiscoverOutcomeFacts = z.infer<typeof DiscoverOutcomeFactsSchema>;
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// source-sync/synthesize
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
/** Why a synthesis (landing then drafting) run ended the way it did. */
|
|
121
|
+
export const SYNTHESIZE_OUTCOME_KIND_OF_REASON = {
|
|
122
|
+
updates_ready: "produced",
|
|
123
|
+
updates_applied: "produced",
|
|
124
|
+
stopped_by_user: "stopped",
|
|
125
|
+
paused_billing: "failed",
|
|
126
|
+
synthesis_incomplete: "failed",
|
|
127
|
+
approval_mismatch: "failed",
|
|
128
|
+
landing_refused: "failed",
|
|
129
|
+
standing_order_refused: "failed",
|
|
130
|
+
no_connection: "failed",
|
|
131
|
+
source_sync_off: "failed",
|
|
132
|
+
internal_error: "failed",
|
|
133
|
+
abandoned: "abandoned",
|
|
134
|
+
unrecognized_terminal_state: "unknown",
|
|
135
|
+
} as const satisfies Record<string, WorkflowOutcomeKind>;
|
|
136
|
+
|
|
137
|
+
export type SynthesizeOutcomeReason =
|
|
138
|
+
keyof typeof SYNTHESIZE_OUTCOME_KIND_OF_REASON;
|
|
139
|
+
|
|
140
|
+
/** What each synthesis ending offers. TOTAL; an empty list is explicit. */
|
|
141
|
+
export const SYNTHESIZE_AFFORDANCES_OF_REASON = {
|
|
142
|
+
updates_ready: [],
|
|
143
|
+
updates_applied: [],
|
|
144
|
+
stopped_by_user: [],
|
|
145
|
+
paused_billing: [OPEN_BILLING],
|
|
146
|
+
synthesis_incomplete: [],
|
|
147
|
+
approval_mismatch: [],
|
|
148
|
+
landing_refused: [],
|
|
149
|
+
standing_order_refused: [],
|
|
150
|
+
no_connection: [CONNECT_GOOGLE_DRIVE],
|
|
151
|
+
source_sync_off: [ENABLE_SOURCE_SYNC],
|
|
152
|
+
internal_error: [],
|
|
153
|
+
abandoned: [],
|
|
154
|
+
unrecognized_terminal_state: [],
|
|
155
|
+
} as const satisfies Record<
|
|
156
|
+
SynthesizeOutcomeReason,
|
|
157
|
+
readonly OfferedAffordance[]
|
|
158
|
+
>;
|
|
159
|
+
|
|
160
|
+
/** The counts a synthesis run reports at its end. */
|
|
161
|
+
export const SynthesizeOutcomeFactsSchema = z
|
|
162
|
+
.object({
|
|
163
|
+
/** Approved scopes the run read. */
|
|
164
|
+
scopeCount: Count,
|
|
165
|
+
/** Authority or size violations that left the batch incomplete. */
|
|
166
|
+
violationCount: Count,
|
|
167
|
+
})
|
|
168
|
+
.strict();
|
|
169
|
+
|
|
170
|
+
export type SynthesizeOutcomeFacts = z.infer<
|
|
171
|
+
typeof SynthesizeOutcomeFactsSchema
|
|
172
|
+
>;
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// org-structure/hris-connect and org-structure/reanalyze
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Why an org-structure analysis ended the way it did. Shared by both
|
|
180
|
+
* org-structure workflow types: they run the same analysis. The directory-only
|
|
181
|
+
* endings (`directory_synced`, `analysis_already_running`, `sync_failed`) occur
|
|
182
|
+
* only on `org-structure/hris-connect`, which syncs the directory first.
|
|
183
|
+
*/
|
|
184
|
+
export const ORG_STRUCTURE_OUTCOME_KIND_OF_REASON = {
|
|
185
|
+
proposal_ready: "produced",
|
|
186
|
+
directory_synced: "produced",
|
|
187
|
+
analysis_already_running: "produced",
|
|
188
|
+
proposal_blocked: "failed",
|
|
189
|
+
no_sync_run: "empty",
|
|
190
|
+
no_persons: "empty",
|
|
191
|
+
reporting_not_keyed: "empty",
|
|
192
|
+
stopped_by_user: "stopped",
|
|
193
|
+
sync_failed: "failed",
|
|
194
|
+
snapshot_too_large: "failed",
|
|
195
|
+
model_failed: "failed",
|
|
196
|
+
run_too_large: "failed",
|
|
197
|
+
placement_failed: "failed",
|
|
198
|
+
internal_error: "failed",
|
|
199
|
+
abandoned: "abandoned",
|
|
200
|
+
unrecognized_terminal_state: "unknown",
|
|
201
|
+
} as const satisfies Record<string, WorkflowOutcomeKind>;
|
|
202
|
+
|
|
203
|
+
export type OrgStructureOutcomeReason =
|
|
204
|
+
keyof typeof ORG_STRUCTURE_OUTCOME_KIND_OF_REASON;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* What each org-structure ending offers; shared by both org-structure workflow
|
|
208
|
+
* types, like the kind table. TOTAL; an empty list is explicit.
|
|
209
|
+
*/
|
|
210
|
+
export const ORG_STRUCTURE_AFFORDANCES_OF_REASON = {
|
|
211
|
+
proposal_ready: [],
|
|
212
|
+
directory_synced: [],
|
|
213
|
+
analysis_already_running: [],
|
|
214
|
+
proposal_blocked: [],
|
|
215
|
+
no_sync_run: [RESYNC_DIRECTORY],
|
|
216
|
+
no_persons: [],
|
|
217
|
+
reporting_not_keyed: [],
|
|
218
|
+
stopped_by_user: [],
|
|
219
|
+
sync_failed: [RESYNC_DIRECTORY],
|
|
220
|
+
snapshot_too_large: [],
|
|
221
|
+
model_failed: [REANALYZE_STRUCTURE],
|
|
222
|
+
run_too_large: [],
|
|
223
|
+
placement_failed: [REANALYZE_STRUCTURE],
|
|
224
|
+
internal_error: [REANALYZE_STRUCTURE],
|
|
225
|
+
abandoned: [REANALYZE_STRUCTURE],
|
|
226
|
+
unrecognized_terminal_state: [],
|
|
227
|
+
} as const satisfies Record<
|
|
228
|
+
OrgStructureOutcomeReason,
|
|
229
|
+
readonly OfferedAffordance[]
|
|
230
|
+
>;
|
|
231
|
+
|
|
232
|
+
/** The counts a finished org-structure proposal carries. */
|
|
233
|
+
export const OrgStructureOutcomeFactsSchema = z
|
|
234
|
+
.object({
|
|
235
|
+
departmentCount: Count,
|
|
236
|
+
teamCount: Count,
|
|
237
|
+
placedCount: Count,
|
|
238
|
+
reviewItemCount: Count,
|
|
239
|
+
})
|
|
240
|
+
.strict();
|
|
241
|
+
|
|
242
|
+
export type OrgStructureOutcomeFacts = z.infer<
|
|
243
|
+
typeof OrgStructureOutcomeFactsSchema
|
|
244
|
+
>;
|