@greatwait/contracts 0.1.0-alpha.1

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 (241) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -0
  3. package/dist/authority.d.ts +217 -0
  4. package/dist/authority.js +261 -0
  5. package/dist/callback-ownership.d.ts +58 -0
  6. package/dist/callback-ownership.js +71 -0
  7. package/dist/callback-safety.d.ts +38 -0
  8. package/dist/callback-safety.js +403 -0
  9. package/dist/compatibility.d.ts +82 -0
  10. package/dist/compatibility.js +115 -0
  11. package/dist/delivery.d.ts +401 -0
  12. package/dist/delivery.js +311 -0
  13. package/dist/endpoints.d.ts +305 -0
  14. package/dist/endpoints.js +179 -0
  15. package/dist/events.d.ts +62 -0
  16. package/dist/events.js +38 -0
  17. package/dist/first-value-journey.d.ts +305 -0
  18. package/dist/first-value-journey.js +466 -0
  19. package/dist/forecast.d.ts +443 -0
  20. package/dist/forecast.js +729 -0
  21. package/dist/identifiers.d.ts +109 -0
  22. package/dist/identifiers.js +131 -0
  23. package/dist/identity.d.ts +196 -0
  24. package/dist/identity.js +182 -0
  25. package/dist/index.d.ts +27 -0
  26. package/dist/index.js +27 -0
  27. package/dist/mcp.d.ts +51 -0
  28. package/dist/mcp.js +56 -0
  29. package/dist/oauth.d.ts +796 -0
  30. package/dist/oauth.js +708 -0
  31. package/dist/operations.d.ts +3985 -0
  32. package/dist/operations.js +1837 -0
  33. package/dist/partition.d.ts +771 -0
  34. package/dist/partition.js +206 -0
  35. package/dist/policy.d.ts +93 -0
  36. package/dist/policy.js +180 -0
  37. package/dist/problems.d.ts +158 -0
  38. package/dist/problems.js +104 -0
  39. package/dist/protocol.d.ts +308 -0
  40. package/dist/protocol.js +515 -0
  41. package/dist/scheduling-records.d.ts +395 -0
  42. package/dist/scheduling-records.js +284 -0
  43. package/dist/scheduling.d.ts +1098 -0
  44. package/dist/scheduling.js +1116 -0
  45. package/dist/secrets.d.ts +383 -0
  46. package/dist/secrets.js +272 -0
  47. package/dist/signatures.d.ts +704 -0
  48. package/dist/signatures.js +867 -0
  49. package/dist/tenancy.d.ts +178 -0
  50. package/dist/tenancy.js +145 -0
  51. package/dist/test-sink.d.ts +986 -0
  52. package/dist/test-sink.js +755 -0
  53. package/dist/time.d.ts +45 -0
  54. package/dist/time.js +128 -0
  55. package/dist/usage.d.ts +121 -0
  56. package/dist/usage.js +200 -0
  57. package/dist/webhooks.d.ts +116 -0
  58. package/dist/webhooks.js +161 -0
  59. package/dist/workspace.d.ts +92 -0
  60. package/dist/workspace.js +59 -0
  61. package/generated/v1/callback-delivery-signature.json +467 -0
  62. package/generated/v1/callback-endpoint-verification.json +59 -0
  63. package/generated/v1/callback-receiver.json +241 -0
  64. package/generated/v1/examples/attempts.list.response.json +24 -0
  65. package/generated/v1/examples/attempts.read.response.json +20 -0
  66. package/generated/v1/examples/credentials.create.request.json +5 -0
  67. package/generated/v1/examples/credentials.create.response.json +10 -0
  68. package/generated/v1/examples/credentials.list.response.json +17 -0
  69. package/generated/v1/examples/credentials.revoke.response.json +11 -0
  70. package/generated/v1/examples/credentials.rotate.request.json +3 -0
  71. package/generated/v1/examples/credentials.rotate.response.json +22 -0
  72. package/generated/v1/examples/endpoints.create.request.json +3 -0
  73. package/generated/v1/examples/endpoints.create.response.json +16 -0
  74. package/generated/v1/examples/endpoints.disable.response.json +15 -0
  75. package/generated/v1/examples/endpoints.list.response.json +21 -0
  76. package/generated/v1/examples/endpoints.read.response.json +14 -0
  77. package/generated/v1/examples/endpoints.rotateSigningKey.request.json +3 -0
  78. package/generated/v1/examples/endpoints.rotateSigningKey.response.json +23 -0
  79. package/generated/v1/examples/endpoints.update.request.json +3 -0
  80. package/generated/v1/examples/endpoints.update.response.json +13 -0
  81. package/generated/v1/examples/endpoints.verify.response.json +15 -0
  82. package/generated/v1/examples/forecast.read.response.json +28 -0
  83. package/generated/v1/examples/forecast.windows.response.json +21 -0
  84. package/generated/v1/examples/grants.list.response.json +17 -0
  85. package/generated/v1/examples/grants.revoke.response.json +16 -0
  86. package/generated/v1/examples/meta.scaffold.response.json +5 -0
  87. package/generated/v1/examples/meta.version.response.json +5 -0
  88. package/generated/v1/examples/occurrences.list.response.json +17 -0
  89. package/generated/v1/examples/occurrences.read.response.json +15 -0
  90. package/generated/v1/examples/occurrences.redeliver.response.json +16 -0
  91. package/generated/v1/examples/projects.create.request.json +3 -0
  92. package/generated/v1/examples/projects.create.response.json +5 -0
  93. package/generated/v1/examples/projects.list.response.json +9 -0
  94. package/generated/v1/examples/schedules.archive.response.json +25 -0
  95. package/generated/v1/examples/schedules.cancel.response.json +24 -0
  96. package/generated/v1/examples/schedules.create.request.json +9 -0
  97. package/generated/v1/examples/schedules.create.response.json +25 -0
  98. package/generated/v1/examples/schedules.list.response.json +23 -0
  99. package/generated/v1/examples/schedules.pause.response.json +23 -0
  100. package/generated/v1/examples/schedules.read.response.json +25 -0
  101. package/generated/v1/examples/schedules.replace.request.json +8 -0
  102. package/generated/v1/examples/schedules.replace.response.json +25 -0
  103. package/generated/v1/examples/schedules.resume.response.json +23 -0
  104. package/generated/v1/examples/sinkdeliveries.list.response.json +19 -0
  105. package/generated/v1/examples/sinkdeliveries.read.response.json +16 -0
  106. package/generated/v1/examples/testsinks.configure.request.json +3 -0
  107. package/generated/v1/examples/testsinks.configure.response.json +13 -0
  108. package/generated/v1/examples/testsinks.create.request.json +8 -0
  109. package/generated/v1/examples/testsinks.create.response.json +13 -0
  110. package/generated/v1/examples/testsinks.delete.response.json +13 -0
  111. package/generated/v1/examples/testsinks.list.response.json +20 -0
  112. package/generated/v1/examples/testsinks.read.response.json +14 -0
  113. package/generated/v1/examples/usage.read.response.json +20 -0
  114. package/generated/v1/first-value-journey.json +436 -0
  115. package/generated/v1/manifest.json +1037 -0
  116. package/generated/v1/openapi.json +23665 -0
  117. package/generated/v1/policy-matrix.json +305 -0
  118. package/generated/v1/schemas/attempts.list.query.json +16 -0
  119. package/generated/v1/schemas/attempts.list.response.json +175 -0
  120. package/generated/v1/schemas/attempts.read.response.json +168 -0
  121. package/generated/v1/schemas/credentials.create.request.json +39 -0
  122. package/generated/v1/schemas/credentials.create.response.json +88 -0
  123. package/generated/v1/schemas/credentials.list.query.json +16 -0
  124. package/generated/v1/schemas/credentials.list.response.json +124 -0
  125. package/generated/v1/schemas/credentials.revoke.response.json +84 -0
  126. package/generated/v1/schemas/credentials.rotate.request.json +11 -0
  127. package/generated/v1/schemas/credentials.rotate.response.json +171 -0
  128. package/generated/v1/schemas/endpoints.create.request.json +12 -0
  129. package/generated/v1/schemas/endpoints.create.response.json +115 -0
  130. package/generated/v1/schemas/endpoints.disable.response-headers.json +11 -0
  131. package/generated/v1/schemas/endpoints.disable.response.json +100 -0
  132. package/generated/v1/schemas/endpoints.list.query.json +16 -0
  133. package/generated/v1/schemas/endpoints.list.response.json +146 -0
  134. package/generated/v1/schemas/endpoints.read.response-headers.json +11 -0
  135. package/generated/v1/schemas/endpoints.read.response.json +100 -0
  136. package/generated/v1/schemas/endpoints.rotateSigningKey.request-headers.json +10 -0
  137. package/generated/v1/schemas/endpoints.rotateSigningKey.request.json +11 -0
  138. package/generated/v1/schemas/endpoints.rotateSigningKey.response-headers.json +11 -0
  139. package/generated/v1/schemas/endpoints.rotateSigningKey.response.json +118 -0
  140. package/generated/v1/schemas/endpoints.update.request-headers.json +10 -0
  141. package/generated/v1/schemas/endpoints.update.request.json +12 -0
  142. package/generated/v1/schemas/endpoints.update.response-headers.json +11 -0
  143. package/generated/v1/schemas/endpoints.update.response.json +100 -0
  144. package/generated/v1/schemas/endpoints.verify.request-headers.json +10 -0
  145. package/generated/v1/schemas/endpoints.verify.response-headers.json +11 -0
  146. package/generated/v1/schemas/endpoints.verify.response.json +100 -0
  147. package/generated/v1/schemas/forecast.read.query.json +26 -0
  148. package/generated/v1/schemas/forecast.read.response.json +100 -0
  149. package/generated/v1/schemas/forecast.windows.query.json +26 -0
  150. package/generated/v1/schemas/forecast.windows.response.json +94 -0
  151. package/generated/v1/schemas/grants.list.response.json +98 -0
  152. package/generated/v1/schemas/grants.revoke.response.json +95 -0
  153. package/generated/v1/schemas/meta.scaffold.response.json +36 -0
  154. package/generated/v1/schemas/meta.version.response.json +19 -0
  155. package/generated/v1/schemas/occurrences.list.query.json +16 -0
  156. package/generated/v1/schemas/occurrences.list.response.json +83 -0
  157. package/generated/v1/schemas/occurrences.read.response.json +76 -0
  158. package/generated/v1/schemas/occurrences.redeliver.request-headers.json +13 -0
  159. package/generated/v1/schemas/occurrences.redeliver.response-headers.json +10 -0
  160. package/generated/v1/schemas/occurrences.redeliver.response.json +168 -0
  161. package/generated/v1/schemas/projects.create.request.json +12 -0
  162. package/generated/v1/schemas/projects.create.response.json +25 -0
  163. package/generated/v1/schemas/projects.list.response.json +35 -0
  164. package/generated/v1/schemas/schedules.archive.request-headers.json +10 -0
  165. package/generated/v1/schemas/schedules.archive.response-headers.json +11 -0
  166. package/generated/v1/schemas/schedules.archive.response.json +189 -0
  167. package/generated/v1/schemas/schedules.cancel.request-headers.json +10 -0
  168. package/generated/v1/schemas/schedules.cancel.response-headers.json +11 -0
  169. package/generated/v1/schemas/schedules.cancel.response.json +189 -0
  170. package/generated/v1/schemas/schedules.create.request-headers.json +12 -0
  171. package/generated/v1/schemas/schedules.create.request.json +99 -0
  172. package/generated/v1/schemas/schedules.create.response-headers.json +15 -0
  173. package/generated/v1/schemas/schedules.create.response.json +189 -0
  174. package/generated/v1/schemas/schedules.list.query.json +16 -0
  175. package/generated/v1/schemas/schedules.list.response.json +170 -0
  176. package/generated/v1/schemas/schedules.pause.request-headers.json +10 -0
  177. package/generated/v1/schemas/schedules.pause.response-headers.json +11 -0
  178. package/generated/v1/schemas/schedules.pause.response.json +189 -0
  179. package/generated/v1/schemas/schedules.read.response-headers.json +11 -0
  180. package/generated/v1/schemas/schedules.read.response.json +189 -0
  181. package/generated/v1/schemas/schedules.replace.request-headers.json +10 -0
  182. package/generated/v1/schemas/schedules.replace.request.json +99 -0
  183. package/generated/v1/schemas/schedules.replace.response-headers.json +11 -0
  184. package/generated/v1/schemas/schedules.replace.response.json +189 -0
  185. package/generated/v1/schemas/schedules.resume.request-headers.json +10 -0
  186. package/generated/v1/schemas/schedules.resume.response-headers.json +11 -0
  187. package/generated/v1/schemas/schedules.resume.response.json +189 -0
  188. package/generated/v1/schemas/sinkdeliveries.list.query.json +16 -0
  189. package/generated/v1/schemas/sinkdeliveries.list.response.json +126 -0
  190. package/generated/v1/schemas/sinkdeliveries.read.response.json +98 -0
  191. package/generated/v1/schemas/testsinks.configure.request.json +66 -0
  192. package/generated/v1/schemas/testsinks.configure.response.json +115 -0
  193. package/generated/v1/schemas/testsinks.create.request.json +76 -0
  194. package/generated/v1/schemas/testsinks.create.response.json +115 -0
  195. package/generated/v1/schemas/testsinks.delete.response.json +115 -0
  196. package/generated/v1/schemas/testsinks.list.query.json +16 -0
  197. package/generated/v1/schemas/testsinks.list.response.json +161 -0
  198. package/generated/v1/schemas/testsinks.read.response.json +115 -0
  199. package/generated/v1/schemas/usage.read.query.json +15 -0
  200. package/generated/v1/schemas/usage.read.response.json +71 -0
  201. package/generated/v1/snippets/attempts.list.json +5 -0
  202. package/generated/v1/snippets/attempts.read.json +5 -0
  203. package/generated/v1/snippets/credentials.create.json +5 -0
  204. package/generated/v1/snippets/credentials.list.json +5 -0
  205. package/generated/v1/snippets/credentials.revoke.json +5 -0
  206. package/generated/v1/snippets/credentials.rotate.json +5 -0
  207. package/generated/v1/snippets/endpoints.create.json +5 -0
  208. package/generated/v1/snippets/endpoints.disable.json +5 -0
  209. package/generated/v1/snippets/endpoints.list.json +5 -0
  210. package/generated/v1/snippets/endpoints.read.json +5 -0
  211. package/generated/v1/snippets/endpoints.rotateSigningKey.json +5 -0
  212. package/generated/v1/snippets/endpoints.update.json +5 -0
  213. package/generated/v1/snippets/endpoints.verify.json +5 -0
  214. package/generated/v1/snippets/forecast.read.json +5 -0
  215. package/generated/v1/snippets/forecast.windows.json +5 -0
  216. package/generated/v1/snippets/grants.list.json +5 -0
  217. package/generated/v1/snippets/grants.revoke.json +5 -0
  218. package/generated/v1/snippets/meta.scaffold.json +5 -0
  219. package/generated/v1/snippets/meta.version.json +5 -0
  220. package/generated/v1/snippets/occurrences.list.json +5 -0
  221. package/generated/v1/snippets/occurrences.read.json +5 -0
  222. package/generated/v1/snippets/occurrences.redeliver.json +5 -0
  223. package/generated/v1/snippets/projects.create.json +5 -0
  224. package/generated/v1/snippets/projects.list.json +5 -0
  225. package/generated/v1/snippets/schedules.archive.json +5 -0
  226. package/generated/v1/snippets/schedules.cancel.json +5 -0
  227. package/generated/v1/snippets/schedules.create.json +5 -0
  228. package/generated/v1/snippets/schedules.list.json +5 -0
  229. package/generated/v1/snippets/schedules.pause.json +5 -0
  230. package/generated/v1/snippets/schedules.read.json +5 -0
  231. package/generated/v1/snippets/schedules.replace.json +5 -0
  232. package/generated/v1/snippets/schedules.resume.json +5 -0
  233. package/generated/v1/snippets/sinkdeliveries.list.json +5 -0
  234. package/generated/v1/snippets/sinkdeliveries.read.json +5 -0
  235. package/generated/v1/snippets/testsinks.configure.json +5 -0
  236. package/generated/v1/snippets/testsinks.create.json +5 -0
  237. package/generated/v1/snippets/testsinks.delete.json +5 -0
  238. package/generated/v1/snippets/testsinks.list.json +5 -0
  239. package/generated/v1/snippets/testsinks.read.json +5 -0
  240. package/generated/v1/snippets/usage.read.json +5 -0
  241. package/package.json +153 -0
@@ -0,0 +1,443 @@
1
+ import { z } from "zod";
2
+ import type { FieldError } from "./problems.js";
3
+ import { type Freshness } from "./protocol.js";
4
+ import type { ForecastPartitionBucket } from "./partition.js";
5
+ /**
6
+ * What is about to happen, in bounded time buckets.
7
+ *
8
+ * A capacity planner asks one question — how much work is coming, and when —
9
+ * and the answer has to be bounded in three directions at once: the horizon it
10
+ * covers, the number of buckets it returns, and the work the service does to
11
+ * compute it. Architecture section 13 lists the forecast horizon among the cost
12
+ * controls for exactly that reason.
13
+ *
14
+ * **`owner` appears nowhere.** The copied service answered
15
+ * `/v1/analytics/:owner/forecast` and trusted the segment; here the authority
16
+ * tuple comes from the verified principal as it does everywhere else, and a
17
+ * legacy `owner` is a W6 migration input rather than authorization.
18
+ */
19
+ /**
20
+ * The bounds a forecast is served under.
21
+ *
22
+ * The horizon, the finest bucket and the slack below `now` are **the copied
23
+ * `ForecastCache`'s numbers, kept rather than re-derived**: 25 hours, 60-second
24
+ * buckets, 120 seconds of slack. That file says why — the horizon must exceed
25
+ * the largest served window plus the maximum staleness so a sliding window
26
+ * stays covered between refreshes, and 25 hours comfortably covers a 24-hour
27
+ * poll. Re-choosing them here would have discarded a tuning that arrived
28
+ * proven.
29
+ *
30
+ * `maxBuckets` is the one bound that is ours, and it is arithmetic rather than
31
+ * taste: the longest window over the finest bucket is 1,500 buckets, so stating
32
+ * it makes the response size bounded by a calculation instead of by hope.
33
+ */
34
+ export declare const FORECAST_BOUNDS: {
35
+ readonly defaultBucketSeconds: number;
36
+ /** Window served when a caller names neither `from` nor `to`. */
37
+ readonly defaultWindowSeconds: number;
38
+ readonly maxBucketSeconds: number;
39
+ /** `maxWindowSeconds / minBucketSeconds`, so the size is arithmetic. */
40
+ readonly maxBuckets: 1500;
41
+ /**
42
+ * Distinct `(minute, latencyClass, partitionKey)` groups one partition may
43
+ * aggregate before it reports the answer as short.
44
+ */
45
+ readonly maxGroupsPerPartition: 100000;
46
+ /**
47
+ * How stale an answer may be before it is refused rather than served.
48
+ *
49
+ * Twice the partition's own refresh ceiling, so a healthy answer never trips
50
+ * it and a broken one always does. It exists because the consumer this shape
51
+ * was measured against reads `count` and not `freshness`: a live-but-badly-
52
+ * stale 200 is indistinguishable, to it, from a fresh one, and the whole
53
+ * argument for refusing rather than under-reporting collapses if staleness is
54
+ * the one degradation that gets through as a number.
55
+ */
56
+ readonly maxLagSeconds: 600;
57
+ /**
58
+ * Instants one partition may project into a single snapshot before it reports
59
+ * the answer as short.
60
+ *
61
+ * Published rather than kept internal so a refusal is classifiable: a shadow
62
+ * comparison that sees a refusal needs to be able to tell "we are near a
63
+ * stated bound" from "something is broken".
64
+ */
65
+ readonly maxProjectedOccurrencesPerPartition: 20000;
66
+ /** Recurring series one partition may project from in a single snapshot. */
67
+ readonly maxRecurringSeriesPerPartition: 20000;
68
+ /**
69
+ * How far ahead of `now` a forecast may look.
70
+ *
71
+ * The copied cache's `DEFAULT_FORECAST_CACHE_HORIZON_SECONDS`. A window
72
+ * reaching past it cannot be served from a snapshot taken at `now`, and the
73
+ * legacy service's seven-day ceiling bounded the *query* with no snapshot
74
+ * behind it at all.
75
+ */
76
+ readonly maxWindowSeconds: number;
77
+ readonly minBucketSeconds: 60;
78
+ /**
79
+ * Longest a partition lets its snapshot stand when nothing has been written.
80
+ *
81
+ * Published because it is the honest answer to "how out of date can this be",
82
+ * and a caller who has to discover that by experiment will discover it during
83
+ * an incident. `freshness.lagSeconds` reports the actual age on every answer;
84
+ * this is the bound it lives inside.
85
+ */
86
+ readonly refreshCeilingSeconds: 300;
87
+ /**
88
+ * Shortest gap between refreshes once something *has* been written.
89
+ *
90
+ * So a caller who has just created or cancelled a schedule knows when it will
91
+ * appear: within this, not instantly. A forecast is a projection, and a
92
+ * projection that rebuilt on every poll would be the cost this whole design
93
+ * exists to avoid.
94
+ */
95
+ readonly refreshFloorSeconds: 30;
96
+ /**
97
+ * How far below `now` a window may begin.
98
+ *
99
+ * The copied cache's `DEFAULT_FORECAST_CACHE_SLACK_SECONDS`, kept for its
100
+ * reason: it absorbs the skew between a caller's clock and ours, so the
101
+ * ordinary request — "from now, for six hours" — does not fail intermittently
102
+ * because the caller's `now` is a second behind. Further back than that is a
103
+ * question about the past, which a forecast does not answer.
104
+ */
105
+ readonly slackSeconds: 120;
106
+ /**
107
+ * Customer keys named individually in one bucket before the rest are summed.
108
+ *
109
+ * The copied route's `FORECAST_TOP_TENANTS`. A customer's key space is
110
+ * unbounded by design, so a breakdown naming every key would let one caller
111
+ * decide the size of our response.
112
+ */
113
+ readonly topPartitionKeys: 10;
114
+ };
115
+ /**
116
+ * Whether the answer carries a per-customer-key breakdown.
117
+ *
118
+ * Off by default. The breakdown is the axis whose size a caller controls — a
119
+ * bucket's payload grows with how many keys fired in it — and most callers want
120
+ * the total and the latency split, which every answer carries.
121
+ */
122
+ export declare const FORECAST_BREAKDOWNS: readonly ["none", "partitionKey"];
123
+ export declare const forecastBreakdownSchema: z.ZodEnum<{
124
+ none: "none";
125
+ partitionKey: "partitionKey";
126
+ }>;
127
+ export type ForecastBreakdown = z.infer<typeof forecastBreakdownSchema>;
128
+ export declare const forecastRequestSchema: z.ZodObject<{
129
+ breakdown: z.ZodDefault<z.ZodEnum<{
130
+ none: "none";
131
+ partitionKey: "partitionKey";
132
+ }>>;
133
+ bucketSeconds: z.ZodDefault<z.ZodNumber>;
134
+ from: z.ZodOptional<z.ZodString>;
135
+ to: z.ZodOptional<z.ZodString>;
136
+ }, z.core.$strip>;
137
+ export type ForecastRequest = z.infer<typeof forecastRequestSchema>;
138
+ export type ForecastRequestParse = {
139
+ readonly errors: readonly FieldError[];
140
+ readonly ok: false;
141
+ } | {
142
+ readonly ok: true;
143
+ readonly request: ForecastRequest;
144
+ };
145
+ /**
146
+ * Reads a forecast request off a URL.
147
+ *
148
+ * Values arrive as strings, so the coercion lives here rather than in every
149
+ * adapter — the reason `parsePageRequest` exists. A repeated parameter is a
150
+ * refusal rather than a last-one-wins guess: two different windows in one
151
+ * request is a caller bug, and answering one of them silently is how it stays a
152
+ * bug.
153
+ */
154
+ export declare function parseForecastRequest(query: URLSearchParams): ForecastRequestParse;
155
+ /** The window a forecast actually covers, once defaults have been applied. */
156
+ export interface ForecastWindow {
157
+ readonly bucketSeconds: number;
158
+ readonly fromMs: number;
159
+ readonly toMs: number;
160
+ }
161
+ export type ForecastWindowRefusal = "range-empty" | "range-too-long" | "window-in-the-past" | "window-past-horizon";
162
+ export type ForecastWindowResolution = {
163
+ readonly ok: false;
164
+ readonly refusal: ForecastWindowRefusal;
165
+ } | {
166
+ readonly ok: true;
167
+ readonly window: ForecastWindow;
168
+ };
169
+ /**
170
+ * Turns a parsed request into the window that will be served, or says why not.
171
+ *
172
+ * Separate from the schema because every refusal here is a relation between two
173
+ * fields rather than a property of one, and a refinement that fails on the
174
+ * object cannot say which field to point at.
175
+ *
176
+ * **The horizon is measured from `now`, not from `from`.** A caller asking for
177
+ * a window that starts tomorrow and ends the day after is asking for something
178
+ * no snapshot taken now can cover; answering it from a snapshot that stops at
179
+ * 25 hours would be a confident wrong answer rather than a refusal.
180
+ */
181
+ export declare function resolveForecastWindow(request: ForecastRequest, nowMs: number): ForecastWindowResolution;
182
+ /**
183
+ * The epoch-aligned bucket an instant belongs to, in milliseconds.
184
+ *
185
+ * Aligned to the epoch rather than to `from`, so two callers asking for
186
+ * different windows at different moments agree about where a bucket begins —
187
+ * which is what makes a shadow comparison between two services a comparison of
188
+ * counts rather than of alignment. The consequence is that the first bucket of
189
+ * a window usually begins before the window does, and that is published.
190
+ */
191
+ export declare function forecastBucketStartMs(instantMs: number, bucketSeconds: number): number;
192
+ /**
193
+ * One count per latency class, with every class present.
194
+ *
195
+ * Written out rather than derived from `LATENCY_CLASSES`, so the published JSON
196
+ * Schema names both keys. `forecast.test.ts` holds the two lists equal, so a
197
+ * third latency class fails a test here rather than silently vanishing from
198
+ * every forecast.
199
+ */
200
+ export declare const forecastLatencyCountsSchema: z.ZodObject<{
201
+ bulk: z.ZodNumber;
202
+ realtime: z.ZodNumber;
203
+ }, z.core.$strip>;
204
+ export type ForecastLatencyCounts = z.infer<typeof forecastLatencyCountsSchema>;
205
+ export declare const forecastBucketSchema: z.ZodObject<{
206
+ byLatencyClass: z.ZodObject<{
207
+ bulk: z.ZodNumber;
208
+ realtime: z.ZodNumber;
209
+ }, z.core.$strip>;
210
+ byPartitionKey: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
211
+ count: z.ZodNumber;
212
+ nextFireCount: z.ZodNumber;
213
+ otherPartitionKeyCount: z.ZodOptional<z.ZodNumber>;
214
+ start: z.ZodString;
215
+ }, z.core.$strip>;
216
+ export type ForecastBucket = z.infer<typeof forecastBucketSchema>;
217
+ export declare const forecastSchema: z.ZodObject<{
218
+ buckets: z.ZodArray<z.ZodObject<{
219
+ byLatencyClass: z.ZodObject<{
220
+ bulk: z.ZodNumber;
221
+ realtime: z.ZodNumber;
222
+ }, z.core.$strip>;
223
+ byPartitionKey: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
224
+ count: z.ZodNumber;
225
+ nextFireCount: z.ZodNumber;
226
+ otherPartitionKeyCount: z.ZodOptional<z.ZodNumber>;
227
+ start: z.ZodString;
228
+ }, z.core.$strip>>;
229
+ freshness: z.ZodObject<{
230
+ asOf: z.ZodString;
231
+ kind: z.ZodLiteral<"projected">;
232
+ lagSeconds: z.ZodNumber;
233
+ }, z.core.$strip>;
234
+ window: z.ZodObject<{
235
+ bucketSeconds: z.ZodNumber;
236
+ from: z.ZodString;
237
+ to: z.ZodString;
238
+ }, z.core.$strip>;
239
+ }, z.core.$strip>;
240
+ export type Forecast = z.infer<typeof forecastSchema>;
241
+ /** One bucket, summed across every shard, before it is rendered. */
242
+ export interface ForecastMergedBucket {
243
+ readonly bucketStartMs: number;
244
+ readonly byLatencyClass: Record<string, number>;
245
+ readonly byPartitionKey: Record<string, number>;
246
+ count: number;
247
+ nextFireCount: number;
248
+ unkeyedCount: number;
249
+ }
250
+ /**
251
+ * Sum one bucket across every shard, ascending by start.
252
+ *
253
+ * Both maps are null-prototype, and that is not defensive style: a customer's
254
+ * `partitionKey` may contain underscores, so `__proto__` is a legal key, and
255
+ * assigning it on an ordinary object runs the prototype setter and silently
256
+ * drops the count. The copied route took the same care for the same reason, and
257
+ * this product's charset makes the key easier to reach rather than harder.
258
+ */
259
+ export declare function mergeForecastBuckets(buckets: readonly ForecastPartitionBucket[]): readonly ForecastMergedBucket[];
260
+ /**
261
+ * The busiest customer keys in one bucket, and everything else as one number.
262
+ *
263
+ * Ordered by count and then by key, so the tenth and eleventh of a tie resolve
264
+ * the same way on every read rather than by whatever order the shards answered
265
+ * in — a breakdown that reshuffles between polls looks like a change to
266
+ * anything comparing two readings.
267
+ *
268
+ * Occurrences from schedules that chose **no** key are counted in the
269
+ * remainder rather than published under a reserved name, because `default` and
270
+ * `other` are both legal keys and a reserved one would absorb a real
271
+ * customer's count. The published invariant is what survives:
272
+ * `count = sum(named) + other`.
273
+ */
274
+ export declare function topPartitionKeys(bucket: ForecastMergedBucket): {
275
+ readonly byPartitionKey: Record<string, number>;
276
+ readonly otherPartitionKeyCount: number;
277
+ };
278
+ /** Why a gathered forecast is refused rather than served. */
279
+ export type ForecastAnswerRefusal = "book-too-large" | "partition-unavailable" | "too-stale";
280
+ /**
281
+ * One partition's contribution, reduced to what the decision depends on.
282
+ *
283
+ * `asOfMs` is absent when the partition has no snapshot at all, which is the
284
+ * ordinary case for a shard a project has never written to.
285
+ */
286
+ export type ForecastShardAnswer = {
287
+ readonly refused: true;
288
+ } | {
289
+ readonly asOfMs?: number;
290
+ readonly truncated: boolean;
291
+ };
292
+ export type ForecastAnswerDecision = {
293
+ readonly ok: false;
294
+ readonly refusal: ForecastAnswerRefusal;
295
+ } | {
296
+ readonly freshness: Extract<Freshness, {
297
+ kind: "projected";
298
+ }>;
299
+ readonly ok: true;
300
+ };
301
+ /**
302
+ * Whether a gathered forecast may be published, and how old it is.
303
+ *
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.
318
+ *
319
+ * `asOfMs` is the **oldest** across the shards, for the reason
320
+ * `projectionCoverage` takes the oldest checkpoint: one busy shard must not be
321
+ * allowed to vouch for three quiet ones.
322
+ */
323
+ export declare function decideForecastAnswer(answers: readonly ForecastShardAnswer[], nowMs: number): ForecastAnswerDecision;
324
+ /**
325
+ * The version of the transitional window shape.
326
+ *
327
+ * Dated like the callback envelope, for the same reason: a consumer pins a
328
+ * literal, and a literal that moves is a change the consumer can be told about
329
+ * rather than one it discovers.
330
+ */
331
+ export declare const FORECAST_WINDOW_VERSION = "2026-08-09";
332
+ export declare const forecastWindowBucketSchema: z.ZodObject<{
333
+ byTenant: z.ZodRecord<z.ZodString, z.ZodNumber>;
334
+ count: z.ZodNumber;
335
+ nextFireCount: z.ZodNumber;
336
+ otherCount: z.ZodNumber;
337
+ start: z.ZodString;
338
+ }, z.core.$strip>;
339
+ export type ForecastWindowBucket = z.infer<typeof forecastWindowBucketSchema>;
340
+ export declare const forecastWindowsSchema: z.ZodObject<{
341
+ bucketSeconds: z.ZodNumber;
342
+ buckets: z.ZodArray<z.ZodObject<{
343
+ byTenant: z.ZodRecord<z.ZodString, z.ZodNumber>;
344
+ count: z.ZodNumber;
345
+ nextFireCount: z.ZodNumber;
346
+ otherCount: z.ZodNumber;
347
+ start: z.ZodString;
348
+ }, z.core.$strip>>;
349
+ freshness: z.ZodDiscriminatedUnion<[z.ZodObject<{
350
+ kind: z.ZodLiteral<"authoritative">;
351
+ }, z.core.$strip>, z.ZodObject<{
352
+ asOf: z.ZodString;
353
+ kind: z.ZodLiteral<"projected">;
354
+ lagSeconds: z.ZodNumber;
355
+ }, z.core.$strip>], "kind">;
356
+ version: z.ZodLiteral<"2026-08-09">;
357
+ }, z.core.$strip>;
358
+ export type ForecastWindows = z.infer<typeof forecastWindowsSchema>;
359
+ /**
360
+ * What the migrating consumer may rely on, published as data.
361
+ *
362
+ * Prose describing a wire contract drifts from the code that produces it —
363
+ * three times during W3-08, and the third published a rule that would have had
364
+ * a customer take a payment twice. So the rules live here, beside the schema,
365
+ * and `scripts/contract-projection.test.ts` holds `docs/contracts/forecast.md`
366
+ * against them.
367
+ *
368
+ * Every entry was read out of the consumer rather than assumed. It parses
369
+ * `start` with `Date.parse` and drops any bucket that does not parse; it reads
370
+ * `count` raw and sums it across buckets to size capacity; it carries
371
+ * `byTenant` through untouched; and it reads no other field — not `otherCount`,
372
+ * not the legacy `partial`, not `unavailablePartitions`. That last fact is what
373
+ * makes "a refusal, never a short answer" a rule rather than an implementation
374
+ * note: a degraded answer this consumer cannot see reads as a quiet book, and a
375
+ * quiet book makes it scale *down*.
376
+ */
377
+ export declare const FORECAST_COMPATIBILITY: {
378
+ /**
379
+ * What a consumer must tolerate or do for itself. Not checkable in a
380
+ * response: each is a permission this producer has, or an obligation the
381
+ * consumer owes, rather than something the producer owes.
382
+ */
383
+ readonly consumerNotes: readonly ["a bucket's start may precede the requested from, because buckets are aligned to the epoch rather than to the window; filter on start + bucketSeconds > now rather than on start > now, or the bucket carrying the next few minutes of work is discarded on almost every poll", "a window this service cannot answer completely is a refusal rather than a short list of buckets, so a non-2xx is no reading rather than an empty book", "the legacy partial and unavailablePartitions fields are gone, because there is no partial answer left for them to describe", "freshness is published on every answer and an answer staler than FORECAST_BOUNDS.maxLagSeconds is refused rather than served, so a 200 is always inside that bound"];
384
+ /** The only fields the migrating consumer reads today. */
385
+ readonly consumedFields: readonly ["byTenant", "count", "start"];
386
+ /** What a caller must change, beyond the base URL. */
387
+ readonly migrationDelta: readonly ["authenticate with a scoped Greatwait credential holding forecast:read, not the legacy shared bearer token", "drop the :owner path segment, because authority comes from the credential and a request can no longer name a project", "send bucketSeconds=600 rather than bucket=10m, because durations on this surface are whole seconds as numbers", "expect a recurring series to contribute every instant it fires in the window, where the legacy answer counted each schedule once at its next fire time — sum nextFireCount to reproduce the legacy total, and retune any threshold calibrated against it before cutting over", "assert version equals the published constant and treat a mismatch as staleness, because nothing else tells a consumer the shape moved"];
388
+ /**
389
+ * What this check cannot see, stated so nobody mistakes a pass for a proof.
390
+ *
391
+ * A response body carries counts, not the reasoning that produced them, so an
392
+ * implementation that quietly stopped expanding recurring series would
393
+ * satisfy every rule below: its counts would simply be smaller. That property
394
+ * is pinned by running the un-expanded implementation through
395
+ * `partition-forecast.test.ts` and the worker acceptance tests and watching
396
+ * them fail, which is the only place it can be pinned.
397
+ */
398
+ readonly notCheckedHere: readonly ["that recurring series were expanded at all, because a smaller count is still an arithmetically consistent one", "that the counts match the partition's own state, because this check never sees it"];
399
+ /**
400
+ * What this producer owes, in the order `forecastWindowViolations` checks
401
+ * them. Every rule has a case that trips it, which `forecast.test.ts` asserts
402
+ * by construction rather than by inspection.
403
+ */
404
+ readonly rules: readonly ["start is an RFC 3339 UTC timestamp with millisecond precision and a literal Z, because a consumer parsing it with Date.parse reads a bucket it cannot parse as no work at all", "count is the total across every customer key and latency class in the bucket, never the top-ten subtotal", "count equals the sum of byTenant plus otherCount", "nextFireCount is at most count, because a bucket cannot hold more first firings than firings", "buckets ascend strictly by start", "a bucket with no occurrences is omitted rather than published with a count of zero"];
405
+ readonly version: "2026-08-09";
406
+ };
407
+ /**
408
+ * What the canonical forecast owes, checked by `forecastViolations`.
409
+ *
410
+ * Published separately from `FORECAST_COMPATIBILITY` because the adapter is
411
+ * dated and scheduled for deletion once W6-02 has cut over, and the shape that
412
+ * outlives it must not be the one with no gate. Two of these are the same
413
+ * obligations in the canonical spelling; the latency-class arithmetic has no
414
+ * counterpart in the adapter, which carries no class breakdown.
415
+ */
416
+ export declare const FORECAST_RULES: readonly ["start is an RFC 3339 UTC timestamp with millisecond precision and a literal Z", "count equals byLatencyClass.bulk plus byLatencyClass.realtime", "byPartitionKey and otherPartitionKeyCount are present together or not at all, so a caller never has to guess whether a breakdown was requested", "when the breakdown is present, count equals the sum of byPartitionKey plus otherPartitionKeyCount", "nextFireCount is at most count", "buckets ascend strictly by start", "a bucket with no occurrences is omitted rather than published with a count of zero"];
417
+ /**
418
+ * Which published rules a canonical forecast breaks.
419
+ *
420
+ * The same shape of check as `forecastWindowViolations`, over `FORECAST_RULES`,
421
+ * so the shape that outlives the migration adapter is gated too. See that
422
+ * function for why both read the body loosely rather than through their schema.
423
+ */
424
+ export declare function forecastViolations(body: unknown): readonly string[];
425
+ /**
426
+ * Which published rules a window response breaks.
427
+ *
428
+ * Deliberately a predicate over a **response body** rather than over the code
429
+ * that produced it, so it can be pointed at a knowingly wrong implementation
430
+ * and watched failing. W3-08 shipped a conformance suite that compared HTTP
431
+ * status codes, and a receiver carrying the exact defect the story existed to
432
+ * fix passed twenty of twenty published cases; reading a suite cannot show what
433
+ * it fails to distinguish.
434
+ *
435
+ * It reads the body loosely rather than through `forecastWindowsSchema`,
436
+ * because the schema refuses several of these outright and a check that could
437
+ * only say "did not parse" would name no rule — which is the same defect in a
438
+ * different coat.
439
+ *
440
+ * Returns the broken rules in `FORECAST_COMPATIBILITY.rules` order. An empty
441
+ * array is the only pass.
442
+ */
443
+ export declare function forecastWindowViolations(body: unknown): readonly string[];