@intentius/chant 0.64.0 → 0.66.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/behaviour-delta.d.ts +181 -0
  2. package/dist/behaviour-delta.d.ts.map +1 -0
  3. package/dist/behaviour-http.d.ts +106 -0
  4. package/dist/behaviour-http.d.ts.map +1 -0
  5. package/dist/behaviour-overlay.d.ts +61 -0
  6. package/dist/behaviour-overlay.d.ts.map +1 -0
  7. package/dist/behaviour.d.ts +1174 -0
  8. package/dist/behaviour.d.ts.map +1 -0
  9. package/dist/cli/handlers/scenario.d.ts.map +1 -1
  10. package/dist/identity.d.ts +28 -0
  11. package/dist/identity.d.ts.map +1 -1
  12. package/dist/index.d.ts +3 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/lexicon.d.ts +45 -0
  15. package/dist/lexicon.d.ts.map +1 -1
  16. package/dist/lifecycle/scenario-eval.d.ts +23 -5
  17. package/dist/lifecycle/scenario-eval.d.ts.map +1 -1
  18. package/dist/lifecycle/scenario.d.ts +45 -3
  19. package/dist/lifecycle/scenario.d.ts.map +1 -1
  20. package/dist/lifecycle/types.d.ts +17 -0
  21. package/dist/lifecycle/types.d.ts.map +1 -1
  22. package/dist/op/activities/activity-contracts.d.ts +42 -0
  23. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  24. package/dist/op/activities/index.d.ts +2 -0
  25. package/dist/op/activities/index.d.ts.map +1 -1
  26. package/dist/op/activities/predict-behaviour.d.ts +207 -0
  27. package/dist/op/activities/predict-behaviour.d.ts.map +1 -0
  28. package/dist/op/activities/reconcile.d.ts +86 -16
  29. package/dist/op/activities/reconcile.d.ts.map +1 -1
  30. package/dist/op/composites/behaviour-op.d.ts +57 -0
  31. package/dist/op/composites/behaviour-op.d.ts.map +1 -0
  32. package/dist/op/composites/index.d.ts +2 -0
  33. package/dist/op/composites/index.d.ts.map +1 -1
  34. package/dist/op/index.d.ts +2 -2
  35. package/dist/op/index.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/src/behaviour-delta.test.ts +331 -0
  38. package/src/behaviour-delta.ts +564 -0
  39. package/src/behaviour-http.test.ts +456 -0
  40. package/src/behaviour-http.ts +252 -0
  41. package/src/behaviour-overlay.test.ts +149 -0
  42. package/src/behaviour-overlay.ts +76 -0
  43. package/src/behaviour.test.ts +2011 -0
  44. package/src/behaviour.ts +2127 -0
  45. package/src/cli/handlers/scenario.test.ts +108 -0
  46. package/src/cli/handlers/scenario.ts +63 -16
  47. package/src/fold/subset-doc-parity.test.ts +60 -0
  48. package/src/identity.ts +31 -2
  49. package/src/index.ts +3 -0
  50. package/src/lexicon.ts +46 -0
  51. package/src/lifecycle/scenario-cost.test.ts +183 -0
  52. package/src/lifecycle/scenario-eval.ts +133 -6
  53. package/src/lifecycle/scenario.ts +72 -4
  54. package/src/lifecycle/types.ts +17 -0
  55. package/src/lint/rules/op/ops012-activity-contract.test.ts +31 -0
  56. package/src/op/activities/activity-contracts.ts +49 -0
  57. package/src/op/activities/index.ts +24 -0
  58. package/src/op/activities/predict-behaviour.test.ts +255 -0
  59. package/src/op/activities/predict-behaviour.ts +468 -0
  60. package/src/op/activities/reconcile.test.ts +84 -0
  61. package/src/op/activities/reconcile.ts +97 -24
  62. package/src/op/activity-contract-registry.test.ts +3 -0
  63. package/src/op/composites/behaviour-op.test.ts +56 -0
  64. package/src/op/composites/behaviour-op.ts +99 -0
  65. package/src/op/composites/index.ts +2 -0
  66. package/src/op/index.ts +2 -0
@@ -0,0 +1,1174 @@
1
+ /**
2
+ * The behaviour prediction contract (#2356) — what a lexicon's
3
+ * `predictBehaviour()` is allowed to mean.
4
+ *
5
+ * `describeResources()` (./observation.ts) answers whether a declared entity
6
+ * exists. `observeResourcesDeep()` (./deep-observation.ts) answers what its
7
+ * properties currently are. Both report facts a substrate was asked for. This
8
+ * third axis reports something no substrate holds: what the declared estate
9
+ * would *do* at a stated traffic level — cost per hour, how far each entity is
10
+ * from saturation, the error rate to expect, whether it survives a named
11
+ * failure, and a smaller size that would still carry the load.
12
+ *
13
+ * None of that is a measurement, and the type is built so it cannot be read as
14
+ * one.
15
+ *
16
+ * ## A prediction is not a bill
17
+ *
18
+ * The one failure mode that matters here is somebody quoting a modeled figure
19
+ * as money owed. Four things in this module work against that, and it is worth
20
+ * being exact about how much they buy, because an overstated guarantee is how
21
+ * a consumer ends up trusting one:
22
+ *
23
+ * 1. Money appears in exactly one shape, {@link PredictedRate}, and that
24
+ * shape carries a literal `rate: "per-hour"` discriminant. There is no
25
+ * field anywhere for an amount, a period, an account, an invoice or a
26
+ * due date, so an elapsed charge is not expressible *in this module's
27
+ * types*.
28
+ * 2. A figure cannot exist without {@link PredictedBehaviour.at}, the traffic
29
+ * level it was predicted for. A bill is for an hour that happened; this is
30
+ * for an hour the engine was asked to imagine.
31
+ * 3. {@link BehaviourProvenance} is required on every entity, and its
32
+ * {@link BehaviourProvenance.basis} is a closed two-value enum: `modeled`
33
+ * off list prices, or `validated` against a real bill. A figure that will
34
+ * not say which of the two it is cannot be constructed.
35
+ * 4. A refusal is a separate member of the {@link BehaviourResult} union with
36
+ * no figures on it at all, so "the engine is gone" and "the engine says
37
+ * zero" are different objects rather than the same object with zeroes in
38
+ * it. This one is airtight: there is no `entities` key on the refusal arm
39
+ * to be empty and no total to be zero.
40
+ *
41
+ * **What this does not do is make a bill a type error.** A consumer holding a
42
+ * {@link PredictedBehaviour} can write
43
+ * `const { rate, ...rest } = cost; return { amount: rest.perHour * hours }`
44
+ * and TypeScript will not object; the discriminant is a field on an object, and
45
+ * a field can be dropped. behold's own validator rebuilds `cost` as
46
+ * `{ perHour, currency }` and its estate sum carries no discriminant at all, so
47
+ * the marker is stripped by the first consumer *by design*. What the shape
48
+ * genuinely buys is that a bill cannot be constructed **accidentally** — every
49
+ * route from a prediction to something that reads as money owed has a
50
+ * deliberate destructure or cast in it, and shows up in review as one. Treat
51
+ * rules 1 to 3 as a speed bump with a name, and rule 4 as the enforced one.
52
+ *
53
+ * ## The engine sees no credential and writes nothing
54
+ *
55
+ * {@link PredictBehaviourOptions} mirrors `observeResourcesDeep`'s options
56
+ * field for field, plus `traffic`, `edges` and `edgeCoverage`, and then works
57
+ * against credentials on two levels. The type declares every obvious name
58
+ * `?: never`, which catches the deliberate attempt; and
59
+ * {@link screenBehaviourRequest} walks the whole request at runtime, matching
60
+ * credential-shaped **values** as well as credential-shaped keys, which is what
61
+ * catches the accident.
62
+ *
63
+ * `screenBehaviourRequest` is the one entry point — its own doc comment carries
64
+ * the call sequence, and nothing else here restates it. The runtime walk is the
65
+ * load-bearing half, because the type cannot see the two channels that actually
66
+ * carry a secret in practice: `entities[*].props` is `Record<string, unknown>`
67
+ * straight out of the build, and a lexicon surfacing a connection string puts
68
+ * one there without deciding to. See {@link assertNoCredentialInOptions} for
69
+ * exactly what the walk detects and, more importantly, what it does not.
70
+ *
71
+ * `edges` is the one place the options mirror breaks, and the field's own doc
72
+ * says why: the epic's input is a resource graph, a deep read has no use for
73
+ * neighbours, and a prediction is nothing but statements about paths through
74
+ * the estate. It carries `IREdge` (./graph-ir.ts) rather than an edge type of
75
+ * this contract's own, because that is already the shape both the declared
76
+ * path and the live path produce.
77
+ *
78
+ * Nothing in the options is a handle. There is no client, no transport, no
79
+ * apply callback, no writer — only strings, a name list and the entity map the
80
+ * build already produced. An engine handed this cannot reach the account even
81
+ * if it wanted to, which is the structural half of "it never writes".
82
+ *
83
+ * ## The tri-state, and why it is not `UnobservedReason`
84
+ *
85
+ * Behaviour keeps the same three-verdict discipline `./observation.ts`
86
+ * established — PREDICTED, NOT-PREDICTABLE-FOR-THIS-KIND, and NOT-PREDICTED
87
+ * with a named reason — but on a stricter total: every entity the caller asked
88
+ * about lands in `entities` or in `unpredicted`, never in neither. The thin
89
+ * read needs a third position because "the provider says it is not there" is a
90
+ * real answer with no row to sit on. A prediction has no such answer. An entity
91
+ * either got a figure or it did not, and when it did not there is a reason,
92
+ * so an entity in neither map means the lexicon lost track of it.
93
+ *
94
+ * {@link BehaviourUnpredictedReason} derives from `UnobservedReason` rather
95
+ * than restating it, so the four shared verdicts provably keep their spelling
96
+ * and their meaning. It differs in two ways, both deliberate:
97
+ *
98
+ * - `no-credentials` is **excluded**. A behaviour read has no credential to
99
+ * be missing — see the section above — so an enum that could say it would
100
+ * be inviting a lexicon to send an operator hunting for a variable this
101
+ * contract forbids.
102
+ * - Four reasons about the predictor itself are **added** — `no-engine`,
103
+ * `engine-unreachable`, `engine-out-of-credit` and `engine-over-quota` —
104
+ * because the epic wants a missing engine named and `no-binding` is about
105
+ * the environment resolving to no target, a different axis. They are four
106
+ * rather than one because each has a different remedy, and a refusal that
107
+ * names the wrong remedy is worse than a slow one: set a variable, check an
108
+ * address, pay for the account, or wait for a window. The last two arrive
109
+ * from an engine that answered perfectly well (#2359), so folding them into
110
+ * `engine-unreachable` would send somebody to debug a network that is fine.
111
+ *
112
+ * ## Deltas: the invariant is here, the presentation is not
113
+ *
114
+ * The epic wants a declared prediction and a live prediction shown as a delta,
115
+ * and #2358 posts one on a merge request. This module deliberately ships no
116
+ * delta type and no differencing function. What it ships is the one thing a
117
+ * hand-rolled diff silently loses, which is that a figure's context does not
118
+ * survive subtraction: {@link compareFigures} classifies a pair `comparable`,
119
+ * `mixed-basis`, `mixed-level` or `mixed-engine`, and the rule this contract
120
+ * binds its consumers to is that anything but `comparable` must be marked
121
+ * wherever it is shown. A `modeled` figure minus a `validated` one is not a
122
+ * change in the estate; part of that difference is the gap between a price list
123
+ * and an invoice. Nor is a 100 rps figure minus a 1000 rps one, which is why
124
+ * {@link compareProvenance} — which cannot see `at` and never could — is not
125
+ * the function to reach for. How the mark looks is #2358's to define. Whether
126
+ * there is one is not.
127
+ *
128
+ * ## Shape compatibility with the overlay
129
+ *
130
+ * {@link PredictedBehaviour} is the object `chant graph --live --overlay` puts
131
+ * on a node as `attrs._behaviour`.
132
+ *
133
+ * On the graph, `meta._behaviour` takes {@link BehaviourReportMeta} **or the
134
+ * whole {@link BehaviourRefusalReport}** — not a bare {@link BehaviourRefusal}.
135
+ * behold reads that key as `{ engine?, version?, at?, total?, refusal? }` and
136
+ * branches on the presence of `refusal`, so a bare `BehaviourRefusal` has no
137
+ * `refusal` key, falls through to the engine check, and is dropped as
138
+ * "meta.engine missing" — a refusal that renders as nothing at all, which is
139
+ * the one outcome this contract exists to prevent. #2360 implements this
140
+ * sentence, so it says what behold does.
141
+ *
142
+ * behold reads those keys and does arithmetic on the engine's figures; it never
143
+ * produces one of its own. Fields beyond what it reads (`cause` and `source` on
144
+ * a refusal, `edgeCoverage` on the meta) are additive and ignorable.
145
+ *
146
+ * ## The transport is part of the contract (#2373, decided in #2359)
147
+ *
148
+ * The first version of this module named the address chain, said the address
149
+ * may be a URL, a socket path or a command on `PATH`, and stopped. augur
150
+ * (#2357) then declared a private `BehaviourEngine` and its own mapping from
151
+ * what the wire said to which of the three engine-answered refusals to build.
152
+ * With a second implementation to generalise from, the seam is here:
153
+ * {@link BehaviourTransport} is one method, `send(body)`, taking the request
154
+ * as the lexicon rendered it and returning either the engine's answer as text
155
+ * or a {@link BehaviourRefusalReport} ready to return.
156
+ *
157
+ * Two things about that shape are decisions rather than defaults.
158
+ *
159
+ * - **The transport returns text, not a report.** #2373 proposed
160
+ * `send(request) → Promise<BehaviourResult>`. A report cannot be built
161
+ * without the request's `entityNames`, `traffic` and `edgeCoverage`, and
162
+ * what an answer's fields *mean* is the lexicon's wire version, not the
163
+ * contract's. A transport that built reports would have to know both, and
164
+ * every lexicon would need its own. One that carries bytes is one HTTP
165
+ * client and one subprocess spawner for every lexicon there will ever be.
166
+ * - **The failure arm is a finished refusal, not a `{cause, detail}` pair.**
167
+ * The transport is the one party that saw the wire condition and holds the
168
+ * lexicon name and the endpoint, so it is the one party that can name the
169
+ * condition, the address and the variable together. Handing back a cause
170
+ * for the lexicon to translate would put the same three-way switch in
171
+ * every lexicon, and one of them would fold `engine-over-quota` into
172
+ * `engine-unreachable` on a bad afternoon. {@link behaviourWireRefusal}
173
+ * is that switch, written once.
174
+ *
175
+ * What the wire says maps to the causes above as follows, and
176
+ * `./behaviour-http.ts` (the first adapter) and augur's command transport both
177
+ * hold to it: an HTTP 402 is `engine-out-of-credit`; a 429 is
178
+ * `engine-over-quota`; a connection refused, a timeout, a 5xx, a redirect, or
179
+ * an answer that is not JSON is `engine-unreachable`; an unset address is
180
+ * `no-engine` before any transport is built; and an unset or rejected bearer
181
+ * token is `no-engine` too, because its remedy is the same kind — set a
182
+ * variable — and the reason says which. A transport that spawns a child hands
183
+ * it {@link behaviourEngineChildEnvironment} and nothing more, for the reason
184
+ * given on that function.
185
+ *
186
+ * A bearer token on the wire is the transport's, and is not the credential
187
+ * rule 3 is about. "The engine is never handed a credential" is a statement
188
+ * about the *request body* — the graph — which {@link screenBehaviourRequest}
189
+ * still walks first. {@link behaviourTokenFrom} resolves the token an engine
190
+ * authenticates chant with, on a chain parallel to the address chain, and the
191
+ * token goes in a header the engine reads and nowhere else: never in the body,
192
+ * never in a refusal, never in a `detail`.
193
+ */
194
+ import type { UnobservedReason } from "./observation.js";
195
+ import type { IREdge } from "./graph-ir.js";
196
+ import type { DanglingRef } from "./graph-refs.js";
197
+ /**
198
+ * Whether a figure came off a price list or off a bill. Closed, and required on
199
+ * every prediction — this is the distinction that keeps rule 1 enforceable.
200
+ *
201
+ * - `modeled` — computed from published list prices and the engine's own model.
202
+ * The honest default, and the word a badge shows unless told otherwise.
203
+ * - `validated` — reconciled against a real invoice for a comparable estate.
204
+ */
205
+ export type BehaviourBasis = "modeled" | "validated";
206
+ export declare const BEHAVIOUR_BASES: readonly BehaviourBasis[];
207
+ /** True when `value` is a legal {@link BehaviourBasis}. */
208
+ export declare function isBehaviourBasis(value: unknown): value is BehaviourBasis;
209
+ /**
210
+ * Where one entity's numbers came from and how far they can be trusted.
211
+ * Required on every {@link PredictedBehaviour}, and per entity rather than per
212
+ * report, because one estate can be priced by two engines.
213
+ */
214
+ export interface BehaviourProvenance {
215
+ /** The engine that produced the figures, as it names itself (`acme-sim`). */
216
+ engine: string;
217
+ /** That engine's own version string (`1.4.2`). Never inferred. */
218
+ version: string;
219
+ /**
220
+ * The engine's stated tolerance, echoed verbatim (`±15%`). chant does not
221
+ * parse it and does not invent one for an engine that states none — an engine
222
+ * with nothing to say here has no business publishing a figure.
223
+ */
224
+ tolerance: string;
225
+ /** List prices, or a real bill. See {@link BehaviourBasis}. */
226
+ basis: BehaviourBasis;
227
+ }
228
+ /**
229
+ * The traffic level a prediction is for. A string the engine names and chant
230
+ * echoes, never a number chant does arithmetic on: `100 rps, p50`, `peak hour,
231
+ * black friday`, `steady state`. Required, because a figure without the
232
+ * question it answers is the figure most likely to be quoted as a bill.
233
+ */
234
+ export interface BehaviourTrafficLevel {
235
+ traffic: string;
236
+ }
237
+ /**
238
+ * Money, in the only shape this module has for it: a rate for one hypothetical
239
+ * hour at a stated traffic level.
240
+ *
241
+ * The literal `rate: "per-hour"` is load-bearing. It makes the type structurally
242
+ * distinct from any billing record — nothing that models an amount charged
243
+ * carries that field — so a `PredictedRate` cannot be passed where a charge is
244
+ * wanted, and a charge cannot be passed here.
245
+ */
246
+ export interface PredictedRate {
247
+ /** Discriminant. A rate for an imagined hour, never an amount charged for a real one. */
248
+ readonly rate: "per-hour";
249
+ /** The rate itself, in `currency` per hour. */
250
+ perHour: number;
251
+ /** ISO 4217 code, as the engine states it. chant converts nothing. */
252
+ currency: string;
253
+ }
254
+ /** Build a {@link PredictedRate}. Lexicons use this rather than writing the discriminant by hand. */
255
+ export declare function predictedRate(perHour: number, currency: string): PredictedRate;
256
+ /**
257
+ * How much of each axis is still free at the stated traffic level, as a
258
+ * fraction in `0..1`. Both axes are optional and an axis the engine does not
259
+ * model is **absent**, never `0` — zero headroom means saturated, which is the
260
+ * opposite claim.
261
+ */
262
+ export type BehaviourHeadroom = {
263
+ cpu: number;
264
+ latency?: number;
265
+ } | {
266
+ cpu?: number;
267
+ latency: number;
268
+ };
269
+ /** What an entity does when the named failure happens. Closed. */
270
+ export type ResilienceVerdict = "survives" | "degrades" | "fails";
271
+ /** Every legal {@link ResilienceVerdict}, for validation and conformance checks. */
272
+ export declare const RESILIENCE_VERDICTS: readonly ResilienceVerdict[];
273
+ /** True when `value` is a legal {@link ResilienceVerdict}. */
274
+ export declare function isResilienceVerdict(value: unknown): value is ResilienceVerdict;
275
+ /** The verdict under one named failure, and which failure it was tested against. */
276
+ export interface BehaviourResilience {
277
+ /**
278
+ * The failure, named by the engine and echoed verbatim: `one zone lost`,
279
+ * `primary database failover`. A verdict without its failure says nothing.
280
+ */
281
+ failure: string;
282
+ verdict: ResilienceVerdict;
283
+ /** Free text from the engine, when it has more to say than the verdict. */
284
+ note?: string;
285
+ }
286
+ /** A smaller size the engine believes would still carry the stated traffic. */
287
+ export interface BehaviourRightSize {
288
+ /** The size, in the provider's own vocabulary (`t3.small`, `db-f1-micro`). */
289
+ suggestion: string;
290
+ /** Why. A suggestion nobody can evaluate is noise. */
291
+ reason?: string;
292
+ }
293
+ /**
294
+ * One entity's prediction — the object that rides as `attrs._behaviour` on an
295
+ * overlay node.
296
+ *
297
+ * Everything here except `rightSize`, `resilience.note` and either
298
+ * {@link BehaviourHeadroom} axis is required. An entity the engine could not
299
+ * price does not get a thinned-out version of this block; it goes in
300
+ * {@link BehaviourReport.unpredicted} with a reason.
301
+ */
302
+ export interface PredictedBehaviour {
303
+ /** The traffic level every figure below is for. */
304
+ at: BehaviourTrafficLevel;
305
+ /** Cost per hour at `at`. */
306
+ cost: PredictedRate;
307
+ /** Distance from saturation at `at`. A modeled axis is present; an unmodeled one is absent. */
308
+ headroom: BehaviourHeadroom;
309
+ /** Expected fraction of requests failing at `at`, in `0..1`. */
310
+ errorRate: number;
311
+ /** The verdict under a named failure. */
312
+ resilience: BehaviourResilience;
313
+ /** Optional: a smaller size that would still do. */
314
+ rightSize?: BehaviourRightSize;
315
+ /** Which engine said all of this, and on what basis. Required. */
316
+ provenance: BehaviourProvenance;
317
+ }
318
+ /**
319
+ * Why one declared entity got no prediction. Total: a lexicon that cannot
320
+ * predict an entity must pick one of these, and consumers may switch
321
+ * exhaustively.
322
+ *
323
+ * Derived from {@link UnobservedReason} so the four shared verdicts keep their
324
+ * exact spelling and meaning:
325
+ *
326
+ * - `read-failed` — the engine was reached and the prediction errored.
327
+ * - `no-binding` — the environment resolves to no concrete target to predict.
328
+ * - `unsupported-kind` — the engine has no model for this entity type. The
329
+ * entity is perfectly real and may well cost money; this engine cannot say
330
+ * how much, and says so instead of returning zero.
331
+ * - `filtered` — reached but withheld by a caller-requested filter (`owned`).
332
+ *
333
+ * `no-credentials` is excluded on purpose: the engine is never handed one, so
334
+ * it can never be missing one. Two reasons are added for the predictor itself:
335
+ *
336
+ * - `no-engine` — no variable in the chain named an engine, or — for a
337
+ * transport that authenticates — no variable in the token chain named a
338
+ * token the engine accepts. Nothing usable is configured; this is a setup
339
+ * state, not a failure, and the reason says which variable.
340
+ * - `engine-unreachable` — a variable named an engine and it did not answer.
341
+ * - `engine-out-of-credit` — the engine answered, and refused because the
342
+ * account behind it has no balance left (#2359).
343
+ * - `engine-over-quota` — the engine answered, and refused because a rate or
344
+ * volume limit is spent (#2359).
345
+ * - `credential-in-request` — the request itself carried something that must
346
+ * not leave the process, so nothing was sent. The one refusal chant raises
347
+ * about itself rather than about the engine; see
348
+ * {@link screenBehaviourRequest}.
349
+ *
350
+ * The last four are one axis split four ways, because each has a different
351
+ * remedy and a refusal exists to be acted on. `no-engine` wants a variable
352
+ * set. `engine-unreachable` wants the address checked. `engine-out-of-credit`
353
+ * wants somebody to pay, and no amount of waiting fixes it.
354
+ * `engine-over-quota` usually wants nothing but the window to roll over, and
355
+ * telling somebody to top up an account that is not empty sends them to the
356
+ * wrong place — as does folding either into `engine-unreachable`, which points
357
+ * at an address that is answering perfectly well.
358
+ */
359
+ export type BehaviourUnpredictedReason = Exclude<UnobservedReason, "no-credentials"> | "no-engine" | "engine-unreachable" | "engine-out-of-credit" | "engine-over-quota" | "credential-in-request";
360
+ /** Every legal {@link BehaviourUnpredictedReason}, for validation and conformance checks. */
361
+ export declare const BEHAVIOUR_UNPREDICTED_REASONS: readonly BehaviourUnpredictedReason[];
362
+ /** True when `value` is a legal {@link BehaviourUnpredictedReason}. */
363
+ export declare function isBehaviourUnpredictedReason(value: unknown): value is BehaviourUnpredictedReason;
364
+ /** One declared entity that got no prediction, and why. */
365
+ export interface UnpredictedEntity {
366
+ /** Declared entity type, when the lexicon knows it (it usually does — the entity is declared). */
367
+ type?: string;
368
+ /** Total verdict. */
369
+ reason: BehaviourUnpredictedReason;
370
+ /** Human-readable detail: the kind with no model, the call that failed. */
371
+ detail?: string;
372
+ }
373
+ /**
374
+ * Report-level facts about a run that produced figures. Rides as
375
+ * `meta._behaviour` on the overlay graph.
376
+ */
377
+ export interface BehaviourReportMeta {
378
+ /** The engine that answered for the run as a whole. */
379
+ engine: string;
380
+ /** Its version. */
381
+ version: string;
382
+ /** The traffic level the run was asked for, echoed from the request. */
383
+ at: BehaviourTrafficLevel;
384
+ /**
385
+ * An estate total, when the engine states one of its own. Optional, and
386
+ * emphatically not a field for chant or a consumer to fill in by summing —
387
+ * a consumer that sums does its own arithmetic and labels it as such.
388
+ */
389
+ total?: PredictedRate;
390
+ /**
391
+ * The edge coverage the run was given, echoed from the request (#2360).
392
+ *
393
+ * Required, and it is the whole reason `edgeCoverage` is worth stating. Held
394
+ * only on {@link PredictBehaviourOptions} it never reached a reader, so a
395
+ * consumer holding a report could not tell whether a "survives one zone lost"
396
+ * verdict was computed over a complete graph or over one whose builder had no
397
+ * idea what it had missed. Under this contract's own second constraint that
398
+ * makes the verdict a faked number the shape renders invisible — the failure
399
+ * the refusal arm exists to prevent, reappearing one level down.
400
+ *
401
+ * `behaviourReport` copies it from the request, so a lexicon does not restate
402
+ * it and cannot restate it differently.
403
+ */
404
+ edgeCoverage: BehaviourEdgeCoverage;
405
+ }
406
+ /**
407
+ * A run that produced figures. The `behaviour: "v1"` discriminant is a wire
408
+ * version for the same reason `observation: "v1"` is: consumers branch on it.
409
+ */
410
+ export interface BehaviourReport {
411
+ /** Discriminant + wire version. */
412
+ readonly behaviour: "v1";
413
+ /** Which engine ran, at what traffic level. */
414
+ meta: BehaviourReportMeta;
415
+ /** PREDICTED, keyed by chant entity name. */
416
+ entities: Record<string, PredictedBehaviour>;
417
+ /**
418
+ * NOT-PREDICTED, keyed by chant entity name, with a total reason. Together
419
+ * with `entities` this must cover every name the caller asked about.
420
+ */
421
+ unpredicted?: Record<string, UnpredictedEntity>;
422
+ }
423
+ /**
424
+ * Why there is no report at all, and what to do about it.
425
+ *
426
+ * `reason` and `remedy` are the two strings a consumer prints where the legend
427
+ * would go; `cause` and `source` are additive and switchable. The refusal is
428
+ * the lexicon's own text in the house style, naming the variable it wanted —
429
+ * see {@link noBehaviourEngineMessage}.
430
+ */
431
+ export interface BehaviourRefusal {
432
+ /** Total, switchable verdict. */
433
+ cause: BehaviourUnpredictedReason;
434
+ /** The sentence a consumer prints. Names what was missing. */
435
+ reason: string;
436
+ /** How to fix it. Names the variable and how to set it. */
437
+ remedy: string;
438
+ /**
439
+ * Which variable answered, when one did: the address variable for a refusal
440
+ * about the engine, the token variable for a refusal about a rejected
441
+ * token. Absent when no variable answered at all.
442
+ */
443
+ source?: string;
444
+ }
445
+ /**
446
+ * A run that produced nothing, and says why.
447
+ *
448
+ * A separate member of the union rather than a flag on {@link BehaviourReport},
449
+ * so a refusal has no `entities` map to be empty and no `meta.total` to be
450
+ * zero. "The engine is unreachable" and "the engine priced this estate at
451
+ * nothing" are different objects, and no amount of downstream carelessness can
452
+ * turn the first into the second.
453
+ */
454
+ export interface BehaviourRefusalReport {
455
+ /** Discriminant + wire version, shared with {@link BehaviourReport}. */
456
+ readonly behaviour: "v1";
457
+ /** The refusal. Present *instead of* every figure, never alongside one. */
458
+ refusal: BehaviourRefusal;
459
+ }
460
+ /** What `predictBehaviour()` returns: figures, or a named refusal. Never both, never neither. */
461
+ export type BehaviourResult = BehaviourReport | BehaviourRefusalReport;
462
+ /** True when the lexicon refused rather than predicting. */
463
+ export declare function isBehaviourRefusalReport(value: BehaviourResult): value is BehaviourRefusalReport;
464
+ /**
465
+ * True when `value` is either arm of the versioned {@link BehaviourResult}
466
+ * envelope.
467
+ *
468
+ * Both the discriminant AND an arm, because the discriminant alone is not the
469
+ * type. `{ behaviour: "v1" }` carries the version and is neither arm: it fails
470
+ * {@link isBehaviourRefusalReport}, so a consumer's `if (refusal) … else …`
471
+ * narrows it to {@link BehaviourReport}, and `result.entities` is `undefined`
472
+ * at a site TypeScript has been told cannot be.
473
+ */
474
+ export declare function isBehaviourResult(value: unknown): value is BehaviourResult;
475
+ /**
476
+ * Every rule behold's `validateBehaviourBlock` applies, applied here instead —
477
+ * before the block is built rather than after it has travelled.
478
+ *
479
+ * The rule set is deliberately, literally the same set. behold
480
+ * (`behold/src/behaviour.ts:209`) validates each block on arrival and **drops
481
+ * the whole block** with a diagnostic when one fails, so a block chant's types
482
+ * accept and behold's validator rejects renders nothing at all, having looked
483
+ * perfectly legal every step of the way here. Types cannot carry most of these:
484
+ * `tolerance: string` accepts `""`, `errorRate: number` accepts `1.2`, and
485
+ * `cost.perHour: number` accepts `-4`.
486
+ *
487
+ * The mapping, by behold's own numbering, so a future change there has a named
488
+ * place to land here:
489
+ *
490
+ * 2 `at.traffic` non-empty · 3 `cost.perHour` finite · 4 not negative ·
491
+ * 5 `cost.currency` non-empty · 6 `headroom` present · 7/8 each axis a
492
+ * fraction · 9 at least one axis · 10 `errorRate` a fraction ·
493
+ * 11 `resilience.failure` non-empty · 12 verdict in the closed set ·
494
+ * 13 `rightSize` implies a `suggestion` · 14 `provenance.engine` non-empty ·
495
+ * 15 `version` non-empty · 16 `tolerance` non-empty · 17 `basis` in the closed
496
+ * set.
497
+ *
498
+ * Rules 9 and 16 are the two chant is stricter on. Rule 9 is also a type here
499
+ * ({@link BehaviourHeadroom} is a union requiring one axis), so this is the
500
+ * backstop for a JavaScript caller. And on 16, behold accepts any non-empty
501
+ * string while this rejects `n/a`, `none`, `unknown` and friends: the epic asks
502
+ * for the engine's *stated* tolerance, and a word meaning "I have none to
503
+ * state" passes behold's check while defeating its purpose.
504
+ */
505
+ export declare function validateBehaviourBlock(name: string, block: PredictedBehaviour): void;
506
+ /** What a lexicon states about the run itself. Everything else in the meta is copied from the request. */
507
+ export interface BehaviourEngineStamp {
508
+ engine: string;
509
+ version: string;
510
+ /** An estate total, when the engine states one of its own. Never chant's sum. */
511
+ total?: PredictedRate;
512
+ }
513
+ /**
514
+ * Build a {@link BehaviourReport}, and refuse to build an invalid one.
515
+ *
516
+ * Takes the request rather than a hand-assembled meta, and derives `at`,
517
+ * `edgeCoverage` and the asked-for names from it. A lexicon states only what is
518
+ * genuinely its own — which engine answered, at what version, and a total if
519
+ * the engine has one — so the three fields that must agree with the request
520
+ * cannot be restated differently.
521
+ *
522
+ * That is a check and not a proof, and the distinction matters enough to say
523
+ * plainly: {@link BehaviourReport} is a plain interface, {@link isBehaviourResult}
524
+ * accepts a hand-built one, and a lexicon calling this with
525
+ * `entityNames: Object.keys(entities)` self-certifies. What passing the request
526
+ * buys is that the ordinary route is checked and the check names what is wrong;
527
+ * a consumer that needs the guarantee runs its own
528
+ * `validateBehaviourResult(result, askedFor)` on arrival, which is #2358's and
529
+ * #2360's to write.
530
+ *
531
+ * Refusals, all naming what went wrong:
532
+ *
533
+ * - an entity in neither map — the tri-state's whole point, and the one a
534
+ * `continue` in a lexicon's loop produces silently;
535
+ * - an entity in both maps — priced and unpriced at once;
536
+ * - a figure for something nobody asked about;
537
+ * - a block priced at a level the run did not ask for;
538
+ * - an edge-coverage claim that names no gap ({@link validateEdgeCoverage});
539
+ * - any block failing {@link validateBehaviourBlock}.
540
+ */
541
+ export declare function behaviourReport(request: Pick<PredictBehaviourOptions, "entityNames" | "traffic" | "edgeCoverage">, stamp: BehaviourEngineStamp, entities: Record<string, PredictedBehaviour>, unpredicted?: Record<string, UnpredictedEntity>): BehaviourReport;
542
+ /** Build a {@link BehaviourRefusalReport}. */
543
+ export declare function behaviourRefusal(refusal: BehaviourRefusal): BehaviourRefusalReport;
544
+ /**
545
+ * Whether two figures are a delta of like things.
546
+ *
547
+ * - `comparable` — same engine, same version, same tolerance, same basis. The
548
+ * difference between the two numbers is a difference in the estate.
549
+ * - `mixed-basis` — same engine and version, one figure `modeled` off list
550
+ * prices and the other `validated` against a bill. Subtracting these does
551
+ * not measure a change in the estate; part of the difference is the
552
+ * difference between a price list and an invoice.
553
+ * - `mixed-engine` — the engine, its version or its stated tolerance differs.
554
+ * Two models are not one scale, and a delta across them is arithmetic on
555
+ * numbers that were never on the same axis.
556
+ */
557
+ export type ProvenanceComparability = "comparable" | "mixed-basis" | "mixed-engine";
558
+ /**
559
+ * Classify a pair of *provenances*. Almost always the wrong function to call —
560
+ * see {@link compareFigures}, which is the one consumers want.
561
+ *
562
+ * The limit is structural rather than an oversight: `at` lives on
563
+ * {@link PredictedBehaviour} and not on {@link BehaviourProvenance}, so this
564
+ * function cannot see the traffic level and will happily answer `comparable`
565
+ * for a figure at 100 rps and a figure at 1000 rps. Two predictions of the same
566
+ * estate by the same engine at different levels are not a delta of like things;
567
+ * they are answers to different questions. Use this only where the two figures
568
+ * are already known to share a level.
569
+ */
570
+ export declare function compareProvenance(a: BehaviourProvenance, b: BehaviourProvenance): ProvenanceComparability;
571
+ /** One axis on which two figures fail to be a delta of like things. */
572
+ export type FigureMismatch = "mixed-engine" | "mixed-level" | "mixed-basis" | "mixed-currency" | "mixed-failure";
573
+ /**
574
+ * Every mismatch, in the order a consumer should show them — most fundamental
575
+ * first. Order is presentation; membership is the contract.
576
+ */
577
+ export declare const FIGURE_MISMATCHES: readonly FigureMismatch[];
578
+ /**
579
+ * Classify a pair of figures for delta purposes. **This is the one to call.**
580
+ *
581
+ * Returns **every** axis on which the two disagree, not the first. An earlier
582
+ * version returned one label and stopped at the first mismatch, which meant a
583
+ * pair differing in level AND basis reported `mixed-level` and dropped the
584
+ * basis crossing on the floor — a consumer told "different traffic level" would
585
+ * caption it as such and show a modeled-minus-validated difference underneath
586
+ * with nothing said. Precedence is a fine way to decide what to show first and
587
+ * a bad way to decide what to know.
588
+ *
589
+ * The axes:
590
+ *
591
+ * - `mixed-engine` — engine, version or tolerance differs. Two models are not
592
+ * one scale.
593
+ * - `mixed-level` — different `at`. The same question asked of two different
594
+ * worlds.
595
+ * - `mixed-currency` — different `cost.currency`. chant converts nothing, so
596
+ * USD minus EUR is not a number. behold already refuses to *sum* these
597
+ * (`behold/src/behaviour.ts`); permitting them to be *differenced* left this
598
+ * contract laxer than its own consumer.
599
+ * - `mixed-basis` — one figure off a price list, the other off an invoice.
600
+ * - `mixed-failure` — different `resilience.failure`. "Survives one zone lost"
601
+ * against "survives a region lost" are two verdicts about two events, and
602
+ * differencing the costs beside them implies they answer the same question.
603
+ *
604
+ * The rule this contract binds its consumers to, in one sentence: **a delta
605
+ * between two figures whose mismatch set is not empty must be marked as such
606
+ * wherever it is shown, and must never be presented as a plain difference.**
607
+ * How it is marked is #2358's to choose. Whether it must be marked is not.
608
+ */
609
+ export declare function compareFigures(a: PredictedBehaviour, b: PredictedBehaviour): ReadonlySet<FigureMismatch>;
610
+ /** The mismatches, ordered for display. A convenience over {@link compareFigures}. */
611
+ export declare function figureMismatches(a: PredictedBehaviour, b: PredictedBehaviour): FigureMismatch[];
612
+ /** True when two whole figures may be shown as a plain difference, with no mark. */
613
+ export declare function isComparableFigure(a: PredictedBehaviour, b: PredictedBehaviour): boolean;
614
+ /** True when two provenances may be shown as a plain difference. Level-blind — see {@link compareProvenance}. */
615
+ export declare function isComparableProvenance(a: BehaviourProvenance, b: BehaviourProvenance): boolean;
616
+ /**
617
+ * The engine a lexicon predicts against, and the variable that named it.
618
+ *
619
+ * `value` is an address — a URL, a socket path, a command on `PATH`. It is
620
+ * **not** a credential and this contract has no channel for one; see
621
+ * {@link PredictBehaviourOptions}. An engine that demands authentication is out
622
+ * of scope for the plugin surface, and a lexicon that needs one must reach it
623
+ * on its own transport without routing it through here.
624
+ */
625
+ export interface BehaviourEngineEndpoint {
626
+ /** The address the lexicon will predict against. */
627
+ value: string;
628
+ /** The variable it came from, so a refusal or a log line can name it. */
629
+ source: string;
630
+ }
631
+ /**
632
+ * The variables that can name a behaviour engine, most specific first. Exported
633
+ * so a refusal message and a test can agree on the chain without restating it.
634
+ *
635
+ * `lexicon` scopes the first entry, which is what lets an estate priced by two
636
+ * engines point each lexicon at its own without a per-call flag.
637
+ */
638
+ export declare function behaviourEngineVariables(lexicon: string): string[];
639
+ /**
640
+ * Resolve the engine one lexicon predicts against, most specific first. Pure —
641
+ * exported for testing.
642
+ *
643
+ * The chain is lexicon-scoped, then chant-scoped, then bare, for the same
644
+ * reason `gitlabNoteTokenFrom` reads `CHANT_GITLAB_TOKEN` before `GITLAB_TOKEN`
645
+ * (`./op/activities/reconcile.ts`): the narrower name exists so a project that
646
+ * has to differ can differ, and the wider one exists so a project that does not
647
+ * need to sets one variable.
648
+ *
649
+ * Returns `undefined` when nothing in the chain answered. That is `no-engine`,
650
+ * and the caller turns it into {@link noBehaviourEngineMessage} — never into an
651
+ * empty report.
652
+ */
653
+ export declare function behaviourEngineFrom(lexicon: string, env: Record<string, string | undefined>): BehaviourEngineEndpoint | undefined;
654
+ /** What a lexicon says when no variable in the chain names an engine. */
655
+ export declare function noBehaviourEngineMessage(lexicon: string): string;
656
+ /**
657
+ * The bearer token a transport authenticates chant to an engine with, and the
658
+ * variable that named it. The counterpart of {@link BehaviourEngineEndpoint}
659
+ * for the second thing a metered engine needs to know: whose account this is.
660
+ *
661
+ * This is **not** the credential rule 3 forbids. That rule is about the
662
+ * request body, and {@link screenBehaviourRequest} enforces it on every
663
+ * request before any transport is reached. The token here never enters the
664
+ * body; it goes in a header the engine reads, and the transport that sends it
665
+ * is the only code that ever holds it.
666
+ */
667
+ export interface BehaviourEngineToken {
668
+ value: string;
669
+ /** The variable it came from, so a refusal or a log line can name it — never the value. */
670
+ source: string;
671
+ }
672
+ /**
673
+ * The variables that can hold a behaviour engine's token, most specific first.
674
+ * Parallel to {@link behaviourEngineVariables} and deliberately not the same
675
+ * chain: `CHANT_BEHAVIOUR_ENGINE` is an address, and an address is printed in
676
+ * refusals, while a token is never printed anywhere. Reusing one chain for
677
+ * both would put the token in every message that names the address.
678
+ */
679
+ export declare function behaviourTokenVariables(lexicon: string): string[];
680
+ /**
681
+ * Resolve the token one lexicon's transport authenticates with, most specific
682
+ * first. Pure — exported for testing. The same shape as `gitlabNoteTokenFrom`
683
+ * (`./op/activities/reconcile.ts`): a chain, the first non-empty value wins,
684
+ * and the result names the variable so a refusal can say which one it read.
685
+ *
686
+ * Returns `undefined` when nothing in the chain answered. A transport whose
687
+ * engine bills an account turns that into {@link noBehaviourTokenRefusal}
688
+ * before sending anything: a request sent without the token is a request the
689
+ * engine will reject, and the refusal should name the variable rather than
690
+ * quote the engine's 401.
691
+ */
692
+ export declare function behaviourTokenFrom(lexicon: string, env: Record<string, string | undefined>): BehaviourEngineToken | undefined;
693
+ /** What a transport says when the engine bills an account and no variable named a token. */
694
+ export declare function noBehaviourTokenMessage(lexicon: string, endpoint: BehaviourEngineEndpoint): string;
695
+ /**
696
+ * The refusal for a metered engine with no token configured. `no-engine`,
697
+ * because that is the cause whose remedy is "set a variable" — the engine is
698
+ * named, reachable for all anyone knows, and unusable until one more variable
699
+ * is set. No `source`: the token chain is the chain this refusal is about, and
700
+ * nothing in it answered.
701
+ */
702
+ export declare function noBehaviourTokenRefusal(lexicon: string, endpoint: BehaviourEngineEndpoint): BehaviourRefusalReport;
703
+ /**
704
+ * The refusal for an engine that answered and rejected the token it was sent.
705
+ * Also `no-engine`: the address is fine, the account is not the problem, and
706
+ * the one action is to set the named variable to a token the engine accepts.
707
+ * `source` is the token variable, not the address variable, because that is
708
+ * the one a consumer would tell somebody to change.
709
+ *
710
+ * `detail` is whatever the engine said, and it goes through
711
+ * {@link scrubEngineDetail}. The token's own value is never in a message: a
712
+ * transport passes the variable's name here and keeps the value to itself.
713
+ */
714
+ export declare function rejectedBehaviourTokenRefusal(lexicon: string, endpoint: BehaviourEngineEndpoint, token: BehaviourEngineToken, detail: string): BehaviourRefusalReport;
715
+ /** What a lexicon says when a variable named an engine and the engine did not answer. */
716
+ export declare function unreachableBehaviourEngineMessage(lexicon: string, endpoint: BehaviourEngineEndpoint, detail: string): string;
717
+ /**
718
+ * The refusal for an estate with no engine configured. The whole of the
719
+ * `no-engine` path, so no lexicon has to assemble one by hand.
720
+ */
721
+ export declare function noBehaviourEngineRefusal(lexicon: string): BehaviourRefusalReport;
722
+ /** The refusal for a configured engine that did not answer. */
723
+ export declare function unreachableBehaviourEngineRefusal(lexicon: string, endpoint: BehaviourEngineEndpoint, detail: string): BehaviourRefusalReport;
724
+ /**
725
+ * What a lexicon says when the engine answered and refused for want of money
726
+ * (#2359). Distinct from unreachable on purpose: the address is fine, the
727
+ * request arrived, and telling somebody to check their networking wastes the
728
+ * one thing a refusal is for.
729
+ */
730
+ export declare function outOfCreditBehaviourEngineMessage(lexicon: string, endpoint: BehaviourEngineEndpoint, detail: string): string;
731
+ /** What a lexicon says when the engine answered and refused for a spent limit (#2359). */
732
+ export declare function overQuotaBehaviourEngineMessage(lexicon: string, endpoint: BehaviourEngineEndpoint, detail: string): string;
733
+ /** The refusal for an engine that answered and said the account has no balance (#2359). */
734
+ export declare function outOfCreditBehaviourEngineRefusal(lexicon: string, endpoint: BehaviourEngineEndpoint, detail: string): BehaviourRefusalReport;
735
+ /** The refusal for an engine that answered and said a limit is spent (#2359). */
736
+ export declare function overQuotaBehaviourEngineRefusal(lexicon: string, endpoint: BehaviourEngineEndpoint, detail: string): BehaviourRefusalReport;
737
+ /**
738
+ * What a transport brings back: the engine's answer as the text it wrote, or a
739
+ * refusal ready to be returned from `predictBehaviour` as it stands.
740
+ *
741
+ * The answer is text rather than a parsed object because what the text means
742
+ * is the lexicon's wire version (`augur/v1`), and the lexicon is the one that
743
+ * validates it — an answer that fails that validation is
744
+ * {@link unreachableBehaviourEngineRefusal} with a detail naming the field,
745
+ * built by the lexicon, since the transport has nothing to say about it.
746
+ */
747
+ export type BehaviourTransportOutcome = {
748
+ ok: true;
749
+ body: string;
750
+ } | {
751
+ ok: false;
752
+ refusal: BehaviourRefusalReport;
753
+ };
754
+ /**
755
+ * The seam between a lexicon and whatever answers its request (#2373).
756
+ *
757
+ * One method. `body` is the request as the lexicon rendered it — augur's
758
+ * canonical JSON, say — and the transport carries it to the address it was
759
+ * built for and brings back what came out, or a refusal naming why nothing
760
+ * did. A transport is built knowing the lexicon and the endpoint, which is
761
+ * what lets it build the refusal itself; see the module doc for why that is
762
+ * better than returning a cause.
763
+ *
764
+ * Two ship today: `./behaviour-http.ts` dials a URL with a bearer token, and
765
+ * augur's `commandTransport` spawns a command on `PATH` with
766
+ * {@link behaviourEngineChildEnvironment}. A lexicon picks one by the shape of
767
+ * the address and does not otherwise know which it got.
768
+ */
769
+ export interface BehaviourTransport {
770
+ send(body: string): Promise<BehaviourTransportOutcome>;
771
+ }
772
+ /**
773
+ * The three things a wire can say that are the engine's to answer for, and
774
+ * that each want a different refusal. `no-engine` is not here: it is decided
775
+ * before a transport exists (an unset address) or by the transport's own
776
+ * constructor (an unset token), never by the wire.
777
+ */
778
+ export type BehaviourWireCause = "engine-unreachable" | "engine-out-of-credit" | "engine-over-quota";
779
+ /**
780
+ * One wire cause onto the refusal builder that names it. The whole of the
781
+ * mapping every transport applies, so it is written once: a transport that
782
+ * classified the wire correctly and then reached for the wrong builder would
783
+ * send an operator to check a network that is answering.
784
+ */
785
+ export declare function behaviourWireRefusal(lexicon: string, endpoint: BehaviourEngineEndpoint, cause: BehaviourWireCause, detail: string): BehaviourRefusalReport;
786
+ /**
787
+ * The environment a transport hands a child process: `PATH`, and nothing else.
788
+ *
789
+ * Rule 3 says the engine never sees a credential, and
790
+ * {@link screenBehaviourRequest} enforces it on the request. A child that
791
+ * inherits `process.env` walks straight around that: the request is spotless
792
+ * and the child holds `AWS_SECRET_ACCESS_KEY` anyway. So a transport that
793
+ * spawns builds the environment from this and nothing more — `PATH` because
794
+ * the address is resolved against it, and no allowlist beyond that, because
795
+ * every name added is a name a credential could be sitting under. augur's
796
+ * command transport pins this by test (#2372); this is the rule it pins.
797
+ */
798
+ export declare function behaviourEngineChildEnvironment(env?: Record<string, string | undefined>): Record<string, string>;
799
+ /**
800
+ * Render a refusal for a terminal, in red.
801
+ *
802
+ * Red rather than the amber a degradation gets, because a missing engine is not
803
+ * a partial answer: every behaviour-derived figure and colour is gone, and the
804
+ * one thing the reader must not do is assume the numbers are merely late.
805
+ *
806
+ * `color` defaults to the same rule the rest of the CLI uses (`NO_COLOR`, and a
807
+ * TTY on stdout); pass it explicitly where the output is asserted on.
808
+ */
809
+ export declare function renderBehaviourRefusal(refusal: BehaviourRefusal, options?: {
810
+ color?: boolean;
811
+ }): string;
812
+ /**
813
+ * Names this contract refuses to carry, each declared `?: never` on
814
+ * {@link PredictBehaviourOptions} so the deliberate attempt is a compile error.
815
+ *
816
+ * This list is the *narrow* half of the guard and never the whole of it. A
817
+ * denylist of names cannot enforce "the engine never sees a credential" — the
818
+ * same argument that made `?: never` better than omission applies to every name
819
+ * the list omits, and `xApiKey`, `pat` and `Authorization` with a capital A all
820
+ * sailed past an earlier version of this file. What actually enforces the rule
821
+ * is {@link assertNoCredentialInOptions}, which walks values.
822
+ */
823
+ export declare const CREDENTIAL_OPTION_KEYS: readonly string[];
824
+ /**
825
+ * An engine address, safe to interpolate into a refusal that a merge-request
826
+ * comment will carry (#2358).
827
+ *
828
+ * `CHANT_BEHAVIOUR_ENGINE=https://svc:s3cr3t@engine.internal/predict?key=abc` is
829
+ * an ordinary way to point at an authenticated endpoint, and it is the shape
830
+ * this contract's own "resolve auth on your own transport" guidance produces.
831
+ * Printing it verbatim in a refusal publishes it.
832
+ *
833
+ * Three passes, in order:
834
+ *
835
+ * 1. The URL parse blanks `username`/`password`, and drops the query string and
836
+ * the fragment **whole** rather than by known parameter name — `?key=`,
837
+ * `?token=`, `?sig=` and `#access_token=` are all common and the set is not
838
+ * enumerable. The fragment matters on its own account: OAuth's implicit
839
+ * flow puts the access token there.
840
+ * 2. Inline command-line flag values (`--token=…`, `-p …`), for an engine that
841
+ * is a command on `PATH` rather than a URL.
842
+ * 3. {@link redactCredentialMaterial}, for env-held values and the token
843
+ * shapes chant knows.
844
+ *
845
+ * **What survives:** a credential written as a bare path segment
846
+ * (`https://host/predict/<token>/go`) unless it matches a known token shape.
847
+ * Nothing here can tell that segment from a resource id, and an earlier version
848
+ * of this doc claimed pass 3 caught it, which was wrong. If an engine
849
+ * authenticates by path, do not put its address in a variable whose refusal
850
+ * text is published.
851
+ */
852
+ export declare function redactEngineAddress(value: string, env?: Record<string, string | undefined>): string;
853
+ /**
854
+ * Engine-supplied detail, bounded and scrubbed before it reaches a refusal.
855
+ *
856
+ * `detail` on a credit or quota refusal is whatever the engine said, echoed.
857
+ * An engine is a third party: it can be verbose, and it can quote the request
858
+ * back at you, which is how a URL or an id ends up in a public comment. So the
859
+ * text is truncated, anything URL-shaped is reduced to its host, and the result
860
+ * goes through {@link redactCredentialMaterial}.
861
+ */
862
+ export declare function scrubEngineDetail(detail: string, env?: Record<string, string | undefined>): string;
863
+ /**
864
+ * How complete a request's edge list is (#2360).
865
+ *
866
+ * - `complete` — every reference between the named entities is in `edges`.
867
+ * Only claim this when the builder knows it: a declared-path build walking
868
+ * resolved `AttrRef`s does, a live rebuild over a partial catalog does not.
869
+ * - `partial` — some references are known to be missing, and `dangling` or
870
+ * `unresolvedKinds` says which. An engine may still answer, and should
871
+ * discount its own confidence.
872
+ * - `unknown` — the builder cannot say. Treat like `partial` and trust nothing
873
+ * that depends on reachability.
874
+ */
875
+ export type EdgeCoverageVerdict = "complete" | "partial" | "unknown";
876
+ /** Every legal {@link EdgeCoverageVerdict}. */
877
+ export declare const EDGE_COVERAGE_VERDICTS: readonly EdgeCoverageVerdict[];
878
+ /** True when `value` is a legal {@link EdgeCoverageVerdict}. */
879
+ export declare function isEdgeCoverageVerdict(value: unknown): value is EdgeCoverageVerdict;
880
+ /**
881
+ * What a caller knows about the completeness of the graph it is handing over.
882
+ *
883
+ * Note on `complete` with a non-empty {@link dangling}: that is **not** a
884
+ * contradiction and must not be "fixed". A dangling reference points at
885
+ * something outside the named entity set by definition — a cross-account VPC, a
886
+ * resource another team owns — so a builder can have found every edge among the
887
+ * entities it was asked about and still have references leaving the estate.
888
+ * `complete` is a claim about the edges *between the named entities*.
889
+ */
890
+ export interface BehaviourEdgeCoverage {
891
+ verdict: EdgeCoverageVerdict;
892
+ /**
893
+ * References that resolved to no entity in this request — the `dangling` list
894
+ * `reconstructEdges` (./graph-refs.ts) already returns and used to discard.
895
+ *
896
+ * `DanglingRef`, not a flattened string. The record carries `from`, and `from`
897
+ * is the field that says *which* entity's path leaves the estate; flattening
898
+ * to the target value alone leaves an engine knowing that something dangles
899
+ * and not what.
900
+ */
901
+ dangling?: readonly DanglingRef[];
902
+ /**
903
+ * Entity types the builder has no reference rules for, so nothing was looked
904
+ * for. This is the quiet one: a kind with no `RefRule` produces no edges and
905
+ * no complaint.
906
+ */
907
+ unresolvedKinds?: readonly string[];
908
+ /**
909
+ * Containment as traversable edges — populate from
910
+ * `reconstructEdges().containmentEdges` (./graph-refs.ts), **not** from the
911
+ * `containment: ContainmentPair[]` field beside it. The two are the same
912
+ * relationships in two shapes, and only the edge shape belongs here; the
913
+ * field was called `containment` and pointed #2360 straight at the wrong one.
914
+ *
915
+ * Carried separately from {@link PredictBehaviourOptions.edges} because
916
+ * `chant graph` draws containment as a boundary rather than a line, and
917
+ * putting it in `edges` would draw a line from every resource to its VPC. An
918
+ * engine asked whether an estate survives one zone lost needs the membership
919
+ * and `edges` will never have it.
920
+ */
921
+ containmentEdges?: readonly IREdge[];
922
+ }
923
+ /**
924
+ * Refuse an edge-coverage claim that does not say anything.
925
+ *
926
+ * `partial` means "some references are known to be missing", and a `partial`
927
+ * with neither `dangling` nor `unresolvedKinds` names none of them — which is
928
+ * `unknown` wearing a more confident word. Use `unknown` for "I cannot say";
929
+ * `partial` is for "I can say, and here it is".
930
+ */
931
+ /**
932
+ * A detached copy, frozen. The report's account of its own inputs must not
933
+ * change after the report exists, and `readonly` buys nothing at runtime.
934
+ */
935
+ export declare function copyEdgeCoverage(coverage: BehaviourEdgeCoverage): BehaviourEdgeCoverage;
936
+ export declare function validateEdgeCoverage(coverage: BehaviourEdgeCoverage): void;
937
+ /**
938
+ * What core hands a lexicon's `predictBehaviour`.
939
+ *
940
+ * The first seven fields are `observeResourcesDeep`'s options, field for field
941
+ * and doc for doc, so a caller that already drives the deep read drives this
942
+ * with the same object plus `traffic`. That is the point of the mirror: the two
943
+ * reads answer different questions about the same request.
944
+ *
945
+ * The `?: never` block is the enforcement half of "the engine never sees
946
+ * credentials". Declaring the keys rather than omitting them buys a real check:
947
+ * an omitted key is only caught by the excess-property check on an object
948
+ * literal, and slips through a spread or a widened variable, while `never`
949
+ * rejects a `string` from any position and names the field in the error.
950
+ */
951
+ export interface PredictBehaviourOptions {
952
+ environment: string;
953
+ buildOutput: string;
954
+ entityNames: string[];
955
+ entities: Map<string, {
956
+ entityType: string;
957
+ props: Record<string, unknown>;
958
+ }>;
959
+ /** Deployed stack to predict for, in a multi-stack project (see `stacks` in `ChantConfig`). */
960
+ stack?: string;
961
+ /** Region the stack is deployed in, mirroring the deep read (#1267). Omitted keeps the ambient default. */
962
+ region?: string;
963
+ /** Restrict to chant-owned resources (#119). An entity withheld here is `filtered`, not absent. */
964
+ owned?: boolean;
965
+ /**
966
+ * The traffic level to predict at, verbatim: `100 rps, p50`. The one field
967
+ * the deep read has no counterpart for, and the reason a prediction can never
968
+ * be mistaken for an observation — an observation is not *at* anything.
969
+ *
970
+ * chant does not parse it, does not default it, and does not convert it. An
971
+ * engine that cannot understand the level it was handed refuses; it does not
972
+ * substitute one it likes better.
973
+ */
974
+ traffic: string;
975
+ /**
976
+ * The edges between the entities above (#2355) — the half of "a resource
977
+ * graph" that a bag of nodes is not.
978
+ *
979
+ * This is the one field where the mirror of `observeResourcesDeep`'s options
980
+ * deliberately breaks, and it breaks because the two reads want different
981
+ * things. A deep read answers per entity and needs no neighbours: an S3
982
+ * bucket's live property tree is the same tree whether or not a Lambda reads
983
+ * from it. A prediction is the opposite. Headroom, an error rate and a
984
+ * resilience verdict under "one zone lost" are all statements about a path
985
+ * through the estate, and an engine handed nodes alone can only price each
986
+ * box in isolation, which is the arithmetic a consumer could already do for
987
+ * itself.
988
+ *
989
+ * `IREdge` (./graph-ir.ts) rather than an edge type of this contract's own,
990
+ * for one reason that outranks the tidiness of a purpose-built shape: it is
991
+ * already the engine-neutral edge that BOTH paths produce. `collectEdges`
992
+ * builds them from declared `AttrRef`s and lexicon-resolved entity
993
+ * references on the declared path, and `reconstructEdges` (./graph-refs.ts)
994
+ * rebuilds them from observed physical identifiers on the live path, which
995
+ * is the path #2360 assembles this request on. A second edge type here would
996
+ * put a lossy translation hop on each side, and the epic wants the declared
997
+ * prediction and the live prediction shown as a delta — two shapes that have
998
+ * each been through a different translation are the worst possible input to
999
+ * a delta. `DependencyObservation.edges` (./lexicon.ts) already carries
1000
+ * `IREdge` for the same reason.
1001
+ *
1002
+ * `from` and `to` are chant entity names, the keys {@link entityNames} and
1003
+ * {@link entities} use. An edge naming an entity outside `entityNames`
1004
+ * points outside the estate the caller asked about, and an engine may ignore
1005
+ * it.
1006
+ *
1007
+ * Required, and an empty array is a claim rather than a shrug: it says this
1008
+ * estate's entities reference nothing of each other. A caller that has not
1009
+ * computed edges must not pass `[]` and call it a graph, for the same reason
1010
+ * absence and unreadness are separate verdicts everywhere else here — which
1011
+ * is what {@link edgeCoverage} exists to let it say instead.
1012
+ */
1013
+ edges: readonly IREdge[];
1014
+ /**
1015
+ * How complete {@link edges} is, and what is known to be missing from it.
1016
+ *
1017
+ * `edges: []` cannot distinguish "nothing references anything" from "I could
1018
+ * not work out what references what", and both are ordinary outcomes on the
1019
+ * live path. `reconstructEdges` (./graph-refs.ts) returns `dangling` — the
1020
+ * references it resolved to no observed node — and drops them; a kind with no
1021
+ * `RefRule` in the lexicon's catalog contributes no edges at all and says
1022
+ * nothing about it; and containment (a subnet inside a VPC) is deliberately
1023
+ * not an edge, which means zone membership is absent from `edges` by design.
1024
+ * An engine asked "does this survive one zone lost" over a graph with no zone
1025
+ * membership answers confidently and wrongly.
1026
+ *
1027
+ * So the completeness is stated rather than assumed, and stated *now*, before
1028
+ * five consumers are written against a field that silently means "complete".
1029
+ */
1030
+ edgeCoverage: BehaviourEdgeCoverage;
1031
+ /** Not a channel. See {@link CREDENTIAL_OPTION_KEYS}. */
1032
+ token?: never;
1033
+ /** Not a channel. */
1034
+ credential?: never;
1035
+ /** Not a channel. */
1036
+ credentials?: never;
1037
+ /** Not a channel. */
1038
+ secret?: never;
1039
+ /** Not a channel. */
1040
+ secrets?: never;
1041
+ /** Not a channel. */
1042
+ password?: never;
1043
+ /** Not a channel. */
1044
+ apiKey?: never;
1045
+ /** Not a channel. */
1046
+ accessKey?: never;
1047
+ /** Not a channel. */
1048
+ secretKey?: never;
1049
+ /** Not a channel. */
1050
+ sessionToken?: never;
1051
+ /** Not a channel. */
1052
+ auth?: never;
1053
+ /** Not a channel. */
1054
+ authorization?: never;
1055
+ /** Not a channel. */
1056
+ bearer?: never;
1057
+ /** Not a channel. */
1058
+ privateKey?: never;
1059
+ }
1060
+ /**
1061
+ * Refuse a request carrying anything credential-shaped, anywhere in it.
1062
+ *
1063
+ * The `?: never` fields on {@link PredictBehaviourOptions} stop the deliberate
1064
+ * attempt at compile time. This stops the accident, which is the one that
1065
+ * happens: it walks **the whole request** — including `entities[*].props`,
1066
+ * which is `Record<string, unknown>` straight out of the build and which no
1067
+ * type on this contract can see into, and every field of every
1068
+ * {@link import("./graph-ir.js").IREdge}.
1069
+ *
1070
+ * ## What it detects
1071
+ *
1072
+ * 1. **A credential-shaped value**, whatever the key is called: the PEM / JWT /
1073
+ * `Bearer` shapes in `CREDENTIAL_SHAPES`, plus the provider-prefixed tokens
1074
+ * in `CREDENTIAL_TOKEN_SHAPES` (GitHub, GitLab, OpenAI, Stripe, Slack, AWS,
1075
+ * Google, npm).
1076
+ * 2. **A password in userinfo**, with or without a scheme — `new URL()` for the
1077
+ * former and a pattern for `app:hunter2@db.internal:5432/prod`, which is how
1078
+ * a DSN is usually written and which `URL` will not parse at all.
1079
+ * 3. **A credential-shaped key**, but only when its value is a string of at
1080
+ * least {@link MIN_CREDENTIAL_LENGTH} characters that is not an evident
1081
+ * reference, and only when the key is not in the reference family
1082
+ * ({@link REFERENCE_KEY_SUFFIXES}).
1083
+ *
1084
+ * Rules 1 and 2 throw. Rule 3, and a structure deeper than the walk reads,
1085
+ * produce a refusal instead — see {@link screenBehaviourRequest} for why the
1086
+ * blast radii differ.
1087
+ *
1088
+ * **The name layer is a fast path, not a safety net.** After the gating above
1089
+ * it fires on very little, and a credential under a benign name — `dsn`,
1090
+ * `config`, `note` — is caught by rules 1 and 2 or not at all. That is the
1091
+ * intended division of labour and the reason those two exist; do not read
1092
+ * rule 3 as the guarantee.
1093
+ *
1094
+ * ## What it deliberately does not detect
1095
+ *
1096
+ * Rule 1 is **a denylist of known formats, not a proof**. A token from a
1097
+ * provider nobody has added, an internal issuer's format, a bare random string
1098
+ * or a base64 blob passes every rule here, as does a secret split across two
1099
+ * fields or encoded. There is no entropy scoring, on purpose: this walks build
1100
+ * output full of ids, ARNs, hashes and digests, and a heuristic that refuses
1101
+ * those would refuse real projects rather than protect them. So the honest
1102
+ * statement of the guarantee is that a credential a human would recognize on
1103
+ * sight will not reach the engine by accident, and that a novel or opaque
1104
+ * secret still can. A lexicon author putting secret material in `props` is
1105
+ * outside what this contract can catch, and the remedy there is not to.
1106
+ */
1107
+ export type CredentialRule = "value-shape" | "key-name" | "walk-depth";
1108
+ /** One thing the walk objected to, and which rule objected. */
1109
+ export interface CredentialFinding {
1110
+ /** Where in the request, as a readable path (`options.entities.get(db).props.dsn`). */
1111
+ path: string;
1112
+ rule: CredentialRule;
1113
+ /** What was found, in words, for the message. Never the value itself. */
1114
+ what: string;
1115
+ }
1116
+ /**
1117
+ * Every objection the walk has to a request. Pure, and exported so a caller can
1118
+ * decide what to do rather than take this module's word for it.
1119
+ */
1120
+ export declare function findCredentialsInOptions(options: object): CredentialFinding[];
1121
+ /**
1122
+ * Throw when the request carries something that **is** a credential.
1123
+ *
1124
+ * **Not the entry point.** This applies one of the three rules and discards the
1125
+ * other two, so a `key-name` hit and a `walk-depth` hit both pass it silently.
1126
+ * Call {@link screenBehaviourRequest}, which runs this and then acts on what is
1127
+ * left. This stays exported because "did the request contain an actual token"
1128
+ * is a question worth asking on its own, and because the split is what keeps
1129
+ * the blast radii different.
1130
+ *
1131
+ * Only the value-shape rule throws, and throwing is deliberate: a live token in
1132
+ * a request bound for a third party is not a degradation to report, it is a
1133
+ * stop. Throwing is the whole-lexicon failure per `lexicon.ts`, which is the
1134
+ * right blast radius for this and the wrong one for a suspicious field name.
1135
+ */
1136
+ export declare function assertNoCredentialInOptions(options: object): void;
1137
+ /**
1138
+ * Screen a whole request. **This is the entry point, and the only one.**
1139
+ *
1140
+ * This doc comment is the source of truth for the sequence; the module header
1141
+ * and the authoring page both point here rather than restating it. A lexicon's
1142
+ * `predictBehaviour` opens with exactly this and nothing else:
1143
+ *
1144
+ * ```ts
1145
+ * const refusal = screenBehaviourRequest("acme", options);
1146
+ * if (refusal) return refusal;
1147
+ * ```
1148
+ *
1149
+ * Calling {@link assertNoCredentialInOptions} instead applies one rule of three
1150
+ * and drops the rest, which is how a `ghp_…` token nested past the walk's depth
1151
+ * budget, and an `awsSecretAccessKey` in `props`, both sailed through into a
1152
+ * request that was then sent.
1153
+ *
1154
+ * Two outcomes, because two things are being caught and they deserve different
1155
+ * blast radii:
1156
+ *
1157
+ * - a value that IS a credential throws, via
1158
+ * {@link assertNoCredentialInOptions};
1159
+ * - a merely suspicious field name, or a structure too deep to have been read,
1160
+ * returns a {@link BehaviourRefusalReport} with cause
1161
+ * `credential-in-request`. No overlay is drawn and the reason names the
1162
+ * path, which is the contract's own answer to "no faked numbers" applied to
1163
+ * its own guard.
1164
+ *
1165
+ * The split exists because the name arm is a heuristic and the old version
1166
+ * threw on it. One `tags: { author: "…" }` anywhere in an estate killed the
1167
+ * entire overlay with a stack trace, which is the exact failure mode constraint
1168
+ * 2 was written against.
1169
+ *
1170
+ * Returns `undefined` when the request is clean, so a lexicon reads
1171
+ * `const refusal = screenBehaviourRequest(name, options); if (refusal) return refusal;`
1172
+ */
1173
+ export declare function screenBehaviourRequest(lexicon: string, options: object): BehaviourRefusalReport | undefined;
1174
+ //# sourceMappingURL=behaviour.d.ts.map