@company-semantics/contracts 27.2.0 → 27.3.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@company-semantics/contracts",
3
- "version": "27.2.0",
3
+ "version": "27.3.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
package/src/index.ts CHANGED
@@ -438,6 +438,32 @@ export type {
438
438
  OrgStructureInferenceDecisionResult,
439
439
  } from "./org/index";
440
440
 
441
+ // Org transformation wire vocabulary: intent -> simulation -> preview. The
442
+ // engine turns a TransformationRequest into scored mutation bundles and returns
443
+ // a TransformationPreview the operator confirms before apply. (ADR-CONTRACTS-072)
444
+ export {
445
+ OrgIntentSchema,
446
+ FactMutationSchema,
447
+ TransformationOriginSchema,
448
+ TransformationRequestSchema,
449
+ ConstraintViolationSchema,
450
+ RecommendationSchema,
451
+ TransformStrategySchema,
452
+ SelectionRationaleSchema,
453
+ TransformationPreviewSchema,
454
+ } from "./org/index";
455
+ export type {
456
+ OrgIntent,
457
+ FactMutation,
458
+ TransformationOrigin,
459
+ TransformationRequest,
460
+ ConstraintViolation,
461
+ Recommendation,
462
+ TransformStrategy,
463
+ SelectionRationale,
464
+ TransformationPreview,
465
+ } from "./org/index";
466
+
441
467
  // Canonical facts: the adapter-seam ingestion batch — external-keyed Canonical*
442
468
  // facts (each with provenance) plus a CanonicalFacts envelope. The reconciler
443
469
  // consumes the batch into the internal org graph. (ADR-CONT-085 / ADR-CTRL-183)
package/src/org/README.md CHANGED
@@ -74,6 +74,8 @@ Shared type vocabulary for organization ownership, type classification, and tran
74
74
  - `CompanyMdVisibility` _(type)_ — Visibility band for a Company.md document — the canonical AUTH `entity_visibility` vocabulary (, backend ). -…
75
75
  - `ConflictRecord` _(type)_
76
76
  - `ConflictRecordSchema`
77
+ - `ConstraintViolation` _(type)_
78
+ - `ConstraintViolationSchema` — One constraint the transformation engine evaluated and found violated.
77
79
  - `CreateDelegationRequest` _(type)_
78
80
  - `CreateDelegationRequestSchema`
79
81
  - `CreateInviteRequest` _(type)_ — Request payload for creating an organization invite.
@@ -95,6 +97,8 @@ Shared type vocabulary for organization ownership, type classification, and tran
95
97
  - `EvaluationStep` _(type)_ — Single step in the access evaluation trace.
96
98
  - `ExecutionScope` _(type)_ — Execution scope determines whose identity is used when executing actions. - 'self': Actions execute under the…
97
99
  - `FACT_SOURCE_TIER_PRECEDENCE` — Truth hierarchy, highest precedence first.
100
+ - `FactMutation` _(type)_
101
+ - `FactMutationSchema` — A single atomic fact change the engine applies to the live org graph.
98
102
  - `FactProvenance` _(type)_
99
103
  - `FactProvenanceSchema` — Provenance envelope attached to a structural fact (reporting edge, org unit, membership, home assignment).
100
104
  - `FactSourceTier` _(type)_
@@ -148,6 +152,8 @@ Shared type vocabulary for organization ownership, type classification, and tran
148
152
  - `OrgDeletionStatus` _(type)_
149
153
  - `OrgDeletionStatusSchema`
150
154
  - `OrgDomain` _(type)_ — Organization domain claim and verification status.
155
+ - `OrgIntent` _(type)_
156
+ - `OrgIntentSchema` — A requested org-graph change expressed as intent.
151
157
  - `OrgInvite` _(type)_ — Organization invite for the workspace invites list.
152
158
  - `OrgInviteStatus` _(type)_ — Status of an organization invite.
153
159
  - `OrgLevelConfig` _(type)_
@@ -242,6 +248,8 @@ Shared type vocabulary for organization ownership, type classification, and tran
242
248
  - `PositionStatusSchema` — Lifecycle of a seat's EXISTENCE — distinct from a hiring pipeline.
243
249
  - `ProviderStatus` _(type)_ — Provider-level configuration lifecycle.
244
250
  - `ProviderSuggestion` _(type)_ — MX-based provider suggestion for SSO setup.
251
+ - `Recommendation` _(type)_
252
+ - `RecommendationSchema`
245
253
  - `RemoveMemberRequest` _(type)_ — Request payload for removing a member from the workspace.
246
254
  - `RemoveMemberResponse` _(type)_
247
255
  - `RemoveMemberResponseSchema`
@@ -252,6 +260,8 @@ Shared type vocabulary for organization ownership, type classification, and tran
252
260
  - `ScopeCheckBatchResponseSchema`
253
261
  - `ScopeCheckResponse` _(type)_
254
262
  - `ScopeCheckResponseSchema`
263
+ - `SelectionRationale` _(type)_
264
+ - `SelectionRationaleSchema`
255
265
  - `SetActiveOrgResponse` _(type)_
256
266
  - `SetActiveOrgResponseSchema`
257
267
  - `SetMemberManagerResponse` _(type)_
@@ -287,6 +297,14 @@ Shared type vocabulary for organization ownership, type classification, and tran
287
297
  - `TransferMemberEligibility` _(type)_ — Per-member eligibility for transfer recipient picker
288
298
  - `TransferResponsibility` _(type)_
289
299
  - `TransferStatusResponse` _(type)_ — Response for transfer status queries — extends existing OwnershipTransferStatus.
300
+ - `TransformStrategy` _(type)_
301
+ - `TransformStrategySchema`
302
+ - `TransformationOrigin` _(type)_
303
+ - `TransformationOriginSchema` — Where a transformation originated.
304
+ - `TransformationPreview` _(type)_
305
+ - `TransformationPreviewSchema`
306
+ - `TransformationRequest` _(type)_
307
+ - `TransformationRequestSchema`
290
308
  - `TreeOrderableNode` _(type)_ — Canonical per-parent sibling ordering for OrgUnit trees.
291
309
  - `UnitMembershipRole` _(type)_
292
310
  - `UnitMembershipRoleSchema` — Positional slot a member holds in the org-unit tree.
@@ -0,0 +1,273 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ OrgIntentSchema,
4
+ FactMutationSchema,
5
+ TransformationOriginSchema,
6
+ TransformationRequestSchema,
7
+ ConstraintViolationSchema,
8
+ RecommendationSchema,
9
+ TransformStrategySchema,
10
+ SelectionRationaleSchema,
11
+ TransformationPreviewSchema,
12
+ } from "../transformation.js";
13
+
14
+ const REPORT = "33333333-3333-4333-8333-333333333333";
15
+ const MANAGER = "44444444-4444-4444-8444-444444444444";
16
+
17
+ const setManagerIntent = {
18
+ kind: "reporting.setManager" as const,
19
+ reportPositionId: REPORT,
20
+ managerPositionId: MANAGER,
21
+ };
22
+
23
+ const setManagerMutation = {
24
+ kind: "reporting.setManager" as const,
25
+ reportPositionId: REPORT,
26
+ managerPositionId: MANAGER,
27
+ };
28
+
29
+ describe("OrgIntentSchema", () => {
30
+ it("accepts a reporting.setManager intent", () => {
31
+ expect(OrgIntentSchema.parse(setManagerIntent)).toEqual(setManagerIntent);
32
+ });
33
+
34
+ it("rejects an unknown intent kind", () => {
35
+ expect(
36
+ OrgIntentSchema.safeParse({ ...setManagerIntent, kind: "home.setUnit" })
37
+ .success,
38
+ ).toBe(false);
39
+ });
40
+
41
+ it("rejects a non-uuid reportPositionId", () => {
42
+ expect(
43
+ OrgIntentSchema.safeParse({ ...setManagerIntent, reportPositionId: "x" })
44
+ .success,
45
+ ).toBe(false);
46
+ });
47
+
48
+ it("rejects a null managerPositionId (intents always set a manager)", () => {
49
+ expect(
50
+ OrgIntentSchema.safeParse({
51
+ ...setManagerIntent,
52
+ managerPositionId: null,
53
+ }).success,
54
+ ).toBe(false);
55
+ });
56
+ });
57
+
58
+ describe("FactMutationSchema", () => {
59
+ it("accepts a reporting.setManager mutation", () => {
60
+ expect(FactMutationSchema.parse(setManagerMutation)).toEqual(
61
+ setManagerMutation,
62
+ );
63
+ });
64
+
65
+ it("accepts a null managerPositionId (un-setting the manager edge)", () => {
66
+ expect(
67
+ FactMutationSchema.safeParse({
68
+ ...setManagerMutation,
69
+ managerPositionId: null,
70
+ }).success,
71
+ ).toBe(true);
72
+ });
73
+
74
+ it("rejects an unknown mutation kind", () => {
75
+ expect(
76
+ FactMutationSchema.safeParse({ ...setManagerMutation, kind: "unit.move" })
77
+ .success,
78
+ ).toBe(false);
79
+ });
80
+ });
81
+
82
+ describe("TransformationOriginSchema", () => {
83
+ it("accepts each known origin", () => {
84
+ for (const origin of [
85
+ "user_intent",
86
+ "business_event",
87
+ "external_observation",
88
+ ]) {
89
+ expect(TransformationOriginSchema.safeParse(origin).success).toBe(true);
90
+ }
91
+ });
92
+
93
+ it("rejects an unknown origin", () => {
94
+ expect(TransformationOriginSchema.safeParse("system").success).toBe(false);
95
+ });
96
+ });
97
+
98
+ describe("TransformationRequestSchema", () => {
99
+ const request = {
100
+ origin: "user_intent" as const,
101
+ intent: setManagerIntent,
102
+ reason: "New manager after team merge",
103
+ };
104
+
105
+ it("accepts a full request", () => {
106
+ expect(TransformationRequestSchema.parse(request)).toEqual(request);
107
+ });
108
+
109
+ it("accepts a request without a reason", () => {
110
+ const { reason: _omit, ...noReason } = request;
111
+ expect(TransformationRequestSchema.safeParse(noReason).success).toBe(true);
112
+ });
113
+
114
+ it("rejects a reason longer than 2000 chars", () => {
115
+ expect(
116
+ TransformationRequestSchema.safeParse({
117
+ ...request,
118
+ reason: "x".repeat(2001),
119
+ }).success,
120
+ ).toBe(false);
121
+ });
122
+
123
+ it("rejects an unknown origin", () => {
124
+ expect(
125
+ TransformationRequestSchema.safeParse({ ...request, origin: "robot" })
126
+ .success,
127
+ ).toBe(false);
128
+ });
129
+ });
130
+
131
+ describe("ConstraintViolationSchema", () => {
132
+ it("accepts an error violation with a subject", () => {
133
+ expect(
134
+ ConstraintViolationSchema.parse({
135
+ code: "reporting.cycle",
136
+ severity: "error",
137
+ subject: REPORT,
138
+ }),
139
+ ).toEqual({ code: "reporting.cycle", severity: "error", subject: REPORT });
140
+ });
141
+
142
+ it("accepts a warning without a subject", () => {
143
+ expect(
144
+ ConstraintViolationSchema.safeParse({
145
+ code: "span.wide",
146
+ severity: "warning",
147
+ }).success,
148
+ ).toBe(true);
149
+ });
150
+
151
+ it("rejects an unknown severity", () => {
152
+ expect(
153
+ ConstraintViolationSchema.safeParse({ code: "c", severity: "fatal" })
154
+ .success,
155
+ ).toBe(false);
156
+ });
157
+ });
158
+
159
+ describe("RecommendationSchema / TransformStrategySchema", () => {
160
+ const recommendation = {
161
+ label: "Reassign report",
162
+ mutations: [setManagerMutation],
163
+ cost: 1,
164
+ explanation: ["Direct edge rewrite", "No descendants affected"],
165
+ };
166
+
167
+ it("accepts a recommendation without a warning", () => {
168
+ expect(RecommendationSchema.parse(recommendation)).toEqual(recommendation);
169
+ });
170
+
171
+ it("accepts a recommendation with a warning", () => {
172
+ expect(
173
+ RecommendationSchema.safeParse({
174
+ ...recommendation,
175
+ warning: "Crosses a department boundary",
176
+ }).success,
177
+ ).toBe(true);
178
+ });
179
+
180
+ it("accepts a strategy wrapping recommendations", () => {
181
+ expect(
182
+ TransformStrategySchema.parse({
183
+ recommendations: [recommendation],
184
+ totalCost: 1,
185
+ warnings: [],
186
+ }).recommendations,
187
+ ).toHaveLength(1);
188
+ });
189
+
190
+ it("rejects a recommendation with a non-array explanation", () => {
191
+ expect(
192
+ RecommendationSchema.safeParse({ ...recommendation, explanation: "one" })
193
+ .success,
194
+ ).toBe(false);
195
+ });
196
+ });
197
+
198
+ describe("SelectionRationaleSchema", () => {
199
+ it("accepts chosen reasons and rejected alternatives", () => {
200
+ const rationale = {
201
+ chosen: ["Lowest cost"],
202
+ rejectedAlternatives: [
203
+ {
204
+ label: "Restructure subtree",
205
+ reasons: ["Higher cost", "Wider span"],
206
+ },
207
+ ],
208
+ };
209
+ expect(SelectionRationaleSchema.parse(rationale)).toEqual(rationale);
210
+ });
211
+
212
+ it("accepts an empty rejected-alternatives list", () => {
213
+ expect(
214
+ SelectionRationaleSchema.safeParse({
215
+ chosen: ["Only option"],
216
+ rejectedAlternatives: [],
217
+ }).success,
218
+ ).toBe(true);
219
+ });
220
+ });
221
+
222
+ describe("TransformationPreviewSchema", () => {
223
+ const strategy = {
224
+ recommendations: [
225
+ {
226
+ label: "Reassign report",
227
+ mutations: [setManagerMutation],
228
+ cost: 1,
229
+ explanation: ["Direct edge rewrite"],
230
+ },
231
+ ],
232
+ totalCost: 1,
233
+ warnings: [],
234
+ };
235
+
236
+ const preview = {
237
+ intentLabel: "Set manager",
238
+ primary: setManagerMutation,
239
+ defaultStrategy: strategy,
240
+ alternatives: [],
241
+ warnings: [],
242
+ selectionRationale: { chosen: ["Lowest cost"], rejectedAlternatives: [] },
243
+ };
244
+
245
+ it("accepts a preview without a hardBlock", () => {
246
+ expect(TransformationPreviewSchema.parse(preview)).toEqual(preview);
247
+ });
248
+
249
+ it("accepts a preview with a hardBlock", () => {
250
+ expect(
251
+ TransformationPreviewSchema.safeParse({
252
+ ...preview,
253
+ hardBlock: { code: "reporting.cycle", reason: "Would create a cycle" },
254
+ }).success,
255
+ ).toBe(true);
256
+ });
257
+
258
+ it("rejects a hardBlock missing its reason", () => {
259
+ expect(
260
+ TransformationPreviewSchema.safeParse({
261
+ ...preview,
262
+ hardBlock: { code: "reporting.cycle" },
263
+ }).success,
264
+ ).toBe(false);
265
+ });
266
+
267
+ it("rejects a preview missing the primary mutation", () => {
268
+ const { primary: _omit, ...noPrimary } = preview;
269
+ expect(TransformationPreviewSchema.safeParse(noPrimary).success).toBe(
270
+ false,
271
+ );
272
+ });
273
+ });
package/src/org/index.ts CHANGED
@@ -150,6 +150,32 @@ export type {
150
150
  OrgStructureInferenceDecisionResult,
151
151
  } from "./reasoning-review";
152
152
 
153
+ // Org transformation wire vocabulary: intent -> simulation -> preview. The
154
+ // engine turns a TransformationRequest into scored mutation bundles and returns
155
+ // a TransformationPreview the operator confirms before apply. (ADR-CONTRACTS-072)
156
+ export {
157
+ OrgIntentSchema,
158
+ FactMutationSchema,
159
+ TransformationOriginSchema,
160
+ TransformationRequestSchema,
161
+ ConstraintViolationSchema,
162
+ RecommendationSchema,
163
+ TransformStrategySchema,
164
+ SelectionRationaleSchema,
165
+ TransformationPreviewSchema,
166
+ } from "./transformation";
167
+ export type {
168
+ OrgIntent,
169
+ FactMutation,
170
+ TransformationOrigin,
171
+ TransformationRequest,
172
+ ConstraintViolation,
173
+ Recommendation,
174
+ TransformStrategy,
175
+ SelectionRationale,
176
+ TransformationPreview,
177
+ } from "./transformation";
178
+
153
179
  // Canonical facts: the adapter-seam ingestion batch. Each fact is keyed by
154
180
  // external identity (externalSourceId/externalSourceSystem) and carries a
155
181
  // FactProvenance envelope; the reconciler consumes the batch. (ADR-CONT-085 / ADR-CTRL-183)
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Org transformation wire vocabulary — intent → simulation → preview.
3
+ *
4
+ * A transformation is a deliberate change to the org graph expressed at the
5
+ * level of INTENT ("make this seat report to that one") rather than raw fact
6
+ * writes. The semantic transformation engine (control ADR-CTRL — see
7
+ * ADR-CONTRACTS-072) turns one {@link TransformationRequestSchema} into a set of
8
+ * candidate {@link FactMutationSchema} bundles, scores them deterministically,
9
+ * and returns a {@link TransformationPreviewSchema} the operator confirms before
10
+ * anything is applied. This module is the shared WIRE vocabulary for that flow.
11
+ *
12
+ * Layers, smallest → largest:
13
+ * - {@link OrgIntentSchema} — WHAT the operator wants, as a discriminated union
14
+ * on `kind`. Today only `reporting.setManager`; shaped so future kinds
15
+ * (home.setUnit, unit.move, position.open, position.close) append without
16
+ * breaking existing consumers.
17
+ * - {@link FactMutationSchema} — the typed ATOMIC fact change the engine applies
18
+ * to the live graph. Distinct from intent: an intent may expand into several
19
+ * mutations, and a mutation can express un-setting (a null target) that an
20
+ * intent never does.
21
+ * - {@link TransformationRequestSchema} — an intent plus its `origin` and an
22
+ * optional human WHY (`reason`).
23
+ * - {@link RecommendationSchema} / {@link TransformStrategySchema} — the scored,
24
+ * explained candidate bundles the engine produces.
25
+ * - {@link TransformationPreviewSchema} — the full operator-facing preview: the
26
+ * primary mutation, the default strategy, alternatives, and the deterministic
27
+ * rationale for what was chosen.
28
+ *
29
+ * NOTE ON PLACEMENT: these are PUBLISHED response/preview shapes (ADR-CONT-029:
30
+ * response shapes live in contracts, request bodies stay backend-local). The
31
+ * transformation REQUEST body the backend route accepts is modeled there; this
32
+ * module carries only the vocabulary the contract surface promises.
33
+ *
34
+ * Reporting edge field names (`reportPositionId` / `managerPositionId`) match
35
+ * {@link PositionReportingSchema} exactly so intent, mutation, and the canonical
36
+ * edge speak one language.
37
+ *
38
+ * @see ADR-CONTRACTS-072 for the transformation vocabulary decision
39
+ * @see position-reporting.ts for the canonical reporting-edge field names
40
+ * @see structure-facts.ts (FactProvenance.reason) for the human-WHY precedent
41
+ */
42
+ import { z } from "zod";
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // OrgIntent — WHAT the operator wants (discriminated union, designed to grow)
46
+ // ---------------------------------------------------------------------------
47
+
48
+ /**
49
+ * A requested org-graph change expressed as intent. Discriminated on `kind`, a
50
+ * dotted string naming the affected relation and verb. Today the only variant
51
+ * is `reporting.setManager` — point a reporting seat at a (non-null) manager
52
+ * seat. Future kinds (`home.setUnit`, `unit.move`, `position.open`,
53
+ * `position.close`) append as new union members without breaking consumers.
54
+ */
55
+ export const OrgIntentSchema = z.discriminatedUnion("kind", [
56
+ z.object({
57
+ kind: z.literal("reporting.setManager"),
58
+ /** The reporting (subordinate) seat whose manager is being set. */
59
+ reportPositionId: z.string().uuid(),
60
+ /** The manager seat to report to. Non-null on an intent (un-setting is a mutation concern). */
61
+ managerPositionId: z.string().uuid(),
62
+ }),
63
+ ]);
64
+ export type OrgIntent = z.infer<typeof OrgIntentSchema>;
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // FactMutation — the atomic fact change the engine applies
68
+ // ---------------------------------------------------------------------------
69
+
70
+ /**
71
+ * A single atomic fact change the engine applies to the live org graph.
72
+ * Discriminated on `kind`, mirroring {@link OrgIntentSchema}. Unlike an intent,
73
+ * a mutation can express UN-SETTING: `reporting.setManager` with a null
74
+ * `managerPositionId` clears the seat's manager edge entirely.
75
+ */
76
+ export const FactMutationSchema = z.discriminatedUnion("kind", [
77
+ z.object({
78
+ kind: z.literal("reporting.setManager"),
79
+ /** The reporting (subordinate) seat whose manager edge is being written. */
80
+ reportPositionId: z.string().uuid(),
81
+ /** The new manager seat, or `null` to un-set (clear) the manager edge. */
82
+ managerPositionId: z.string().uuid().nullable(),
83
+ }),
84
+ ]);
85
+ export type FactMutation = z.infer<typeof FactMutationSchema>;
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // TransformationRequest — an intent plus origin and an optional human WHY
89
+ // ---------------------------------------------------------------------------
90
+
91
+ /**
92
+ * Where a transformation originated. Closed set: a direct `user_intent`, a
93
+ * `business_event` (e.g. a sync-observed reorg), or an `external_observation`
94
+ * (an inference proposed from outside the system).
95
+ */
96
+ export const TransformationOriginSchema = z.enum([
97
+ "user_intent",
98
+ "business_event",
99
+ "external_observation",
100
+ ]);
101
+ export type TransformationOrigin = z.infer<typeof TransformationOriginSchema>;
102
+
103
+ export const TransformationRequestSchema = z.object({
104
+ /** Provenance class of the request — drives how much scrutiny it gets. */
105
+ origin: TransformationOriginSchema,
106
+ /** The requested change, as intent. */
107
+ intent: OrgIntentSchema,
108
+ /**
109
+ * Human-meaningful WHY for an authority-relevant change — a free-text audit
110
+ * record of intent, capped at 2000 chars to mirror {@link FactProvenanceSchema}'s
111
+ * `reason`. `null`/absent for machine-originated requests. It records why a
112
+ * change was requested, NOT who may make it (ADR-CTRL-180).
113
+ */
114
+ reason: z.string().max(2000).nullable().optional(),
115
+ });
116
+ export type TransformationRequest = z.infer<typeof TransformationRequestSchema>;
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // ConstraintViolation — a rule the engine found broken or at risk
120
+ // ---------------------------------------------------------------------------
121
+
122
+ /**
123
+ * One constraint the transformation engine evaluated and found violated. `code`
124
+ * is an open string naming the rule; `severity` distinguishes a hard `error`
125
+ * (the change is rejected) from a `warning` (allowed but flagged); `subject` is
126
+ * the optional id of the entity the violation is about.
127
+ */
128
+ export const ConstraintViolationSchema = z.object({
129
+ /** Open string naming the violated rule (e.g. "reporting.cycle"). */
130
+ code: z.string().min(1),
131
+ /** `error` blocks the change; `warning` allows it but surfaces a caution. */
132
+ severity: z.enum(["error", "warning"]),
133
+ /** Optional id of the entity the violation concerns. */
134
+ subject: z.string().optional(),
135
+ });
136
+ export type ConstraintViolation = z.infer<typeof ConstraintViolationSchema>;
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // Recommendation — one scored, explained candidate bundle of mutations
140
+ // ---------------------------------------------------------------------------
141
+
142
+ export const RecommendationSchema = z.object({
143
+ /** Short human label for this candidate (e.g. "Move report to new manager"). */
144
+ label: z.string().min(1),
145
+ /** The atomic fact changes this candidate would apply. */
146
+ mutations: z.array(FactMutationSchema),
147
+ /** Deterministic relative cost of this candidate; lower is preferred. */
148
+ cost: z.number(),
149
+ /** Optional single caution shown alongside this candidate. */
150
+ warning: z.string().optional(),
151
+ /** Deterministic, ordered reasons explaining how this candidate was derived. */
152
+ explanation: z.array(z.string()),
153
+ });
154
+ export type Recommendation = z.infer<typeof RecommendationSchema>;
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // TransformStrategy — a scored set of recommendations
158
+ // ---------------------------------------------------------------------------
159
+
160
+ export const TransformStrategySchema = z.object({
161
+ /** Candidate bundles, in the engine's preference order. */
162
+ recommendations: z.array(RecommendationSchema),
163
+ /** Aggregate cost across the strategy's recommendations. */
164
+ totalCost: z.number(),
165
+ /** Strategy-level cautions not tied to a single recommendation. */
166
+ warnings: z.array(z.string()),
167
+ });
168
+ export type TransformStrategy = z.infer<typeof TransformStrategySchema>;
169
+
170
+ // ---------------------------------------------------------------------------
171
+ // SelectionRationale — why the default strategy was chosen over alternatives
172
+ // ---------------------------------------------------------------------------
173
+
174
+ export const SelectionRationaleSchema = z.object({
175
+ /** Ordered reasons the chosen strategy won. */
176
+ chosen: z.array(z.string()),
177
+ /** Each alternative that lost, with the ordered reasons it was rejected. */
178
+ rejectedAlternatives: z.array(
179
+ z.object({
180
+ label: z.string().min(1),
181
+ reasons: z.array(z.string()),
182
+ }),
183
+ ),
184
+ });
185
+ export type SelectionRationale = z.infer<typeof SelectionRationaleSchema>;
186
+
187
+ // ---------------------------------------------------------------------------
188
+ // TransformationPreview — the full operator-facing preview before apply
189
+ // ---------------------------------------------------------------------------
190
+
191
+ export const TransformationPreviewSchema = z.object({
192
+ /** Human label for the intent being previewed. */
193
+ intentLabel: z.string().min(1),
194
+ /** The headline fact change the preview is centered on. */
195
+ primary: FactMutationSchema,
196
+ /** The strategy the engine recommends by default. */
197
+ defaultStrategy: TransformStrategySchema,
198
+ /** Other viable strategies the operator may pick instead. */
199
+ alternatives: z.array(TransformStrategySchema),
200
+ /** Preview-level cautions spanning the whole transformation. */
201
+ warnings: z.array(z.string()),
202
+ /** Deterministic rationale for the default choice over the alternatives. */
203
+ selectionRationale: SelectionRationaleSchema,
204
+ /**
205
+ * Present only when the transformation is hard-blocked: a terminal reason the
206
+ * change cannot proceed at all. Absent on a previewable (confirmable) change.
207
+ */
208
+ hardBlock: z
209
+ .object({
210
+ code: z.string().min(1),
211
+ reason: z.string().min(1),
212
+ })
213
+ .optional(),
214
+ });
215
+ export type TransformationPreview = z.infer<typeof TransformationPreviewSchema>;