@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.
- package/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/authority.d.ts +217 -0
- package/dist/authority.js +261 -0
- package/dist/callback-ownership.d.ts +58 -0
- package/dist/callback-ownership.js +71 -0
- package/dist/callback-safety.d.ts +38 -0
- package/dist/callback-safety.js +403 -0
- package/dist/compatibility.d.ts +82 -0
- package/dist/compatibility.js +115 -0
- package/dist/delivery.d.ts +401 -0
- package/dist/delivery.js +311 -0
- package/dist/endpoints.d.ts +305 -0
- package/dist/endpoints.js +179 -0
- package/dist/events.d.ts +62 -0
- package/dist/events.js +38 -0
- package/dist/first-value-journey.d.ts +305 -0
- package/dist/first-value-journey.js +466 -0
- package/dist/forecast.d.ts +443 -0
- package/dist/forecast.js +729 -0
- package/dist/identifiers.d.ts +109 -0
- package/dist/identifiers.js +131 -0
- package/dist/identity.d.ts +196 -0
- package/dist/identity.js +182 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +27 -0
- package/dist/mcp.d.ts +51 -0
- package/dist/mcp.js +56 -0
- package/dist/oauth.d.ts +796 -0
- package/dist/oauth.js +708 -0
- package/dist/operations.d.ts +3985 -0
- package/dist/operations.js +1837 -0
- package/dist/partition.d.ts +771 -0
- package/dist/partition.js +206 -0
- package/dist/policy.d.ts +93 -0
- package/dist/policy.js +180 -0
- package/dist/problems.d.ts +158 -0
- package/dist/problems.js +104 -0
- package/dist/protocol.d.ts +308 -0
- package/dist/protocol.js +515 -0
- package/dist/scheduling-records.d.ts +395 -0
- package/dist/scheduling-records.js +284 -0
- package/dist/scheduling.d.ts +1098 -0
- package/dist/scheduling.js +1116 -0
- package/dist/secrets.d.ts +383 -0
- package/dist/secrets.js +272 -0
- package/dist/signatures.d.ts +704 -0
- package/dist/signatures.js +867 -0
- package/dist/tenancy.d.ts +178 -0
- package/dist/tenancy.js +145 -0
- package/dist/test-sink.d.ts +986 -0
- package/dist/test-sink.js +755 -0
- package/dist/time.d.ts +45 -0
- package/dist/time.js +128 -0
- package/dist/usage.d.ts +121 -0
- package/dist/usage.js +200 -0
- package/dist/webhooks.d.ts +116 -0
- package/dist/webhooks.js +161 -0
- package/dist/workspace.d.ts +92 -0
- package/dist/workspace.js +59 -0
- package/generated/v1/callback-delivery-signature.json +467 -0
- package/generated/v1/callback-endpoint-verification.json +59 -0
- package/generated/v1/callback-receiver.json +241 -0
- package/generated/v1/examples/attempts.list.response.json +24 -0
- package/generated/v1/examples/attempts.read.response.json +20 -0
- package/generated/v1/examples/credentials.create.request.json +5 -0
- package/generated/v1/examples/credentials.create.response.json +10 -0
- package/generated/v1/examples/credentials.list.response.json +17 -0
- package/generated/v1/examples/credentials.revoke.response.json +11 -0
- package/generated/v1/examples/credentials.rotate.request.json +3 -0
- package/generated/v1/examples/credentials.rotate.response.json +22 -0
- package/generated/v1/examples/endpoints.create.request.json +3 -0
- package/generated/v1/examples/endpoints.create.response.json +16 -0
- package/generated/v1/examples/endpoints.disable.response.json +15 -0
- package/generated/v1/examples/endpoints.list.response.json +21 -0
- package/generated/v1/examples/endpoints.read.response.json +14 -0
- package/generated/v1/examples/endpoints.rotateSigningKey.request.json +3 -0
- package/generated/v1/examples/endpoints.rotateSigningKey.response.json +23 -0
- package/generated/v1/examples/endpoints.update.request.json +3 -0
- package/generated/v1/examples/endpoints.update.response.json +13 -0
- package/generated/v1/examples/endpoints.verify.response.json +15 -0
- package/generated/v1/examples/forecast.read.response.json +28 -0
- package/generated/v1/examples/forecast.windows.response.json +21 -0
- package/generated/v1/examples/grants.list.response.json +17 -0
- package/generated/v1/examples/grants.revoke.response.json +16 -0
- package/generated/v1/examples/meta.scaffold.response.json +5 -0
- package/generated/v1/examples/meta.version.response.json +5 -0
- package/generated/v1/examples/occurrences.list.response.json +17 -0
- package/generated/v1/examples/occurrences.read.response.json +15 -0
- package/generated/v1/examples/occurrences.redeliver.response.json +16 -0
- package/generated/v1/examples/projects.create.request.json +3 -0
- package/generated/v1/examples/projects.create.response.json +5 -0
- package/generated/v1/examples/projects.list.response.json +9 -0
- package/generated/v1/examples/schedules.archive.response.json +25 -0
- package/generated/v1/examples/schedules.cancel.response.json +24 -0
- package/generated/v1/examples/schedules.create.request.json +9 -0
- package/generated/v1/examples/schedules.create.response.json +25 -0
- package/generated/v1/examples/schedules.list.response.json +23 -0
- package/generated/v1/examples/schedules.pause.response.json +23 -0
- package/generated/v1/examples/schedules.read.response.json +25 -0
- package/generated/v1/examples/schedules.replace.request.json +8 -0
- package/generated/v1/examples/schedules.replace.response.json +25 -0
- package/generated/v1/examples/schedules.resume.response.json +23 -0
- package/generated/v1/examples/sinkdeliveries.list.response.json +19 -0
- package/generated/v1/examples/sinkdeliveries.read.response.json +16 -0
- package/generated/v1/examples/testsinks.configure.request.json +3 -0
- package/generated/v1/examples/testsinks.configure.response.json +13 -0
- package/generated/v1/examples/testsinks.create.request.json +8 -0
- package/generated/v1/examples/testsinks.create.response.json +13 -0
- package/generated/v1/examples/testsinks.delete.response.json +13 -0
- package/generated/v1/examples/testsinks.list.response.json +20 -0
- package/generated/v1/examples/testsinks.read.response.json +14 -0
- package/generated/v1/examples/usage.read.response.json +20 -0
- package/generated/v1/first-value-journey.json +436 -0
- package/generated/v1/manifest.json +1037 -0
- package/generated/v1/openapi.json +23665 -0
- package/generated/v1/policy-matrix.json +305 -0
- package/generated/v1/schemas/attempts.list.query.json +16 -0
- package/generated/v1/schemas/attempts.list.response.json +175 -0
- package/generated/v1/schemas/attempts.read.response.json +168 -0
- package/generated/v1/schemas/credentials.create.request.json +39 -0
- package/generated/v1/schemas/credentials.create.response.json +88 -0
- package/generated/v1/schemas/credentials.list.query.json +16 -0
- package/generated/v1/schemas/credentials.list.response.json +124 -0
- package/generated/v1/schemas/credentials.revoke.response.json +84 -0
- package/generated/v1/schemas/credentials.rotate.request.json +11 -0
- package/generated/v1/schemas/credentials.rotate.response.json +171 -0
- package/generated/v1/schemas/endpoints.create.request.json +12 -0
- package/generated/v1/schemas/endpoints.create.response.json +115 -0
- package/generated/v1/schemas/endpoints.disable.response-headers.json +11 -0
- package/generated/v1/schemas/endpoints.disable.response.json +100 -0
- package/generated/v1/schemas/endpoints.list.query.json +16 -0
- package/generated/v1/schemas/endpoints.list.response.json +146 -0
- package/generated/v1/schemas/endpoints.read.response-headers.json +11 -0
- package/generated/v1/schemas/endpoints.read.response.json +100 -0
- package/generated/v1/schemas/endpoints.rotateSigningKey.request-headers.json +10 -0
- package/generated/v1/schemas/endpoints.rotateSigningKey.request.json +11 -0
- package/generated/v1/schemas/endpoints.rotateSigningKey.response-headers.json +11 -0
- package/generated/v1/schemas/endpoints.rotateSigningKey.response.json +118 -0
- package/generated/v1/schemas/endpoints.update.request-headers.json +10 -0
- package/generated/v1/schemas/endpoints.update.request.json +12 -0
- package/generated/v1/schemas/endpoints.update.response-headers.json +11 -0
- package/generated/v1/schemas/endpoints.update.response.json +100 -0
- package/generated/v1/schemas/endpoints.verify.request-headers.json +10 -0
- package/generated/v1/schemas/endpoints.verify.response-headers.json +11 -0
- package/generated/v1/schemas/endpoints.verify.response.json +100 -0
- package/generated/v1/schemas/forecast.read.query.json +26 -0
- package/generated/v1/schemas/forecast.read.response.json +100 -0
- package/generated/v1/schemas/forecast.windows.query.json +26 -0
- package/generated/v1/schemas/forecast.windows.response.json +94 -0
- package/generated/v1/schemas/grants.list.response.json +98 -0
- package/generated/v1/schemas/grants.revoke.response.json +95 -0
- package/generated/v1/schemas/meta.scaffold.response.json +36 -0
- package/generated/v1/schemas/meta.version.response.json +19 -0
- package/generated/v1/schemas/occurrences.list.query.json +16 -0
- package/generated/v1/schemas/occurrences.list.response.json +83 -0
- package/generated/v1/schemas/occurrences.read.response.json +76 -0
- package/generated/v1/schemas/occurrences.redeliver.request-headers.json +13 -0
- package/generated/v1/schemas/occurrences.redeliver.response-headers.json +10 -0
- package/generated/v1/schemas/occurrences.redeliver.response.json +168 -0
- package/generated/v1/schemas/projects.create.request.json +12 -0
- package/generated/v1/schemas/projects.create.response.json +25 -0
- package/generated/v1/schemas/projects.list.response.json +35 -0
- package/generated/v1/schemas/schedules.archive.request-headers.json +10 -0
- package/generated/v1/schemas/schedules.archive.response-headers.json +11 -0
- package/generated/v1/schemas/schedules.archive.response.json +189 -0
- package/generated/v1/schemas/schedules.cancel.request-headers.json +10 -0
- package/generated/v1/schemas/schedules.cancel.response-headers.json +11 -0
- package/generated/v1/schemas/schedules.cancel.response.json +189 -0
- package/generated/v1/schemas/schedules.create.request-headers.json +12 -0
- package/generated/v1/schemas/schedules.create.request.json +99 -0
- package/generated/v1/schemas/schedules.create.response-headers.json +15 -0
- package/generated/v1/schemas/schedules.create.response.json +189 -0
- package/generated/v1/schemas/schedules.list.query.json +16 -0
- package/generated/v1/schemas/schedules.list.response.json +170 -0
- package/generated/v1/schemas/schedules.pause.request-headers.json +10 -0
- package/generated/v1/schemas/schedules.pause.response-headers.json +11 -0
- package/generated/v1/schemas/schedules.pause.response.json +189 -0
- package/generated/v1/schemas/schedules.read.response-headers.json +11 -0
- package/generated/v1/schemas/schedules.read.response.json +189 -0
- package/generated/v1/schemas/schedules.replace.request-headers.json +10 -0
- package/generated/v1/schemas/schedules.replace.request.json +99 -0
- package/generated/v1/schemas/schedules.replace.response-headers.json +11 -0
- package/generated/v1/schemas/schedules.replace.response.json +189 -0
- package/generated/v1/schemas/schedules.resume.request-headers.json +10 -0
- package/generated/v1/schemas/schedules.resume.response-headers.json +11 -0
- package/generated/v1/schemas/schedules.resume.response.json +189 -0
- package/generated/v1/schemas/sinkdeliveries.list.query.json +16 -0
- package/generated/v1/schemas/sinkdeliveries.list.response.json +126 -0
- package/generated/v1/schemas/sinkdeliveries.read.response.json +98 -0
- package/generated/v1/schemas/testsinks.configure.request.json +66 -0
- package/generated/v1/schemas/testsinks.configure.response.json +115 -0
- package/generated/v1/schemas/testsinks.create.request.json +76 -0
- package/generated/v1/schemas/testsinks.create.response.json +115 -0
- package/generated/v1/schemas/testsinks.delete.response.json +115 -0
- package/generated/v1/schemas/testsinks.list.query.json +16 -0
- package/generated/v1/schemas/testsinks.list.response.json +161 -0
- package/generated/v1/schemas/testsinks.read.response.json +115 -0
- package/generated/v1/schemas/usage.read.query.json +15 -0
- package/generated/v1/schemas/usage.read.response.json +71 -0
- package/generated/v1/snippets/attempts.list.json +5 -0
- package/generated/v1/snippets/attempts.read.json +5 -0
- package/generated/v1/snippets/credentials.create.json +5 -0
- package/generated/v1/snippets/credentials.list.json +5 -0
- package/generated/v1/snippets/credentials.revoke.json +5 -0
- package/generated/v1/snippets/credentials.rotate.json +5 -0
- package/generated/v1/snippets/endpoints.create.json +5 -0
- package/generated/v1/snippets/endpoints.disable.json +5 -0
- package/generated/v1/snippets/endpoints.list.json +5 -0
- package/generated/v1/snippets/endpoints.read.json +5 -0
- package/generated/v1/snippets/endpoints.rotateSigningKey.json +5 -0
- package/generated/v1/snippets/endpoints.update.json +5 -0
- package/generated/v1/snippets/endpoints.verify.json +5 -0
- package/generated/v1/snippets/forecast.read.json +5 -0
- package/generated/v1/snippets/forecast.windows.json +5 -0
- package/generated/v1/snippets/grants.list.json +5 -0
- package/generated/v1/snippets/grants.revoke.json +5 -0
- package/generated/v1/snippets/meta.scaffold.json +5 -0
- package/generated/v1/snippets/meta.version.json +5 -0
- package/generated/v1/snippets/occurrences.list.json +5 -0
- package/generated/v1/snippets/occurrences.read.json +5 -0
- package/generated/v1/snippets/occurrences.redeliver.json +5 -0
- package/generated/v1/snippets/projects.create.json +5 -0
- package/generated/v1/snippets/projects.list.json +5 -0
- package/generated/v1/snippets/schedules.archive.json +5 -0
- package/generated/v1/snippets/schedules.cancel.json +5 -0
- package/generated/v1/snippets/schedules.create.json +5 -0
- package/generated/v1/snippets/schedules.list.json +5 -0
- package/generated/v1/snippets/schedules.pause.json +5 -0
- package/generated/v1/snippets/schedules.read.json +5 -0
- package/generated/v1/snippets/schedules.replace.json +5 -0
- package/generated/v1/snippets/schedules.resume.json +5 -0
- package/generated/v1/snippets/sinkdeliveries.list.json +5 -0
- package/generated/v1/snippets/sinkdeliveries.read.json +5 -0
- package/generated/v1/snippets/testsinks.configure.json +5 -0
- package/generated/v1/snippets/testsinks.create.json +5 -0
- package/generated/v1/snippets/testsinks.delete.json +5 -0
- package/generated/v1/snippets/testsinks.list.json +5 -0
- package/generated/v1/snippets/testsinks.read.json +5 -0
- package/generated/v1/snippets/usage.read.json +5 -0
- 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[];
|