@company-semantics/contracts 94.0.0 → 96.0.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.
@@ -0,0 +1,129 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ PriceListModelSchema,
4
+ PriceListSchema,
5
+ PriceBookVersionListSchema,
6
+ } from "../schemas";
7
+ import { PROCESSING_MODES, PROMPT_TIER_MEASURES } from "../vocabulary";
8
+
9
+ const rates = (input: string) => ({
10
+ inputPerMMicros: input,
11
+ outputPerMMicros: "500000",
12
+ cacheReadPerMMicros: "10000",
13
+ cacheWritePerMMicros: "125000",
14
+ cacheWrite1hPerMMicros: "200000",
15
+ reasoningPerMMicros: "500000",
16
+ });
17
+ const haiku55 = {
18
+ provider: "anthropic",
19
+ modelId: "claude-haiku-5-5",
20
+ aliases: [],
21
+ sourceUrl: "https://platform.claude.com/docs/en/about-claude/pricing",
22
+ retrievedOn: "2026-10-09",
23
+ promptTierMeasure: "input_incl_cache",
24
+ promptTokensCeiling: null,
25
+ tiers: [
26
+ {
27
+ processingMode: "standard",
28
+ promptTokensAbove: 0,
29
+ reference: rates("100000"),
30
+ hosted: rates("120000"),
31
+ },
32
+ {
33
+ processingMode: "standard",
34
+ promptTokensAbove: 100000,
35
+ reference: rates("500000"),
36
+ hosted: rates("600000"),
37
+ },
38
+ ],
39
+ };
40
+
41
+ describe("price list tiers", () => {
42
+ it("accepts a two-tier model that names its measure", () => {
43
+ expect(PriceListModelSchema.safeParse(haiku55).success).toBe(true);
44
+ });
45
+ it("refuses two tiers without a promptTierMeasure", () => {
46
+ expect(
47
+ PriceListModelSchema.safeParse({ ...haiku55, promptTierMeasure: null })
48
+ .success,
49
+ ).toBe(false);
50
+ });
51
+ it("refuses a model with no standard base tier", () => {
52
+ expect(
53
+ PriceListModelSchema.safeParse({ ...haiku55, tiers: [haiku55.tiers[1]] })
54
+ .success,
55
+ ).toBe(false);
56
+ });
57
+ it("accepts a null cache read as not separately priced", () => {
58
+ const t = {
59
+ ...haiku55.tiers[0],
60
+ reference: { ...rates("500000"), cacheReadPerMMicros: null },
61
+ };
62
+ expect(
63
+ PriceListModelSchema.safeParse({
64
+ ...haiku55,
65
+ promptTierMeasure: null,
66
+ tiers: [t],
67
+ }).success,
68
+ ).toBe(true);
69
+ });
70
+ it("accepts a pre-catalog card with no source", () => {
71
+ expect(
72
+ PriceListModelSchema.safeParse({
73
+ ...haiku55,
74
+ sourceUrl: null,
75
+ retrievedOn: null,
76
+ }).success,
77
+ ).toBe(true);
78
+ });
79
+ it("refuses an alias that is also listed as its own model", () => {
80
+ const dated = { ...haiku55, modelId: "claude-haiku-5-5-20261001" };
81
+ expect(
82
+ PriceListSchema.safeParse({
83
+ seatPriceMicros: null,
84
+ hostedSurchargeBps: 2000,
85
+ byoFeeBps: 500,
86
+ priceBookVersion: "pb-2026-10-v3",
87
+ priceBookEffectiveAt: "2026-10-10T00:00:00Z",
88
+ models: [{ ...haiku55, aliases: ["claude-haiku-5-5-20261001"] }, dated],
89
+ }).success,
90
+ ).toBe(false);
91
+ });
92
+ it("names the closed processing modes and tier measures", () => {
93
+ expect(PROCESSING_MODES).toEqual([
94
+ "standard",
95
+ "batch",
96
+ "flex",
97
+ "fast",
98
+ "ultrafast",
99
+ ]);
100
+ expect(PROMPT_TIER_MEASURES).toEqual([
101
+ "input_incl_cache",
102
+ "input_excl_cache",
103
+ ]);
104
+ });
105
+ it("requires the effective-at on the list and the version history shape", () => {
106
+ expect(
107
+ PriceListSchema.safeParse({
108
+ seatPriceMicros: null,
109
+ hostedSurchargeBps: 2000,
110
+ byoFeeBps: 500,
111
+ priceBookVersion: "pb-2026-10-v3",
112
+ models: [haiku55],
113
+ }).success,
114
+ ).toBe(false);
115
+ expect(
116
+ PriceBookVersionListSchema.safeParse({
117
+ versions: [
118
+ {
119
+ versionLabel: "pb-2026-10-v3",
120
+ effectiveAt: "2026-10-10T00:00:00Z",
121
+ source: "catalog",
122
+ notes: null,
123
+ active: true,
124
+ },
125
+ ],
126
+ }).success,
127
+ ).toBe(true);
128
+ });
129
+ });
@@ -0,0 +1,19 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ PROCESSING_MODES,
4
+ ProcessingModeSchema,
5
+ PROMPT_TIER_MEASURES,
6
+ PromptTierMeasureSchema,
7
+ } from "../index";
8
+
9
+ describe("processing modes and prompt tier measures", () => {
10
+ it("exports the processing modes through the billing index", () => {
11
+ expect(PROCESSING_MODES).toContain("fast");
12
+ expect(PROMPT_TIER_MEASURES).toContain("input_excl_cache");
13
+ // "priority" was renamed to "fast"; the closed vocabulary refuses it.
14
+ expect(ProcessingModeSchema.safeParse("priority").success).toBe(false);
15
+ expect(PromptTierMeasureSchema.safeParse("input_incl_cache").success).toBe(
16
+ true,
17
+ );
18
+ });
19
+ });
@@ -179,27 +179,50 @@ describe("the billing response schemas", () => {
179
179
  });
180
180
 
181
181
  it("rejects a price list whose per-million rate is a number", () => {
182
- expect(
183
- PriceListSchema.safeParse({
184
- seatPriceMicros: null,
185
- hostedSurchargeBps: 2000,
186
- byoFeeBps: 500,
187
- priceBookVersion: "2026-09-01",
188
- models: [
189
- {
190
- provider: "anthropic",
191
- modelId: "claude-opus-5",
192
- inputPerMMicros: 15000000,
193
- outputPerMMicros: "75000000",
194
- cacheReadPerMMicros: "1500000",
195
- cacheWritePerMMicros: "18750000",
196
- reasoningPerMMicros: "75000000",
197
- hostedInputPerMMicros: "18000000",
198
- hostedOutputPerMMicros: "90000000",
199
- },
200
- ],
201
- }).success,
202
- ).toBe(false);
182
+ const rates = {
183
+ inputPerMMicros: "15000000",
184
+ outputPerMMicros: "75000000",
185
+ cacheReadPerMMicros: "1500000",
186
+ cacheWritePerMMicros: "18750000",
187
+ cacheWrite1hPerMMicros: "30000000",
188
+ reasoningPerMMicros: "75000000",
189
+ };
190
+ const priceList = (inputPerMMicros: string | number) => ({
191
+ seatPriceMicros: null,
192
+ hostedSurchargeBps: 2000,
193
+ byoFeeBps: 500,
194
+ priceBookVersion: "2026-09-01",
195
+ priceBookEffectiveAt: "2026-09-01T00:00:00Z",
196
+ models: [
197
+ {
198
+ provider: "anthropic",
199
+ modelId: "claude-opus-5",
200
+ aliases: [],
201
+ sourceUrl:
202
+ "https://platform.claude.com/docs/en/about-claude/pricing",
203
+ retrievedOn: "2026-09-01",
204
+ promptTierMeasure: null,
205
+ promptTokensCeiling: null,
206
+ tiers: [
207
+ {
208
+ processingMode: "standard",
209
+ promptTokensAbove: 0,
210
+ reference: { ...rates, inputPerMMicros },
211
+ hosted: { ...rates, inputPerMMicros: "18000000" },
212
+ },
213
+ ],
214
+ },
215
+ ],
216
+ });
217
+
218
+ // The same list with the rate as a string parses, so the only defect is
219
+ // the JSON number.
220
+ expect(PriceListSchema.safeParse(priceList("15000000")).success).toBe(
221
+ true,
222
+ );
223
+ expect(PriceListSchema.safeParse(priceList(15000000)).success).toBe(
224
+ false,
225
+ );
203
226
  });
204
227
  });
205
228
 
@@ -30,6 +30,8 @@ export type {
30
30
  TokenDimension,
31
31
  PrincipalKind,
32
32
  BillingProvider,
33
+ ProcessingMode,
34
+ PromptTierMeasure,
33
35
  SeatBillability,
34
36
  } from "./vocabulary";
35
37
 
@@ -60,6 +62,10 @@ export {
60
62
  PrincipalKindSchema,
61
63
  BILLING_PROVIDERS,
62
64
  BillingProviderSchema,
65
+ PROCESSING_MODES,
66
+ ProcessingModeSchema,
67
+ PROMPT_TIER_MEASURES,
68
+ PromptTierMeasureSchema,
63
69
  SEAT_BILLABILITY,
64
70
  TokenDimensionsSchema,
65
71
  } from "./vocabulary";
@@ -102,8 +108,13 @@ export {
102
108
  ModelAllowlistSchema,
103
109
  OrgBillingOverviewSchema,
104
110
  PortalSessionResponseSchema,
111
+ PriceBookVersionListSchema,
112
+ PriceBookVersionSummarySchema,
105
113
  PriceListModelSchema,
114
+ PriceListQuerySchema,
115
+ PriceListRatesSchema,
106
116
  PriceListSchema,
117
+ PriceListTierSchema,
107
118
  ProviderKeyListSchema,
108
119
  ProviderKeyStatusSchema,
109
120
  SpendingControlsSchema,
@@ -144,8 +155,13 @@ export type {
144
155
  ModelAllowlist,
145
156
  OrgBillingOverview,
146
157
  PortalSessionResponse,
158
+ PriceBookVersionList,
159
+ PriceBookVersionSummary,
147
160
  PriceList,
148
161
  PriceListModel,
162
+ PriceListQuery,
163
+ PriceListRates,
164
+ PriceListTier,
149
165
  ProviderKeyList,
150
166
  ProviderKeyStatus,
151
167
  SpendingControls,
@@ -19,7 +19,11 @@
19
19
  * - EVERY percentage is {@link BpsSchema} — an integer number of basis points.
20
20
  * `effectiveMarkupBps: 20.5` is not a fine-grained markup, it is a bug.
21
21
  * - EVERY timestamp is {@link IsoDateTime}, the package's single definition
22
- * site for a `format: date-time` field (ADR-CONTRACTS-156).
22
+ * site for a `format: date-time` field (ADR-CONTRACTS-156); the price book's
23
+ * effective-at timestamps use its offset-tolerant sibling.
24
+ * - A price-list rate that is `null` means the provider publishes no separate
25
+ * rate for it — never `"0"`. A processing mode is an explicit rate set, never
26
+ * a multiplier (ADR slug price-list-tiers).
23
27
  * - EVERY token count is a non-negative integer, reported per dimension via
24
28
  * {@link TokenDimensionsSchema} and never collapsed into one total
25
29
  * (INV-BILL-RAW-TOKENS-ALWAYS-VISIBLE).
@@ -35,7 +39,7 @@
35
39
  */
36
40
  import { z } from "zod";
37
41
 
38
- import { IsoDateTime } from "../api/primitives";
42
+ import { IsoDateTime, IsoDateTimeWithOffset } from "../api/primitives";
39
43
  import {
40
44
  AI_PAUSE_REASONS,
41
45
  BILLING_PLATFORM_STATUSES,
@@ -45,6 +49,9 @@ import {
45
49
  FUNDING_MODES,
46
50
  LEDGER_ENTRY_TYPES,
47
51
  MoneyMicrosSchema,
52
+ PROCESSING_MODES,
53
+ ProcessingModeSchema,
54
+ PromptTierMeasureSchema,
48
55
  TOP_UP_KINDS,
49
56
  TOP_UP_STATUSES,
50
57
  TokenDimensionsSchema,
@@ -412,37 +419,151 @@ export const ProviderKeyListSchema = z.object({
412
419
  /* -------------------------------------------------------------------------- */
413
420
 
414
421
  /**
415
- * One priced model. Every rate is micro-USD PER MILLION tokens, which is what
416
- * the `PerM` suffix means and the only unit any of them is ever in — the
417
- * provider publishes per-million prices, and converting on the way in would
418
- * round before anything has been metered.
422
+ * One rate set, micro-USD PER MILLION tokens. `null` means the provider publishes
423
+ * no separate rate for that dimension: an input-side dimension (cache read,
424
+ * cache write) is then billed at the tier's input rate, which is what the
425
+ * provider charges; an output-side null on a model is refused at the gate when
426
+ * the call could produce output. `cacheWritePerMMicros` is the provider's default
427
+ * (Anthropic: 5-minute) write; `cacheWrite1hPerMMicros` the Anthropic 1-hour write.
419
428
  *
420
- * The `hosted*` pair is the rate charged when this platform's credential paid.
421
- * There is no BYO counterpart: a BYO call is billed by the provider directly
422
- * and this platform charges `byoFeeBps` on it, not a per-token rate.
429
+ * Null is never encoded as `"0"`: zero would be a price (free), not the absence
430
+ * of one.
423
431
  */
424
- export const PriceListModelSchema = z.object({
425
- provider: z.enum(BILLING_PROVIDERS),
426
- modelId: z.string(),
432
+ export const PriceListRatesSchema = z.object({
427
433
  inputPerMMicros: MoneyMicrosSchema,
428
- outputPerMMicros: MoneyMicrosSchema,
429
- cacheReadPerMMicros: MoneyMicrosSchema,
430
- cacheWritePerMMicros: MoneyMicrosSchema,
431
- reasoningPerMMicros: MoneyMicrosSchema,
432
- hostedInputPerMMicros: MoneyMicrosSchema,
433
- hostedOutputPerMMicros: MoneyMicrosSchema,
434
+ outputPerMMicros: MoneyMicrosSchema.nullable(),
435
+ cacheReadPerMMicros: MoneyMicrosSchema.nullable(),
436
+ cacheWritePerMMicros: MoneyMicrosSchema.nullable(),
437
+ cacheWrite1hPerMMicros: MoneyMicrosSchema.nullable(),
438
+ reasoningPerMMicros: MoneyMicrosSchema.nullable(),
434
439
  });
435
440
 
441
+ /**
442
+ * One tier of one processing mode. `promptTokensAbove` is 0 for the base tier;
443
+ * a request whose prompt (per the model's `promptTierMeasure`) is ABOVE it pays
444
+ * this tier's rates on EVERY dimension. `reference` is the provider's list
445
+ * rate; `hosted` is what a hosted call is charged per million (reference plus
446
+ * the surcharge, same rounding as settlement, null preserved).
447
+ *
448
+ * A processing mode is its own explicit rate set, never a multiplier on the
449
+ * standard one: the providers do not publish clean multipliers.
450
+ */
451
+ export const PriceListTierSchema = z.object({
452
+ processingMode: ProcessingModeSchema,
453
+ promptTokensAbove: z.number().int().min(0),
454
+ reference: PriceListRatesSchema,
455
+ hosted: PriceListRatesSchema,
456
+ });
457
+
458
+ /**
459
+ * One priced model's card: its provenance, the ids it serves, and its tiers
460
+ * per processing mode. Every model has a `standard` base tier; a model with
461
+ * more than one tier in any mode names the {@link PromptTierMeasureSchema} that
462
+ * decides which tier a request falls in.
463
+ *
464
+ * There is no BYO rate: a BYO call is billed by the provider directly and this
465
+ * platform charges `byoFeeBps` on it, not a per-token rate.
466
+ */
467
+ export const PriceListModelSchema = z
468
+ .object({
469
+ provider: z.enum(BILLING_PROVIDERS),
470
+ modelId: z.string().min(1),
471
+ /**
472
+ * Ids this version records as served by the same card; [] when the version
473
+ * records none. An id appears once in a price list, as a modelId or as an
474
+ * alias, never both.
475
+ */
476
+ aliases: z.array(z.string().min(1)),
477
+ /**
478
+ * The provider's official pricing page the card was authored from. Null for
479
+ * a price-book version authored before the catalog recorded provenance.
480
+ */
481
+ sourceUrl: z.string().url().nullable(),
482
+ /**
483
+ * ISO date (YYYY-MM-DD) the source was last read. Null for a price-book
484
+ * version authored before the catalog recorded provenance.
485
+ */
486
+ retrievedOn: z
487
+ .string()
488
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
489
+ .nullable(),
490
+ promptTierMeasure: PromptTierMeasureSchema.nullable(),
491
+ /** Prompts above this many tokens are refused (a tier we have not published). Null = no ceiling. */
492
+ promptTokensCeiling: z.number().int().positive().nullable(),
493
+ tiers: z.array(PriceListTierSchema).min(1),
494
+ })
495
+ .superRefine((m, ctx) => {
496
+ const standardBase = m.tiers.some(
497
+ (t) => t.processingMode === "standard" && t.promptTokensAbove === 0,
498
+ );
499
+ if (!standardBase) {
500
+ ctx.addIssue("every priced model has a standard base tier");
501
+ }
502
+ for (const mode of PROCESSING_MODES) {
503
+ const above = m.tiers
504
+ .filter((t) => t.processingMode === mode)
505
+ .map((t) => t.promptTokensAbove);
506
+ if (above.length > 1 && m.promptTierMeasure === null) {
507
+ ctx.addIssue(
508
+ "a model with more than one tier names its promptTierMeasure",
509
+ );
510
+ }
511
+ if (new Set(above).size !== above.length) {
512
+ ctx.addIssue(`duplicate tier threshold in mode ${mode}`);
513
+ }
514
+ }
515
+ });
516
+
436
517
  /**
437
518
  * The published price book, versioned. Read by the app to show what a model
438
519
  * costs before it is used, and by control to audit what it charged after.
520
+ * A correction is a new version; a published version is never recomputed.
439
521
  */
440
- export const PriceListSchema = z.object({
441
- seatPriceMicros: MoneyMicrosSchema.nullable(),
442
- hostedSurchargeBps: BpsSchema,
443
- byoFeeBps: BpsSchema,
444
- priceBookVersion: z.string(),
445
- models: z.array(PriceListModelSchema),
522
+ export const PriceListSchema = z
523
+ .object({
524
+ seatPriceMicros: MoneyMicrosSchema.nullable(),
525
+ hostedSurchargeBps: BpsSchema,
526
+ byoFeeBps: BpsSchema,
527
+ priceBookVersion: z.string(),
528
+ /** When this version became the one calls are priced against (ISO timestamp). */
529
+ priceBookEffectiveAt: IsoDateTimeWithOffset,
530
+ models: z.array(PriceListModelSchema),
531
+ })
532
+ .superRefine((list, ctx) => {
533
+ // An id is listed once: as a card's modelId or as one card's alias, never both.
534
+ const seen = new Set<string>();
535
+ for (const m of list.models) {
536
+ for (const id of [m.modelId, ...m.aliases]) {
537
+ if (seen.has(id)) {
538
+ ctx.addIssue(`model id ${id} is listed more than once`);
539
+ }
540
+ seen.add(id);
541
+ }
542
+ }
543
+ });
544
+
545
+ /**
546
+ * `GET /api/billing/price-list?version=<label>` — omit for the active version.
547
+ * A request shape published here by exception (ADR slug price-list-tiers): the
548
+ * app and the backend must agree on how a past version is addressed.
549
+ */
550
+ export const PriceListQuerySchema = z.object({
551
+ version: z.string().min(1).optional(),
552
+ });
553
+
554
+ /** One published price-book version, newest first in the list. */
555
+ export const PriceBookVersionSummarySchema = z.object({
556
+ versionLabel: z.string(),
557
+ effectiveAt: IsoDateTimeWithOffset,
558
+ source: z.string(),
559
+ notes: z.string().nullable(),
560
+ /** True for exactly the version calls are priced against now. */
561
+ active: z.boolean(),
562
+ });
563
+
564
+ /** The price book's version history, newest first. */
565
+ export const PriceBookVersionListSchema = z.object({
566
+ versions: z.array(PriceBookVersionSummarySchema),
446
567
  });
447
568
 
448
569
  /**
@@ -37,8 +37,13 @@ import type {
37
37
  ModelAllowlistSchema,
38
38
  OrgBillingOverviewSchema,
39
39
  PortalSessionResponseSchema,
40
+ PriceBookVersionListSchema,
41
+ PriceBookVersionSummarySchema,
40
42
  PriceListModelSchema,
43
+ PriceListQuerySchema,
44
+ PriceListRatesSchema,
41
45
  PriceListSchema,
46
+ PriceListTierSchema,
42
47
  ProviderKeyListSchema,
43
48
  ProviderKeyStatusSchema,
44
49
  SpendingControlsSchema,
@@ -151,11 +156,28 @@ export type ProviderKeyStatus = z.infer<typeof ProviderKeyStatusSchema>;
151
156
  /** Every provider the org could configure a key for. */
152
157
  export type ProviderKeyList = z.infer<typeof ProviderKeyListSchema>;
153
158
 
154
- /** One priced model. Every rate is micro-USD per million tokens. */
159
+ /** One rate set, micro-USD per million tokens; null = no separate rate. */
160
+ export type PriceListRates = z.infer<typeof PriceListRatesSchema>;
161
+
162
+ /** One tier of one processing mode: reference and hosted rates. */
163
+ export type PriceListTier = z.infer<typeof PriceListTierSchema>;
164
+
165
+ /** One priced model's card: provenance, aliases and tiers per mode. */
155
166
  export type PriceListModel = z.infer<typeof PriceListModelSchema>;
156
167
 
157
168
  /** The published price book, versioned. */
158
169
  export type PriceList = z.infer<typeof PriceListSchema>;
159
170
 
171
+ /** The price-list query: an optional version label, else the active one. */
172
+ export type PriceListQuery = z.infer<typeof PriceListQuerySchema>;
173
+
174
+ /** One published price-book version. */
175
+ export type PriceBookVersionSummary = z.infer<
176
+ typeof PriceBookVersionSummarySchema
177
+ >;
178
+
179
+ /** The price book's version history, newest first. */
180
+ export type PriceBookVersionList = z.infer<typeof PriceBookVersionListSchema>;
181
+
160
182
  /** A house-funded org's grant. */
161
183
  export type HouseFunding = z.infer<typeof HouseFundingSchema>;
@@ -332,6 +332,43 @@ export const BILLING_PROVIDERS = [
332
332
  export type BillingProvider = (typeof BILLING_PROVIDERS)[number];
333
333
  export const BillingProviderSchema = z.enum(BILLING_PROVIDERS);
334
334
 
335
+ /**
336
+ * How a provider processes a request, which selects the rate set it is billed
337
+ * at. Each mode is an EXPLICIT rate set published by the provider, never a
338
+ * multiplier we derive: OpenAI's gpt-4o "Fast" is 1.7x Standard, gpt-6-luna's is
339
+ * 2x, and Anthropic's Batch is 0.5x. "fast" is what OpenAI called "priority"
340
+ * until 2026-07-30 and what Anthropic calls fast mode.
341
+ */
342
+ export const PROCESSING_MODES = [
343
+ /** The provider's default processing. Every priced model has it. */
344
+ "standard",
345
+ /** Asynchronous batch processing. */
346
+ "batch",
347
+ /** OpenAI Flex: lower price, slower and best-effort capacity. */
348
+ "flex",
349
+ /** Premium latency (OpenAI Fast, formerly Priority; Anthropic fast mode). */
350
+ "fast",
351
+ /** OpenAI Ultrafast. */
352
+ "ultrafast",
353
+ ] as const;
354
+ export type ProcessingMode = (typeof PROCESSING_MODES)[number];
355
+ export const ProcessingModeSchema = z.enum(PROCESSING_MODES);
356
+
357
+ /**
358
+ * What a provider counts to decide which prompt-length tier a request pays.
359
+ * Anthropic (Claude Haiku 5.5, >100K): every input token, INCLUDING cache reads
360
+ * and cache writes. A model with more than one tier must name its measure; an
361
+ * ambiguous provider rule is never guessed (the long tier stays unpublished).
362
+ */
363
+ export const PROMPT_TIER_MEASURES = [
364
+ /** input + cache read + cache write. */
365
+ "input_incl_cache",
366
+ /** Fresh (uncached) input only. */
367
+ "input_excl_cache",
368
+ ] as const;
369
+ export type PromptTierMeasure = (typeof PROMPT_TIER_MEASURES)[number];
370
+ export const PromptTierMeasureSchema = z.enum(PROMPT_TIER_MEASURES);
371
+
335
372
  /* -------------------------------------------------------------------------- */
336
373
  /* Derived tables */
337
374
  /* -------------------------------------------------------------------------- */
@@ -11,6 +11,9 @@ export const openApiRoutes = {
11
11
  '/api/account/sessions/{sessionId}': ['DELETE'],
12
12
  '/api/affordances/state': ['GET'],
13
13
  '/api/analysis-workflows/{id}': ['GET'],
14
+ '/api/analysis-workflows/{id}/continue': ['POST'],
15
+ '/api/analysis-workflows/{id}/retry': ['POST'],
16
+ '/api/analysis-workflows/{id}/run-again': ['POST'],
14
17
  '/api/analysis-workflows/{id}/settlement': ['POST'],
15
18
  '/api/analysis-workflows/{id}/stop': ['POST'],
16
19
  '/api/auth/consent/grant': ['POST'],
@@ -215,9 +215,41 @@ export type {
215
215
  // chat part and by the generic projection read. Namespaced `<domain>/<flow>`
216
216
  // so domains cannot collide, and a CLOSED const array so the discriminator is
217
217
  // validated and handled exhaustively. See src/workflows/README.md.
218
- export { ANALYSIS_WORKFLOW_TYPES, isWorkflowType } from "../workflows/index";
218
+ export {
219
+ ANALYSIS_WORKFLOW_STATES,
220
+ ANALYSIS_WORKFLOW_TYPES,
221
+ isWorkflowType,
222
+ } from "../workflows/index";
223
+
224
+ export type { AnalysisWorkflowState, WorkflowType } from "../workflows/index";
225
+
226
+ // Workflow execution actions (ADR-CONTRACTS-227): `executionAffordancesFor` is
227
+ // the one total offer table from a workflow's state, its type's support and
228
+ // its frozen outcome to the workflow.* affordances. See
229
+ // src/workflows/execution-affordances.ts.
230
+ export {
231
+ DOMAIN_RELAUNCH_AFFORDANCE_IDS,
232
+ WORKFLOW_EXECUTION_ACTIONS,
233
+ executionAffordancesFor,
234
+ } from "../workflows/index";
235
+
236
+ export type {
237
+ ExecutionAffordancesInput,
238
+ WorkflowExecutionAction,
239
+ WorkflowExecutionAffordance,
240
+ WorkflowExecutionSupport,
241
+ } from "../workflows/index";
242
+
243
+ // Workflow execution projection fields (ADR-CONTRACTS-227): the offered
244
+ // workflow.* affordances, relaunch lineage and per-epoch timing the backend
245
+ // composes into its (backend-local) workflow projection. See
246
+ // src/workflows/projection-fields.ts.
247
+ export {
248
+ WorkflowExecutionAffordanceSchema,
249
+ WorkflowExecutionProjectionFieldsSchema,
250
+ } from "../workflows/index";
219
251
 
220
- export type { WorkflowType } from "../workflows/index";
252
+ export type { WorkflowExecutionProjectionFields } from "../workflows/index";
221
253
 
222
254
  // The typed outcome of a workflow's terminal transition (control ADR-CTRL-540):
223
255
  // per-type closed reasons, typed facts and an explicit disposition, classified
@@ -317,6 +349,7 @@ export {
317
349
  ReviewOpenArgsSchema,
318
350
  SourceSyncOpenUpdateArgsSchema,
319
351
  TeamOpenArgsSchema,
352
+ WorkflowExecutionArgsSchema,
320
353
  sameAffordances,
321
354
  } from "../affordances/index";
322
355
 
@@ -62,13 +62,37 @@ so every producer, validator and renderer agrees on it.
62
62
  the backend verifies the covering occurrence at route time), and `silent`
63
63
  exactly for `stopped_by_user`. A `produced`, `empty` or `partial` ending is
64
64
  never explained.
65
+ - **`ANALYSIS_WORKFLOW_STATES` is the backend's six states, in order**
66
+ (`pending`, `running`, `draining`, `stopped`, `complete`, `failed`). A
67
+ workflow id is the logical run; the backend's execution epoch is the attempt.
68
+ - **Execution actions come from one total table** (ADR-CONTRACTS-227).
69
+ `executionAffordancesFor` maps a workflow's state, its type's
70
+ `WorkflowExecutionSupport` and its frozen outcome offer to the `workflow.*`
71
+ affordances: `pending`/`running` → stop; `draining` → nothing; `stopped` →
72
+ continue, runAgain; `failed` → retry, runAgain; `complete` → runAgain. An
73
+ unsupported action is never offered, and `workflow.runAgain` is dropped
74
+ beside a frozen domain relaunch (`DOMAIN_RELAUNCH_AFFORDANCE_IDS`) — one
75
+ relaunch control, the domain's. It never emits `review.open`.
76
+ - **Projection fields, not the projection.** The workflow projection is
77
+ backend-local; `WorkflowExecutionProjectionFieldsSchema` is the one wire
78
+ shape for the fields it composes (`executionAffordances`, `relaunchedFrom`,
79
+ `epochStartedAt`, `epochCompletedAt`), every key required. Contracts does not
80
+ touch the projection's `capabilities` flags.
65
81
  - **Workflows never imports proactive.** The covering kinds are literal
66
82
  strings pinned against `PROACTIVE_EVENT_KIND_IDS` by a test; the dependency
67
83
  runs proactive → workflows only.
68
84
 
69
85
  ## Public API
70
86
 
71
- - `ANALYSIS_WORKFLOW_TYPES`, `WorkflowType`, `isWorkflowType` (`types.ts`).
87
+ - `ANALYSIS_WORKFLOW_TYPES`, `WorkflowType`, `isWorkflowType`,
88
+ `ANALYSIS_WORKFLOW_STATES`, `AnalysisWorkflowState` (`types.ts`).
89
+ - `WORKFLOW_EXECUTION_ACTIONS`, `WorkflowExecutionAction`,
90
+ `WorkflowExecutionSupport`, `WorkflowExecutionAffordance`,
91
+ `DOMAIN_RELAUNCH_AFFORDANCE_IDS`, `ExecutionAffordancesInput`,
92
+ `executionAffordancesFor` (`execution-affordances.ts`).
93
+ - `WorkflowExecutionAffordanceSchema`,
94
+ `WorkflowExecutionProjectionFieldsSchema` /
95
+ `WorkflowExecutionProjectionFields` (`projection-fields.ts`).
72
96
  - `WorkflowOutcomeSchema` / `WorkflowOutcome`, the per-type member schemas and
73
97
  `WorkflowOutcomeDispositionSchema` (`outcome.ts`).
74
98
  - `WORKFLOW_OUTCOME_KINDS` and the per-type reason tables and fact schemas
@@ -88,5 +112,6 @@ so every producer, validator and renderer agrees on it.
88
112
 
89
113
  `zod` (outcome schemas), `../input-requests/types` (the review disposition's
90
114
  request type) and `../affordances/affordance` (the offered-affordance schema,
91
- its sanity bound and `sameAffordances`). Otherwise pure vocabulary data, one
115
+ its sanity bound and `sameAffordances`) and `../affordances/args` (the
116
+ `workflow.*` args schema). Otherwise pure vocabulary data, one
92
117
  guard and pure helpers.