@greatwait/contracts 0.1.0-alpha.4 → 0.1.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/README.md +1 -1
  2. package/dist/authority.d.ts +4 -1
  3. package/dist/authority.js +1 -0
  4. package/dist/billing.d.ts +231 -1
  5. package/dist/billing.js +150 -1
  6. package/dist/callback-safety.js +13 -3
  7. package/dist/configuration-bounds.d.ts +13 -0
  8. package/dist/configuration-bounds.js +13 -0
  9. package/dist/data-export.d.ts +2 -0
  10. package/dist/delivery.d.ts +26 -0
  11. package/dist/delivery.js +2 -0
  12. package/dist/effective-limits.d.ts +40 -0
  13. package/dist/effective-limits.js +42 -0
  14. package/dist/endpoints.d.ts +173 -5
  15. package/dist/endpoints.js +77 -6
  16. package/dist/first-value-journey.d.ts +1 -1
  17. package/dist/first-value-journey.js +11 -7
  18. package/dist/forecast.d.ts +111 -21
  19. package/dist/forecast.js +87 -24
  20. package/dist/http-request.d.ts +193 -0
  21. package/dist/http-request.js +305 -0
  22. package/dist/identity.d.ts +4 -4
  23. package/dist/index.d.ts +9 -6
  24. package/dist/index.js +8 -5
  25. package/dist/mcp.d.ts +2 -1
  26. package/dist/mcp.js +1 -1
  27. package/dist/members.d.ts +141 -0
  28. package/dist/members.js +223 -0
  29. package/dist/oauth.d.ts +7 -0
  30. package/dist/operation-definitions.d.ts +20 -4
  31. package/dist/operation-definitions.js +9 -1
  32. package/dist/operational-config.d.ts +101 -0
  33. package/dist/operational-config.js +341 -0
  34. package/dist/operations.d.ts +1299 -52
  35. package/dist/operations.js +466 -32
  36. package/dist/organization-lifecycle.d.ts +10 -0
  37. package/dist/partition.d.ts +17 -9
  38. package/dist/partition.js +3 -3
  39. package/dist/policy.js +5 -2
  40. package/dist/problems.d.ts +25 -2
  41. package/dist/problems.js +49 -1
  42. package/dist/protocol.d.ts +5 -5
  43. package/dist/protocol.js +2 -1
  44. package/dist/scheduling-records.d.ts +226 -0
  45. package/dist/scheduling-records.js +5 -0
  46. package/dist/scheduling.d.ts +273 -1
  47. package/dist/scheduling.js +91 -4
  48. package/dist/secrets.d.ts +7 -0
  49. package/dist/tenancy.d.ts +1 -1
  50. package/dist/tenancy.js +1 -1
  51. package/dist/usage.js +6 -21
  52. package/dist/zod-values.d.ts +2 -2
  53. package/dist/zod-values.js +1 -1
  54. package/generated/v1/examples/attempts.list.response.json +2 -0
  55. package/generated/v1/examples/attempts.read.response.json +2 -0
  56. package/generated/v1/examples/billing.fundingconfirm.response.json +120 -0
  57. package/generated/v1/examples/billing.invoicehistory.response.json +6 -0
  58. package/generated/v1/examples/billing.invoices.response.json +15 -0
  59. package/generated/v1/examples/billing.paymenthistory.response.json +6 -0
  60. package/generated/v1/examples/billing.paymentmethod.response.json +16 -0
  61. package/generated/v1/examples/billing.paymentmethodconfirm.request.json +3 -0
  62. package/generated/v1/examples/billing.paymentmethodconfirm.response.json +16 -0
  63. package/generated/v1/examples/billing.paymentmethodremove.request.json +3 -0
  64. package/generated/v1/examples/billing.paymentmethodremove.response.json +4 -0
  65. package/generated/v1/examples/billing.paymentmethodsetup.request.json +1 -0
  66. package/generated/v1/examples/billing.paymentmethodsetup.response.json +3 -0
  67. package/generated/v1/examples/endpoints.ensure.request.json +3 -0
  68. package/generated/v1/examples/endpoints.ensure.response.json +20 -0
  69. package/generated/v1/examples/invitations.create.request.json +5 -0
  70. package/generated/v1/examples/invitations.create.response.json +7 -0
  71. package/generated/v1/examples/invitations.list.response.json +11 -0
  72. package/generated/v1/examples/invitations.resend.request.json +1 -0
  73. package/generated/v1/examples/invitations.resend.response.json +4 -0
  74. package/generated/v1/examples/invitations.revoke.request.json +3 -0
  75. package/generated/v1/examples/invitations.revoke.response.json +4 -0
  76. package/generated/v1/examples/limits.read.response.json +16 -0
  77. package/generated/v1/examples/members.list.response.json +13 -0
  78. package/generated/v1/examples/members.remove.request.json +3 -0
  79. package/generated/v1/examples/members.remove.response.json +4 -0
  80. package/generated/v1/examples/members.role.request.json +3 -0
  81. package/generated/v1/examples/members.role.response.json +4 -0
  82. package/generated/v1/examples/occurrences.redeliver.response.json +2 -0
  83. package/generated/v1/examples/projectmembers.add.request.json +1 -0
  84. package/generated/v1/examples/projectmembers.add.response.json +4 -0
  85. package/generated/v1/examples/projectmembers.remove.request.json +3 -0
  86. package/generated/v1/examples/projectmembers.remove.response.json +4 -0
  87. package/generated/v1/first-value-journey.json +24 -8
  88. package/generated/v1/manifest.json +627 -10
  89. package/generated/v1/openapi.json +29081 -15663
  90. package/generated/v1/policy-matrix.json +137 -2
  91. package/generated/v1/schemas/attempts.list.response.json +15 -0
  92. package/generated/v1/schemas/attempts.read.response.json +15 -0
  93. package/generated/v1/schemas/billing.fundingconfirm.response.json +552 -0
  94. package/generated/v1/schemas/billing.invoicehistory.query.json +16 -0
  95. package/generated/v1/schemas/billing.invoicehistory.response.json +88 -0
  96. package/generated/v1/schemas/billing.invoices.response.json +51 -0
  97. package/generated/v1/schemas/billing.ledger.response.json +5 -0
  98. package/generated/v1/schemas/billing.notificationsettingsupdate.request.json +24 -17
  99. package/generated/v1/schemas/billing.paymenthistory.query.json +16 -0
  100. package/generated/v1/schemas/billing.paymenthistory.response.json +182 -0
  101. package/generated/v1/schemas/billing.paymentmethod.response.json +137 -0
  102. package/generated/v1/schemas/billing.paymentmethodconfirm.request.json +14 -0
  103. package/generated/v1/schemas/billing.paymentmethodconfirm.response.json +137 -0
  104. package/generated/v1/schemas/billing.paymentmethodremove.request.json +12 -0
  105. package/generated/v1/schemas/billing.paymentmethodremove.response.json +137 -0
  106. package/generated/v1/schemas/billing.paymentmethodsetup.request.json +12 -0
  107. package/generated/v1/schemas/billing.paymentmethodsetup.response.json +11 -0
  108. package/generated/v1/schemas/billing.purchase.request.json +4 -0
  109. package/generated/v1/schemas/context.read.response.json +1 -0
  110. package/generated/v1/schemas/credentials.connection.response.json +1 -0
  111. package/generated/v1/schemas/credentials.create.request.json +1 -0
  112. package/generated/v1/schemas/credentials.create.response.json +1 -0
  113. package/generated/v1/schemas/credentials.list.response.json +1 -0
  114. package/generated/v1/schemas/credentials.revoke.response.json +1 -0
  115. package/generated/v1/schemas/credentials.rotate.response.json +2 -0
  116. package/generated/v1/schemas/endpoints.acknowledgeSigningKeyRotation.response.json +59 -0
  117. package/generated/v1/schemas/endpoints.create.request.json +74 -0
  118. package/generated/v1/schemas/endpoints.create.response.json +59 -0
  119. package/generated/v1/schemas/endpoints.disable.response.json +59 -0
  120. package/generated/v1/schemas/endpoints.enable.response.json +59 -0
  121. package/generated/v1/schemas/endpoints.ensure.request.json +91 -0
  122. package/generated/v1/schemas/endpoints.ensure.response.json +169 -0
  123. package/generated/v1/schemas/endpoints.list.response.json +59 -0
  124. package/generated/v1/schemas/endpoints.read.response.json +59 -0
  125. package/generated/v1/schemas/endpoints.rotateSigningKey.response.json +59 -0
  126. package/generated/v1/schemas/endpoints.update.request.json +74 -0
  127. package/generated/v1/schemas/endpoints.update.response.json +59 -0
  128. package/generated/v1/schemas/forecast.read.request-headers.json +10 -0
  129. package/generated/v1/schemas/forecast.read.response.json +148 -0
  130. package/generated/v1/schemas/grants.list.response.json +1 -0
  131. package/generated/v1/schemas/grants.revoke.response.json +1 -0
  132. package/generated/v1/schemas/invitations.create.request-headers.json +13 -0
  133. package/generated/v1/schemas/invitations.create.request.json +24 -0
  134. package/generated/v1/schemas/invitations.create.response.json +31 -0
  135. package/generated/v1/schemas/invitations.list.response.json +40 -0
  136. package/generated/v1/schemas/invitations.resend.request-headers.json +13 -0
  137. package/generated/v1/schemas/invitations.resend.request.json +6 -0
  138. package/generated/v1/schemas/invitations.resend.response.json +27 -0
  139. package/generated/v1/schemas/invitations.revoke.request-headers.json +13 -0
  140. package/generated/v1/schemas/invitations.revoke.request.json +12 -0
  141. package/generated/v1/schemas/invitations.revoke.response.json +27 -0
  142. package/generated/v1/schemas/limits.read.response.json +79 -0
  143. package/generated/v1/schemas/members.list.response.json +53 -0
  144. package/generated/v1/schemas/members.remove.request-headers.json +13 -0
  145. package/generated/v1/schemas/members.remove.request.json +12 -0
  146. package/generated/v1/schemas/members.remove.response.json +27 -0
  147. package/generated/v1/schemas/members.role.request-headers.json +13 -0
  148. package/generated/v1/schemas/members.role.request.json +12 -0
  149. package/generated/v1/schemas/members.role.response.json +27 -0
  150. package/generated/v1/schemas/occurrences.redeliver.response.json +15 -0
  151. package/generated/v1/schemas/projectmembers.add.request-headers.json +13 -0
  152. package/generated/v1/schemas/projectmembers.add.request.json +6 -0
  153. package/generated/v1/schemas/projectmembers.add.response.json +27 -0
  154. package/generated/v1/schemas/projectmembers.remove.request-headers.json +13 -0
  155. package/generated/v1/schemas/projectmembers.remove.request.json +12 -0
  156. package/generated/v1/schemas/projectmembers.remove.response.json +27 -0
  157. package/generated/v1/schemas/schedules.archive.response.json +138 -1
  158. package/generated/v1/schemas/schedules.cancel.response.json +138 -1
  159. package/generated/v1/schemas/schedules.create.request.json +150 -1
  160. package/generated/v1/schemas/schedules.create.response.json +156 -1
  161. package/generated/v1/schemas/schedules.list.response.json +138 -1
  162. package/generated/v1/schemas/schedules.pause.response.json +138 -1
  163. package/generated/v1/schemas/schedules.read.response.json +138 -1
  164. package/generated/v1/schemas/schedules.replace.request.json +150 -1
  165. package/generated/v1/schemas/schedules.replace.response.json +156 -1
  166. package/generated/v1/schemas/schedules.resume.response.json +138 -1
  167. package/generated/v1/snippets/billing.fundingconfirm.json +6 -0
  168. package/generated/v1/snippets/billing.invoicehistory.json +6 -0
  169. package/generated/v1/snippets/billing.invoices.json +6 -0
  170. package/generated/v1/snippets/billing.notificationsettingsupdate.json +1 -1
  171. package/generated/v1/snippets/billing.paymenthistory.json +6 -0
  172. package/generated/v1/snippets/billing.paymentmethod.json +6 -0
  173. package/generated/v1/snippets/billing.paymentmethodconfirm.json +6 -0
  174. package/generated/v1/snippets/billing.paymentmethodremove.json +6 -0
  175. package/generated/v1/snippets/billing.paymentmethodsetup.json +6 -0
  176. package/generated/v1/snippets/endpoints.create.json +1 -1
  177. package/generated/v1/snippets/endpoints.ensure.json +6 -0
  178. package/generated/v1/snippets/forecast.windows.json +1 -1
  179. package/generated/v1/snippets/invitations.create.json +6 -0
  180. package/generated/v1/snippets/invitations.list.json +6 -0
  181. package/generated/v1/snippets/invitations.resend.json +6 -0
  182. package/generated/v1/snippets/invitations.revoke.json +6 -0
  183. package/generated/v1/snippets/journey/create-destination.json +1 -1
  184. package/generated/v1/snippets/limits.read.json +6 -0
  185. package/generated/v1/snippets/members.list.json +6 -0
  186. package/generated/v1/snippets/members.remove.json +6 -0
  187. package/generated/v1/snippets/members.role.json +6 -0
  188. package/generated/v1/snippets/projectmembers.add.json +6 -0
  189. package/generated/v1/snippets/projectmembers.remove.json +6 -0
  190. package/package.json +7 -2
@@ -53,10 +53,11 @@ export declare const FORECAST_BOUNDS: {
53
53
  * argument for refusing rather than under-reporting collapses if staleness is
54
54
  * the one degradation that gets through as a number.
55
55
  */
56
- readonly maxLagSeconds: 600;
56
+ readonly maxLagSeconds: number;
57
57
  /**
58
- * Instants one partition may project into a single snapshot before it reports
59
- * the answer as short.
58
+ * Actual recurrence work units (shared cron advances or interval-series
59
+ * calculations) before a partition reports the answer as short. Output-cell
60
+ * allocation is bounded independently by maxGroupsPerPartition.
60
61
  *
61
62
  * Published rather than kept internal so a refusal is classifiable: a shadow
62
63
  * comparison that sees a refusal needs to be able to tell "we are near a
@@ -83,7 +84,7 @@ export declare const FORECAST_BOUNDS: {
83
84
  * an incident. `freshness.lagSeconds` reports the actual age on every answer;
84
85
  * this is the bound it lives inside.
85
86
  */
86
- readonly refreshCeilingSeconds: 300;
87
+ readonly refreshCeilingSeconds: number;
87
88
  /**
88
89
  * Shortest gap between refreshes once something *has* been written.
89
90
  *
@@ -92,7 +93,7 @@ export declare const FORECAST_BOUNDS: {
92
93
  * projection that rebuilt on every poll would be the cost this whole design
93
94
  * exists to avoid.
94
95
  */
95
- readonly refreshFloorSeconds: 30;
96
+ readonly refreshFloorSeconds: number;
96
97
  /**
97
98
  * How far below `now` a window may begin.
98
99
  *
@@ -102,7 +103,7 @@ export declare const FORECAST_BOUNDS: {
102
103
  * because the caller's `now` is a second behind. Further back than that is a
103
104
  * question about the past, which a forecast does not answer.
104
105
  */
105
- readonly slackSeconds: 120;
106
+ readonly slackSeconds: number;
106
107
  /**
107
108
  * Customer keys named individually in one bucket before the rest are summed.
108
109
  *
@@ -214,7 +215,106 @@ export declare const forecastBucketSchema: import("zod").ZodObject<{
214
215
  start: import("zod").ZodString;
215
216
  }, import("zod/v4/core").$strip>;
216
217
  export type ForecastBucket = z.infer<typeof forecastBucketSchema>;
218
+ /** Capability opt-in prevents old clients that read only counts from mistaking
219
+ * a lower bound for complete work. Older servers safely ignore this header. */
220
+ export declare const FORECAST_PARTIAL_ACCEPT_HEADER = "X-Greatwait-Accept-Partial-Forecast";
221
+ export declare const forecastRequestHeadersSchema: import("zod").ZodObject<{
222
+ "X-Greatwait-Accept-Partial-Forecast": import("zod").ZodOptional<import("zod").ZodLiteral<"true">>;
223
+ }, import("zod/v4/core").$strip>;
224
+ export declare const forecastCoverageSchema: import("zod").ZodObject<{
225
+ complete: import("zod").ZodBoolean;
226
+ countMeaning: import("zod").ZodEnum<{
227
+ "complete-total": "complete-total";
228
+ "lower-bound": "lower-bound";
229
+ }>;
230
+ projectedWindow: import("zod").ZodObject<{
231
+ from: import("zod").ZodString;
232
+ to: import("zod").ZodString;
233
+ }, import("zod/v4/core").$strip>;
234
+ completeThrough: import("zod").ZodString;
235
+ heads: import("zod").ZodObject<{
236
+ groupsRead: import("zod").ZodNumber;
237
+ occurrencesCounted: import("zod").ZodNumber;
238
+ complete: import("zod").ZodBoolean;
239
+ selection: import("zod").ZodLiteral<"earliest bucket, latency class, partition key groups">;
240
+ lastGroup: import("zod").ZodOptional<import("zod").ZodObject<{
241
+ start: import("zod").ZodString;
242
+ latencyClass: import("zod").ZodEnum<{
243
+ realtime: "realtime";
244
+ bulk: "bulk";
245
+ }>;
246
+ partitionKey: import("zod").ZodString;
247
+ }, import("zod/v4/core").$strip>>;
248
+ }, import("zod/v4/core").$strip>;
249
+ recurring: import("zod").ZodObject<{
250
+ schedulesRead: import("zod").ZodNumber;
251
+ populationComplete: import("zod").ZodBoolean;
252
+ selection: import("zod").ZodLiteral<"oldest eligible active recurring schedules by createdAt and scheduleId">;
253
+ firstScheduleId: import("zod").ZodOptional<import("zod").ZodString>;
254
+ lastScheduleId: import("zod").ZodOptional<import("zod").ZodString>;
255
+ }, import("zod/v4/core").$strip>;
256
+ work: import("zod").ZodObject<{
257
+ usedUnits: import("zod").ZodNumber;
258
+ limitUnits: import("zod").ZodNumber;
259
+ }, import("zod/v4/core").$strip>;
260
+ boundsReached: import("zod").ZodArray<import("zod").ZodObject<{
261
+ kind: import("zod").ZodEnum<{
262
+ "head-cells": "head-cells";
263
+ "recurring-series": "recurring-series";
264
+ "projection-work": "projection-work";
265
+ "projection-cells": "projection-cells";
266
+ }>;
267
+ limit: import("zod").ZodNumber;
268
+ }, import("zod/v4/core").$strip>>;
269
+ }, import("zod/v4/core").$strip>;
270
+ export type ForecastCoverage = z.infer<typeof forecastCoverageSchema>;
217
271
  export declare const forecastSchema: import("zod").ZodObject<{
272
+ coverage: import("zod").ZodOptional<import("zod").ZodObject<{
273
+ complete: import("zod").ZodBoolean;
274
+ countMeaning: import("zod").ZodEnum<{
275
+ "complete-total": "complete-total";
276
+ "lower-bound": "lower-bound";
277
+ }>;
278
+ projectedWindow: import("zod").ZodObject<{
279
+ from: import("zod").ZodString;
280
+ to: import("zod").ZodString;
281
+ }, import("zod/v4/core").$strip>;
282
+ completeThrough: import("zod").ZodString;
283
+ heads: import("zod").ZodObject<{
284
+ groupsRead: import("zod").ZodNumber;
285
+ occurrencesCounted: import("zod").ZodNumber;
286
+ complete: import("zod").ZodBoolean;
287
+ selection: import("zod").ZodLiteral<"earliest bucket, latency class, partition key groups">;
288
+ lastGroup: import("zod").ZodOptional<import("zod").ZodObject<{
289
+ start: import("zod").ZodString;
290
+ latencyClass: import("zod").ZodEnum<{
291
+ realtime: "realtime";
292
+ bulk: "bulk";
293
+ }>;
294
+ partitionKey: import("zod").ZodString;
295
+ }, import("zod/v4/core").$strip>>;
296
+ }, import("zod/v4/core").$strip>;
297
+ recurring: import("zod").ZodObject<{
298
+ schedulesRead: import("zod").ZodNumber;
299
+ populationComplete: import("zod").ZodBoolean;
300
+ selection: import("zod").ZodLiteral<"oldest eligible active recurring schedules by createdAt and scheduleId">;
301
+ firstScheduleId: import("zod").ZodOptional<import("zod").ZodString>;
302
+ lastScheduleId: import("zod").ZodOptional<import("zod").ZodString>;
303
+ }, import("zod/v4/core").$strip>;
304
+ work: import("zod").ZodObject<{
305
+ usedUnits: import("zod").ZodNumber;
306
+ limitUnits: import("zod").ZodNumber;
307
+ }, import("zod/v4/core").$strip>;
308
+ boundsReached: import("zod").ZodArray<import("zod").ZodObject<{
309
+ kind: import("zod").ZodEnum<{
310
+ "head-cells": "head-cells";
311
+ "recurring-series": "recurring-series";
312
+ "projection-work": "projection-work";
313
+ "projection-cells": "projection-cells";
314
+ }>;
315
+ limit: import("zod").ZodNumber;
316
+ }, import("zod/v4/core").$strip>>;
317
+ }, import("zod/v4/core").$strip>>;
218
318
  buckets: import("zod").ZodArray<import("zod").ZodObject<{
219
319
  byLatencyClass: import("zod").ZodObject<{
220
320
  bulk: import("zod").ZodNumber;
@@ -245,6 +345,7 @@ export interface ForecastMergedBucket {
245
345
  readonly byPartitionKey: Record<string, number>;
246
346
  count: number;
247
347
  nextFireCount: number;
348
+ omittedPartitionKeyCount?: number;
248
349
  unkeyedCount: number;
249
350
  }
250
351
  /**
@@ -301,26 +402,15 @@ export type ForecastAnswerDecision = {
301
402
  /**
302
403
  * Whether a gathered forecast may be published, and how old it is.
303
404
  *
304
- * **Every degradation this operation can suffer ends here, and every one of
305
- * them is a refusal.** That is the whole safety argument of the shape, and it
306
- * follows from a measured fact about the consumer it exists for: it reads the
307
- * bucket total and nothing else — not a degraded flag, not `freshness` — so a
308
- * degraded answer reads to it as a quiet book, and a quiet book makes it
309
- * release capacity. Its own failure path is fail-safe: a request that errors
310
- * makes it hold its last good reading. So the honest degradation is the one
311
- * that errors.
312
- *
313
- * The three are distinct because they mean different things to whoever is
314
- * looking: a partition that did not answer is an availability problem, a book
315
- * past the projection bound is a capacity problem with a published number
316
- * beside it, and an answer older than the bound is a partition that has stopped
317
- * refreshing.
405
+ * Unavailable and stale sources always refuse. Work-bound partial answers are
406
+ * permitted only for a reader that explicitly understands coverage; the
407
+ * default retains the count-only legacy consumer's safe failure behavior.
318
408
  *
319
409
  * `asOfMs` is the **oldest** across the shards, for the reason
320
410
  * `projectionCoverage` takes the oldest checkpoint: one busy shard must not be
321
411
  * allowed to vouch for three quiet ones.
322
412
  */
323
- export declare function decideForecastAnswer(answers: readonly ForecastShardAnswer[], nowMs: number): ForecastAnswerDecision;
413
+ export declare function decideForecastAnswer(answers: readonly ForecastShardAnswer[], nowMs: number, acceptPartial?: boolean): ForecastAnswerDecision;
324
414
  /**
325
415
  * The version of the transitional window shape.
326
416
  *
package/dist/forecast.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { MAX_FORECAST_HORIZON_SECONDS } from "./configuration-bounds.js";
2
+ import { OPERATIONAL_TIMING } from "./operational-config.js";
1
3
  import { z } from "./zod.js";
2
4
  import { freshnessSchema, projectedFreshnessSchema, } from "./protocol.js";
3
5
  import { timestampSchema } from "./time.js";
@@ -55,10 +57,11 @@ export const FORECAST_BOUNDS = {
55
57
  * argument for refusing rather than under-reporting collapses if staleness is
56
58
  * the one degradation that gets through as a number.
57
59
  */
58
- maxLagSeconds: 600,
60
+ maxLagSeconds: (OPERATIONAL_TIMING.forecastRefreshMaximumMs * 2) / 1000,
59
61
  /**
60
- * Instants one partition may project into a single snapshot before it reports
61
- * the answer as short.
62
+ * Actual recurrence work units (shared cron advances or interval-series
63
+ * calculations) before a partition reports the answer as short. Output-cell
64
+ * allocation is bounded independently by maxGroupsPerPartition.
62
65
  *
63
66
  * Published rather than kept internal so a refusal is classifiable: a shadow
64
67
  * comparison that sees a refusal needs to be able to tell "we are near a
@@ -75,7 +78,7 @@ export const FORECAST_BOUNDS = {
75
78
  * legacy service's seven-day ceiling bounded the *query* with no snapshot
76
79
  * behind it at all.
77
80
  */
78
- maxWindowSeconds: 25 * 60 * 60,
81
+ maxWindowSeconds: MAX_FORECAST_HORIZON_SECONDS,
79
82
  minBucketSeconds: 60,
80
83
  /**
81
84
  * Longest a partition lets its snapshot stand when nothing has been written.
@@ -85,7 +88,7 @@ export const FORECAST_BOUNDS = {
85
88
  * an incident. `freshness.lagSeconds` reports the actual age on every answer;
86
89
  * this is the bound it lives inside.
87
90
  */
88
- refreshCeilingSeconds: 300,
91
+ refreshCeilingSeconds: OPERATIONAL_TIMING.forecastRefreshMaximumMs / 1000,
89
92
  /**
90
93
  * Shortest gap between refreshes once something *has* been written.
91
94
  *
@@ -94,7 +97,7 @@ export const FORECAST_BOUNDS = {
94
97
  * projection that rebuilt on every poll would be the cost this whole design
95
98
  * exists to avoid.
96
99
  */
97
- refreshFloorSeconds: 30,
100
+ refreshFloorSeconds: OPERATIONAL_TIMING.forecastRefreshFloorMs / 1000,
98
101
  /**
99
102
  * How far below `now` a window may begin.
100
103
  *
@@ -104,7 +107,7 @@ export const FORECAST_BOUNDS = {
104
107
  * because the caller's `now` is a second behind. Further back than that is a
105
108
  * question about the past, which a forecast does not answer.
106
109
  */
107
- slackSeconds: 120,
110
+ slackSeconds: OPERATIONAL_TIMING.forecastCoverageSlackMs / 1000,
108
111
  /**
109
112
  * Customer keys named individually in one bucket before the rest are summed.
110
113
  *
@@ -337,7 +340,73 @@ export const forecastBucketSchema = z.object({
337
340
  /** The bucket's epoch-aligned start. May precede the requested `from`. */
338
341
  start: timestampSchema,
339
342
  });
343
+ /** Capability opt-in prevents old clients that read only counts from mistaking
344
+ * a lower bound for complete work. Older servers safely ignore this header. */
345
+ export const FORECAST_PARTIAL_ACCEPT_HEADER = "X-Greatwait-Accept-Partial-Forecast";
346
+ export const forecastRequestHeadersSchema = z.object({
347
+ [FORECAST_PARTIAL_ACCEPT_HEADER]: z.literal("true").optional(),
348
+ });
349
+ export const forecastCoverageSchema = z
350
+ .object({
351
+ complete: z.boolean(),
352
+ countMeaning: z.enum(["complete-total", "lower-bound"]),
353
+ projectedWindow: z.object({ from: timestampSchema, to: timestampSchema }),
354
+ /** No interval after this can be assumed quiet from absent buckets. */
355
+ completeThrough: timestampSchema,
356
+ heads: z.object({
357
+ groupsRead: z.number().int().nonnegative(),
358
+ occurrencesCounted: z.number().int().nonnegative(),
359
+ complete: z.boolean(),
360
+ selection: z.literal("earliest bucket, latency class, partition key groups"),
361
+ lastGroup: z
362
+ .object({
363
+ start: timestampSchema,
364
+ latencyClass: z.enum(["realtime", "bulk"]),
365
+ partitionKey: z.string(),
366
+ })
367
+ .optional(),
368
+ }),
369
+ recurring: z.object({
370
+ schedulesRead: z.number().int().nonnegative(),
371
+ /** The selected population is complete, not necessarily its expansion. */
372
+ populationComplete: z.boolean(),
373
+ selection: z.literal("oldest eligible active recurring schedules by createdAt and scheduleId"),
374
+ firstScheduleId: z.string().optional(),
375
+ lastScheduleId: z.string().optional(),
376
+ }),
377
+ work: z.object({
378
+ usedUnits: z.number().int().nonnegative(),
379
+ limitUnits: z.number().int().nonnegative(),
380
+ }),
381
+ boundsReached: z.array(z.object({
382
+ kind: z.enum([
383
+ "head-cells",
384
+ "recurring-series",
385
+ "projection-work",
386
+ "projection-cells",
387
+ ]),
388
+ limit: z.number().int().nonnegative(),
389
+ })),
390
+ })
391
+ .superRefine((coverage, context) => {
392
+ if ((coverage.countMeaning === "complete-total") !== coverage.complete ||
393
+ (coverage.complete &&
394
+ (coverage.boundsReached.length !== 0 ||
395
+ !coverage.heads.complete ||
396
+ !coverage.recurring.populationComplete ||
397
+ coverage.completeThrough !== coverage.projectedWindow.to)) ||
398
+ (!coverage.complete &&
399
+ (coverage.boundsReached.length === 0 ||
400
+ coverage.completeThrough !== coverage.projectedWindow.from))) {
401
+ context.addIssue({
402
+ code: "custom",
403
+ message: "Coverage must distinguish complete totals from lower bounds without claiming an unproven time prefix",
404
+ });
405
+ }
406
+ });
340
407
  export const forecastSchema = z.object({
408
+ /** Optional for older servers and callers without the coverage capability. */
409
+ coverage: forecastCoverageSchema.optional(),
341
410
  /** Ascending by `start`. A bucket with no occurrences is omitted. */
342
411
  buckets: z.array(forecastBucketSchema),
343
412
  /**
@@ -391,6 +460,10 @@ export function mergeForecastBuckets(buckets) {
391
460
  }
392
461
  into.count += bucket.count;
393
462
  into.nextFireCount += bucket.nextFireCount;
463
+ if (bucket.omittedPartitionKeyCount !== undefined) {
464
+ into.omittedPartitionKeyCount =
465
+ (into.omittedPartitionKeyCount ?? 0) + bucket.omittedPartitionKeyCount;
466
+ }
394
467
  into.unkeyedCount += bucket.unkeyedCount;
395
468
  }
396
469
  return [...merged.values()].sort((left, right) => left.bucketStartMs - right.bucketStartMs);
@@ -412,7 +485,7 @@ export function mergeForecastBuckets(buckets) {
412
485
  export function topPartitionKeys(bucket) {
413
486
  const ranked = Object.entries(bucket.byPartitionKey).sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]));
414
487
  const byPartitionKey = Object.create(null);
415
- let otherPartitionKeyCount = bucket.unkeyedCount;
488
+ let otherPartitionKeyCount = bucket.unkeyedCount + (bucket.omittedPartitionKeyCount ?? 0);
416
489
  ranked.forEach(([key, n], index) => {
417
490
  if (index < FORECAST_BOUNDS.topPartitionKeys) {
418
491
  byPartitionKey[key] = n;
@@ -426,30 +499,20 @@ export function topPartitionKeys(bucket) {
426
499
  /**
427
500
  * Whether a gathered forecast may be published, and how old it is.
428
501
  *
429
- * **Every degradation this operation can suffer ends here, and every one of
430
- * them is a refusal.** That is the whole safety argument of the shape, and it
431
- * follows from a measured fact about the consumer it exists for: it reads the
432
- * bucket total and nothing else — not a degraded flag, not `freshness` — so a
433
- * degraded answer reads to it as a quiet book, and a quiet book makes it
434
- * release capacity. Its own failure path is fail-safe: a request that errors
435
- * makes it hold its last good reading. So the honest degradation is the one
436
- * that errors.
437
- *
438
- * The three are distinct because they mean different things to whoever is
439
- * looking: a partition that did not answer is an availability problem, a book
440
- * past the projection bound is a capacity problem with a published number
441
- * beside it, and an answer older than the bound is a partition that has stopped
442
- * refreshing.
502
+ * Unavailable and stale sources always refuse. Work-bound partial answers are
503
+ * permitted only for a reader that explicitly understands coverage; the
504
+ * default retains the count-only legacy consumer's safe failure behavior.
443
505
  *
444
506
  * `asOfMs` is the **oldest** across the shards, for the reason
445
507
  * `projectionCoverage` takes the oldest checkpoint: one busy shard must not be
446
508
  * allowed to vouch for three quiet ones.
447
509
  */
448
- export function decideForecastAnswer(answers, nowMs) {
510
+ export function decideForecastAnswer(answers, nowMs, acceptPartial = false) {
449
511
  if (answers.some((answer) => "refused" in answer)) {
450
512
  return { ok: false, refusal: "partition-unavailable" };
451
513
  }
452
- if (answers.some((answer) => "truncated" in answer && answer.truncated)) {
514
+ if (!acceptPartial &&
515
+ answers.some((answer) => "truncated" in answer && answer.truncated)) {
453
516
  return { ok: false, refusal: "book-too-large" };
454
517
  }
455
518
  // **Only a partition that built a snapshot vouches for freshness.** One that
@@ -0,0 +1,193 @@
1
+ import { z } from "./zod.js";
2
+ export declare const HTTP_HEADER_BOUNDS: {
3
+ readonly count: 32;
4
+ readonly nameBytes: 128;
5
+ readonly valueBytes: 8192;
6
+ readonly totalBytes: 16384;
7
+ };
8
+ export declare const HTTP_METHODS: readonly ["POST", "PUT", "GET"];
9
+ /** Platform-owned authentication, content and transport fields cannot be overridden. */
10
+ export declare function reservedHttpHeaderReason(name: string): string | undefined;
11
+ export declare const httpHeaderNameSchema: import("zod").ZodString;
12
+ export declare const httpHeaderValueSchema: import("zod").ZodString;
13
+ export declare const headerChangeSchema: import("zod").ZodDiscriminatedUnion<[import("zod").ZodObject<{
14
+ action: import("zod").ZodLiteral<"set">;
15
+ name: import("zod").ZodString;
16
+ value: import("zod").ZodString;
17
+ secret: import("zod").ZodOptional<import("zod").ZodBoolean>;
18
+ }, import("zod/v4/core").$strict>, import("zod").ZodObject<{
19
+ action: import("zod").ZodLiteral<"keep">;
20
+ name: import("zod").ZodString;
21
+ }, import("zod/v4/core").$strict>, import("zod").ZodObject<{
22
+ action: import("zod").ZodLiteral<"remove">;
23
+ name: import("zod").ZodString;
24
+ }, import("zod/v4/core").$strict>, import("zod").ZodObject<{
25
+ action: import("zod").ZodLiteral<"suppress">;
26
+ name: import("zod").ZodString;
27
+ }, import("zod/v4/core").$strict>], "action">;
28
+ export type HeaderChange = z.infer<typeof headerChangeSchema>;
29
+ export declare const headerChangesSchema: import("zod").ZodArray<import("zod").ZodDiscriminatedUnion<[import("zod").ZodObject<{
30
+ action: import("zod").ZodLiteral<"set">;
31
+ name: import("zod").ZodString;
32
+ value: import("zod").ZodString;
33
+ secret: import("zod").ZodOptional<import("zod").ZodBoolean>;
34
+ }, import("zod/v4/core").$strict>, import("zod").ZodObject<{
35
+ action: import("zod").ZodLiteral<"keep">;
36
+ name: import("zod").ZodString;
37
+ }, import("zod/v4/core").$strict>, import("zod").ZodObject<{
38
+ action: import("zod").ZodLiteral<"remove">;
39
+ name: import("zod").ZodString;
40
+ }, import("zod/v4/core").$strict>, import("zod").ZodObject<{
41
+ action: import("zod").ZodLiteral<"suppress">;
42
+ name: import("zod").ZodString;
43
+ }, import("zod/v4/core").$strict>], "action">>;
44
+ export declare const endpointHeaderChangesSchema: import("zod").ZodArray<import("zod").ZodDiscriminatedUnion<[import("zod").ZodObject<{
45
+ action: import("zod").ZodLiteral<"set">;
46
+ name: import("zod").ZodString;
47
+ value: import("zod").ZodString;
48
+ secret: import("zod").ZodOptional<import("zod").ZodBoolean>;
49
+ }, import("zod/v4/core").$strict>, import("zod").ZodObject<{
50
+ action: import("zod").ZodLiteral<"keep">;
51
+ name: import("zod").ZodString;
52
+ }, import("zod/v4/core").$strict>, import("zod").ZodObject<{
53
+ action: import("zod").ZodLiteral<"remove">;
54
+ name: import("zod").ZodString;
55
+ }, import("zod/v4/core").$strict>, import("zod").ZodObject<{
56
+ action: import("zod").ZodLiteral<"suppress">;
57
+ name: import("zod").ZodString;
58
+ }, import("zod/v4/core").$strict>], "action">>;
59
+ export declare const publicHttpHeaderSchema: import("zod").ZodUnion<readonly [import("zod").ZodObject<{
60
+ name: import("zod").ZodString;
61
+ secret: import("zod").ZodLiteral<false>;
62
+ value: import("zod").ZodString;
63
+ }, import("zod/v4/core").$strict>, import("zod").ZodObject<{
64
+ name: import("zod").ZodString;
65
+ secret: import("zod").ZodLiteral<true>;
66
+ configured: import("zod").ZodLiteral<true>;
67
+ value: import("zod").ZodOptional<import("zod").ZodNever>;
68
+ }, import("zod/v4/core").$strict>, import("zod").ZodObject<{
69
+ name: import("zod").ZodString;
70
+ suppressed: import("zod").ZodLiteral<true>;
71
+ }, import("zod/v4/core").$strict>]>;
72
+ export type PublicHttpHeader = z.infer<typeof publicHttpHeaderSchema>;
73
+ export declare const httpRequestSchema: import("zod").ZodObject<{
74
+ method: import("zod").ZodOptional<import("zod").ZodEnum<{
75
+ POST: "POST";
76
+ PUT: "PUT";
77
+ GET: "GET";
78
+ }>>;
79
+ body: import("zod").ZodOptional<import("zod").ZodDiscriminatedUnion<[import("zod").ZodObject<{
80
+ mode: import("zod").ZodLiteral<"envelope">;
81
+ }, import("zod/v4/core").$strict>, import("zod").ZodObject<{
82
+ mode: import("zod").ZodLiteral<"json">;
83
+ value: import("zod").ZodJSONSchema;
84
+ }, import("zod/v4/core").$strict>], "mode">>;
85
+ headers: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodDiscriminatedUnion<[import("zod").ZodObject<{
86
+ action: import("zod").ZodLiteral<"set">;
87
+ name: import("zod").ZodString;
88
+ value: import("zod").ZodString;
89
+ secret: import("zod").ZodOptional<import("zod").ZodBoolean>;
90
+ }, import("zod/v4/core").$strict>, import("zod").ZodObject<{
91
+ action: import("zod").ZodLiteral<"keep">;
92
+ name: import("zod").ZodString;
93
+ }, import("zod/v4/core").$strict>, import("zod").ZodObject<{
94
+ action: import("zod").ZodLiteral<"remove">;
95
+ name: import("zod").ZodString;
96
+ }, import("zod/v4/core").$strict>, import("zod").ZodObject<{
97
+ action: import("zod").ZodLiteral<"suppress">;
98
+ name: import("zod").ZodString;
99
+ }, import("zod/v4/core").$strict>], "action">>>;
100
+ }, import("zod/v4/core").$strict>;
101
+ export type HttpRequest = z.infer<typeof httpRequestSchema>;
102
+ export declare const publicHttpRequestSchema: import("zod").ZodObject<{
103
+ method: import("zod").ZodEnum<{
104
+ POST: "POST";
105
+ PUT: "PUT";
106
+ GET: "GET";
107
+ }>;
108
+ body: import("zod").ZodDiscriminatedUnion<[import("zod").ZodObject<{
109
+ mode: import("zod").ZodLiteral<"envelope">;
110
+ }, import("zod/v4/core").$strict>, import("zod").ZodObject<{
111
+ mode: import("zod").ZodLiteral<"none">;
112
+ }, import("zod/v4/core").$strict>, import("zod").ZodObject<{
113
+ mode: import("zod").ZodLiteral<"json">;
114
+ value: import("zod").ZodOptional<import("zod").ZodJSONSchema>;
115
+ }, import("zod/v4/core").$strict>], "mode">;
116
+ headers: import("zod").ZodArray<import("zod").ZodUnion<readonly [import("zod").ZodObject<{
117
+ name: import("zod").ZodString;
118
+ secret: import("zod").ZodLiteral<false>;
119
+ value: import("zod").ZodString;
120
+ }, import("zod/v4/core").$strict>, import("zod").ZodObject<{
121
+ name: import("zod").ZodString;
122
+ secret: import("zod").ZodLiteral<true>;
123
+ configured: import("zod").ZodLiteral<true>;
124
+ value: import("zod").ZodOptional<import("zod").ZodNever>;
125
+ }, import("zod/v4/core").$strict>, import("zod").ZodObject<{
126
+ name: import("zod").ZodString;
127
+ suppressed: import("zod").ZodLiteral<true>;
128
+ }, import("zod/v4/core").$strict>]>>;
129
+ }, import("zod/v4/core").$strict>;
130
+ export type PublicHttpRequest = z.infer<typeof publicHttpRequestSchema>;
131
+ export declare const storedHttpHeaderSchema: import("zod").ZodUnion<readonly [import("zod").ZodObject<{
132
+ name: import("zod").ZodString;
133
+ secret: import("zod").ZodLiteral<false>;
134
+ value: import("zod").ZodString;
135
+ valueBytes: import("zod").ZodNumber;
136
+ }, import("zod/v4/core").$strict>, import("zod").ZodObject<{
137
+ name: import("zod").ZodString;
138
+ secret: import("zod").ZodLiteral<true>;
139
+ envelope: import("zod").ZodObject<{
140
+ keyId: import("zod").ZodLiteral<"v1">;
141
+ nonce: import("zod").ZodString;
142
+ ciphertext: import("zod").ZodString;
143
+ }, import("zod/v4/core").$strict>;
144
+ valueBytes: import("zod").ZodNumber;
145
+ ownerKind: import("zod").ZodEnum<{
146
+ endpoint: "endpoint";
147
+ schedule: "schedule";
148
+ }>;
149
+ ownerId: import("zod").ZodString;
150
+ }, import("zod/v4/core").$strict>, import("zod").ZodObject<{
151
+ name: import("zod").ZodString;
152
+ suppressed: import("zod").ZodLiteral<true>;
153
+ }, import("zod/v4/core").$strict>]>;
154
+ export declare const storedHttpRequestSchema: import("zod").ZodObject<{
155
+ method: import("zod").ZodEnum<{
156
+ POST: "POST";
157
+ PUT: "PUT";
158
+ GET: "GET";
159
+ }>;
160
+ bodyMode: import("zod").ZodEnum<{
161
+ envelope: "envelope";
162
+ json: "json";
163
+ none: "none";
164
+ }>;
165
+ headers: import("zod").ZodArray<import("zod").ZodUnion<readonly [import("zod").ZodObject<{
166
+ name: import("zod").ZodString;
167
+ secret: import("zod").ZodLiteral<false>;
168
+ value: import("zod").ZodString;
169
+ valueBytes: import("zod").ZodNumber;
170
+ }, import("zod/v4/core").$strict>, import("zod").ZodObject<{
171
+ name: import("zod").ZodString;
172
+ secret: import("zod").ZodLiteral<true>;
173
+ envelope: import("zod").ZodObject<{
174
+ keyId: import("zod").ZodLiteral<"v1">;
175
+ nonce: import("zod").ZodString;
176
+ ciphertext: import("zod").ZodString;
177
+ }, import("zod/v4/core").$strict>;
178
+ valueBytes: import("zod").ZodNumber;
179
+ ownerKind: import("zod").ZodEnum<{
180
+ endpoint: "endpoint";
181
+ schedule: "schedule";
182
+ }>;
183
+ ownerId: import("zod").ZodString;
184
+ }, import("zod/v4/core").$strict>, import("zod").ZodObject<{
185
+ name: import("zod").ZodString;
186
+ suppressed: import("zod").ZodLiteral<true>;
187
+ }, import("zod/v4/core").$strict>]>>;
188
+ encryptedTemplate: import("zod").ZodOptional<import("zod").ZodObject<{
189
+ keyId: import("zod").ZodLiteral<"v1">;
190
+ nonce: import("zod").ZodString;
191
+ ciphertext: import("zod").ZodString;
192
+ }, import("zod/v4/core").$strict>>;
193
+ }, import("zod/v4/core").$strict>;