@mark1russell7/service 0.5.14 → 0.5.15

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/README.md CHANGED
@@ -15,9 +15,13 @@ This release has the declaration, its export, the law, the health runtime, the l
15
15
  - **Keyed operations.** A keyed operation has the window that the server keeps its key for. `mayReissue` answers from the declaration of an operation. An operation without a declaration does not go again.
16
16
  - **The law.** `evaluate` and `verdictOf` decide one rule of the timer-ordering law on one binding. A rule is data: linear inequalities over `symbol@hop`, with a margin. A verdict is `holds`, `violated`, `unbounded` (it holds only because a timer is absent) or `depends` (nothing is bound).
17
17
  - **The linter.** `lint(description, scenario, rules)` applies the service rules (`SERVICE_RULES`) to a declaration. The declaration binds the symbols of the service: the polls of its checks, the windows of its policies, its drain. The scenario binds the symbols of the platform and of each dependency. A `depends` finding names the symbols that have no bound.
18
+ - **The claims of a scenario.** A default is true only for a version: the keep-alive of Node is 5 s, but 65 s on main. Thus a value of the scenario can be a claim that names its epoch: `{ epoch: "node@22", value }`. Each finding then gives the epoch of each value that its rule reads (`epochs`).
19
+ - **A measured value.** A claim can give a distribution by its statistics: `{ epoch, distribution: { p50, p99 } }`. The law binds the p99. A distribution without a p99 binds nothing, thus each rule that reads it depends.
18
20
  - **The scopes of a rule.** A rule about a client applies one time for each dependency. A rule about a consumer, for example O20, applies one time for each consumer and each worker.
19
21
  - **The loops of a progress check.** A check of the step `progress` can name the loops that mark it, with its stall time: `marks: { loops, stall }`. Each loop is a check or a worker of the service.
20
22
  - **The stall time.** `lint()` compares the stall time with the longest gap between two marks of the slowest loop (`PROGRESS_RULE`). A loop marks at the end of each turn, also a turn that fails. The gap of a check is `interval × (1 + jitter) + timeout`. The gap of a worker is its sleep or its longest backoff, the longer one, plus `unit_processing_time`. At 300 s ± 20 % and a timeout of 10 s, a poll gives 370 s.
23
+ - **The marks of the timers.** A harness that runs a service k times faster scales its timers. Each timer has a mark: `scalable`, `fixed`, `stepped` (only multiples of a step) or `floored` (a peer sets a floor).
24
+ - **The compression of a service.** `compressionOf(description, scenario, rules, k, marks)` gives each rule and scope whose verdict the compression changes. It also names the symbols that have no mark. `PLATFORM_MARKS` marks the timers of Kubernetes. For example, the propagation of the endpoints is `fixed`, thus a compression of the hold of O04 can break O04.
21
25
  - **A name that is not a loop.** A name that is not a check or a worker gets a `depends` finding. Without `marks`, a check gets no finding of this rule.
22
26
  - **Keyed limiters.** `svc.limiter(name, spec)` declares a rate limit for each key, as data. `keyedLimiter` makes its runtime on a store ([The keyed limiter](#the-keyed-limiter)).
23
27
  - **The hold.** The cap of the `fail readiness` phase binds `readiness_hold@process`, and a service without that phase holds for zero. The hold keeps the listener open after the signal, as a preStop sleep does. Thus O04 can compare the sum of the two with the propagation of the endpoints.
package/build/index.d.ts CHANGED
@@ -84,6 +84,55 @@ export declare const evaluate: (rule: Rule, bind: Binding) => Evaluated[];
84
84
  * applies, the arithmetic cannot decide, and the verdict is "depends".
85
85
  */
86
86
  export declare const verdictOf: (rule: Rule, bind: Binding, evaluated: readonly Evaluated[]) => Verdict;
87
+ /**
88
+ * A mark tells how a timer moves when a compression scales time by k (E10). A "scalable" timer
89
+ * becomes k times its value. A "fixed" timer keeps its value. A "stepped" timer takes only
90
+ * multiples of its step, thus it rounds up. A "floored" timer has a floor that a peer sets, for
91
+ * example the minimum retransmission timeout of the kernel. The step and the floor are in seconds.
92
+ */
93
+ type TimerMark = {
94
+ readonly kind: "scalable";
95
+ } | {
96
+ readonly kind: "fixed";
97
+ } | {
98
+ readonly kind: "stepped";
99
+ readonly step: number;
100
+ } | {
101
+ readonly kind: "floored";
102
+ readonly floor: number;
103
+ readonly by: string;
104
+ };
105
+ /** The marks map `symbol@hop` to a mark. A bare symbol marks the symbol at each hop. */
106
+ type TimerMarks = Readonly<Record<string, TimerMark | undefined>>;
107
+ /** A compressed binding, and the symbols that have no mark. */
108
+ interface Compressed {
109
+ readonly bind: Binding;
110
+ /** These symbols have no mark. They keep their values, thus the compression proves nothing about them. */
111
+ readonly unmarked: readonly string[];
112
+ }
113
+ /** A change is a rule whose verdict a compression changes. */
114
+ interface Change {
115
+ readonly rule: string;
116
+ readonly before: Verdict;
117
+ readonly after: Verdict;
118
+ }
119
+ /** The result of a compression: the rules whose verdict it changes, and the symbols of the rules that have no mark. */
120
+ interface Compression {
121
+ readonly changed: readonly Change[];
122
+ readonly unmarked: readonly string[];
123
+ }
124
+ /**
125
+ * This function scales each timer of a binding by k, by its mark (E10). An absent timer ("inf" or
126
+ * "off") stays absent. A chain scales each value. The margins of the binding and the constants of
127
+ * the rules do not scale: they are tolerances, not timers. A symbol with no mark keeps its value.
128
+ */
129
+ export declare const compress: (bind: Binding, k: number, marks: TimerMarks) => Compressed;
130
+ /**
131
+ * This function tells which rules a compression by k changes. A compression keeps each ordering
132
+ * when no verdict changes. A change in either direction counts: a violation that the compression
133
+ * hides is as wrong as one that it makes. The unmarked symbols are the symbols that the rules read.
134
+ */
135
+ export declare const compression: (rules: readonly Rule[], bind: Binding, k: number, marks: TimerMarks) => Compression;
87
136
  //#endregion
88
137
  //#region src/lint.d.ts
89
138
  /**
@@ -96,10 +145,31 @@ export declare const verdictOf: (rule: Rule, bind: Binding, evaluated: readonly
96
145
  * A rule about a client applies one time for each dependency. A rule about a consumer applies one
97
146
  * time for each consumer and each worker. A rule about a limiter applies one time for each limiter.
98
147
  */
148
+ /** A statistic of a measured distribution. The law binds p99 (`LAW_STATISTIC`). */
149
+ type Statistic = "p50" | "p90" | "p99" | "p999" | "max";
150
+ /** The law binds this statistic of a distribution. */
151
+ export declare const LAW_STATISTIC: Statistic;
152
+ /**
153
+ * A claim is a value of the scenario that names its epoch: the component and the version that give
154
+ * it, for example "node@22" (E2). A default is true only for a version: the keep-alive of Node is
155
+ * 5 s, but 65 s on main. A claim gives a set value, or a measured distribution by its statistics.
156
+ */
157
+ type Claim = {
158
+ readonly epoch: string;
159
+ readonly value: Extent;
160
+ } | {
161
+ readonly epoch: string;
162
+ readonly distribution: Readonly<Partial<Record<Statistic, Quantity>>>;
163
+ };
164
+ /**
165
+ * A value of the scenario is an amount, or a claim. An amount names no epoch, thus the finding of
166
+ * each rule that reads it gives null for its epoch.
167
+ */
168
+ type ScenarioValue = Extent | Claim;
99
169
  /** A scenario gives the values of the platform and of each dependency, by the `symbol@hop` of the law. */
100
170
  interface Scenario {
101
171
  /** These are the values of the platform: prestop_sleep@pod, termination_grace@pod, probe_period@pod, propagation_bound@cluster. */
102
- readonly platform: Readonly<Record<string, Extent>>;
172
+ readonly platform: Readonly<Record<string, ScenarioValue>>;
103
173
  /**
104
174
  * These are the probe windows of the kubelet, period × failure threshold. They add to the
105
175
  * windows of the declaration. A probe reads the cached verdict, thus detection is the latch,
@@ -110,16 +180,18 @@ interface Scenario {
110
180
  readonly liveness: Quantity;
111
181
  };
112
182
  /** These are the values of each dependency, by its name: failover_time@dependency, server_idle_close@server. */
113
- readonly dependencies?: Readonly<Record<string, Readonly<Record<string, Extent>>>> | undefined;
183
+ readonly dependencies?: Readonly<Record<string, Readonly<Record<string, ScenarioValue>>>> | undefined;
114
184
  /** These are the values of the source of each consumer and worker, by its name and by the `symbol@hop` of the law: lease_duration@broker. */
115
- readonly transports?: Readonly<Record<string, Readonly<Record<string, Extent>>>> | undefined;
185
+ readonly transports?: Readonly<Record<string, Readonly<Record<string, ScenarioValue>>>> | undefined;
116
186
  /**
117
187
  * These are the values of the callers of each limiter, by its name and by the `symbol@hop` of the
118
188
  * law: client_retry_window@caller, retry_delay@caller. A symbol without a hop binds at `caller`.
119
189
  */
120
- readonly limiters?: Readonly<Record<string, Readonly<Record<string, Extent>>>> | undefined;
190
+ readonly limiters?: Readonly<Record<string, Readonly<Record<string, ScenarioValue>>>> | undefined;
121
191
  }
122
- /** A finding is the verdict of one rule for one scope: the service, or the client of a dependency. */
192
+ /** The epochs of a scope map each symbol that the scenario binds to the epoch of its value, or null when the value names none. */
193
+ type Epochs = Readonly<Record<string, string | null>>;
194
+ /** A finding is the verdict of one rule for one scope: the service, or the client of a dependency. When a value of the scenario names an epoch, the finding gives the epoch of each value that the rule reads. */
123
195
  interface Finding {
124
196
  readonly rule: string;
125
197
  readonly scope: string;
@@ -129,6 +201,8 @@ interface Finding {
129
201
  readonly evaluated: readonly Evaluated[];
130
202
  /** These are the symbols that have no bound. A "depends" verdict waits for them. */
131
203
  readonly unbound: readonly string[];
204
+ /** These are the epochs of the symbols of the rule that the scenario binds (E2). Thus a finding is true for these versions. */
205
+ readonly epochs?: Epochs | undefined;
132
206
  }
133
207
  /** These are the identifiers of the rules that each declaration gets. */
134
208
  export declare const SERVICE_RULES: readonly string[];
@@ -150,11 +224,31 @@ export declare const LIMITER_RULES: readonly Rule[];
150
224
  export declare const serviceBindings: (svc: ServiceDescription, scenario: Scenario) => {
151
225
  readonly bind: Record<string, Bound>;
152
226
  readonly from: Record<string, string>;
227
+ readonly epochs: Epochs;
153
228
  };
154
229
  /** This identifier is the rule of the findings about a field that the declaration does not give. */
155
230
  export declare const DECLARATION_RULE = "declaration";
156
231
  /** This identifier is the rule of the findings about the stall time of a progress check (`Check.marks`). */
157
232
  export declare const PROGRESS_RULE = "progress";
233
+ /**
234
+ * These are the marks of the timers of the platform (E10). Kubernetes takes the probe period, the
235
+ * preStop sleep and the termination grace in whole seconds, thus each is stepped by 1 s. The
236
+ * cluster sets the propagation of the endpoints, thus a compression of the service cannot scale it.
237
+ */
238
+ export declare const PLATFORM_MARKS: TimerMarks;
239
+ /** A change of a compression of a service: a rule and a scope whose verdict the compression changes. */
240
+ interface ScopedChange extends Change {
241
+ readonly scope: string;
242
+ }
243
+ /**
244
+ * This function tells which findings of `lint` a compression by k changes (E10). It scales the
245
+ * bindings of each scope by the marks. Thus a harness that runs the service k times faster learns
246
+ * the orderings that its run breaks. The unmarked symbols are the symbols of the rules with no mark.
247
+ */
248
+ export declare const compressionOf: (svc: ServiceDescription, scenario: Scenario, rules: readonly Rule[], k: number, marks: TimerMarks) => {
249
+ readonly changed: readonly ScopedChange[];
250
+ readonly unmarked: readonly string[];
251
+ };
158
252
  /**
159
253
  * This function applies each service rule to a declaration and a scenario. A rule applies one time
160
254
  * for the service, for each client, for each consumer and worker, or for each limiter. The function
@@ -292,5 +386,5 @@ export declare const drainMachine: (description: ServiceDescription) => Machine;
292
386
  /** These are the static machines of the runtime, by the name that their events carry. */
293
387
  export declare const MACHINES: Readonly<Record<Exclude<MachineName, "drain" | "client">, Machine>>;
294
388
  //#endregion
295
- export { type Allowed, type Backoff, type Binding, type Bound, type Cancel, type Check, type CheckFn, type CheckMetrics, type CheckVerdict, type Clock, type Controller, type ControllerBuilder, type CooperativeFloor, type Coupling, DURATION_FRACTIONS, type Decision, type DelayBand, type Dependency, type DependencyHandle, type DurationHistogram, EPISODES_KEPT, EXIT_SIGNALS, type EmbeddedProcess, type Episode, type Evaluated, type ExitCodes, type ExitSignal, type ExitTrigger, FORMAT, type FailsBy, type Finding, type Gate, type Health, type HealthMetrics, type HealthServer, type HealthVerdict, type Heartbeat, type HeartbeatOptions, type HostStream, type Idempotency, type Inequality, type IterationContext, type Jitter, type JobAction, type JobContext, type JobFailure, type JobSettle, type JobSource, type KeyState, type KeyedLimiter, type Lifecycle, type LifecycleEvent, type LifecycleState, type LimitPolicy, type LimitRule, type Limiter, type LimiterEviction, type LimiterSpec, type LimiterState, type LimiterStore, type LimiterStoreSpec, type MachineName, type MemoryStore, type MessageShape, type MessageTrigger, type Operation, type OperationOf, type Outcome, PROCESS_TRIGGERS, type Phase, type PhaseContext, type PhaseWork, type PlatformClockOptions, type PodTiming, type Poll, type ProbeAnswer, type ProbeKind, type ProcessHost, type ProcessPort, type ProcessTrigger, type Profile, type ProgressMarks, type QueueDrain, type QueueDrainOptions, type QueueKnobs, type Recorder, type RedeliveryPolicy, type RefusalCause, type Refused, type RepeatSignal, type RequestEnd, type RequestEvent, type RequestFn, type RequestOptions, type RequestStream, type RequestWork, type RequeueCause, type Result, type RoutePattern, type RouteRequest, type Rule, type RunOptions, SLIDING_WINDOW_MACHINE, STAGES, type Scenario, type ServiceBuilder, type ServiceDescription, type ShutdownOptions, type Side, type SlidingWindow, type Span, type Stage, type StartOptions, type Step, type StoreFailure, TOKEN_BUCKET_MACHINE, type TakeRequest, type TakeStep, type Term, type Timing, type TokenBucket, type Transport, type TransportImpl, type UnitRequest, type Verdict, type Watchdog, type WatchdogSpec, type WorkerKnobs, type WorkerLoop, type WorkerLoopOptions, asDebounce, asWindow, backoffBand, backoffDelay, backoffProblems, cooperativeAbortAt, daemon, decide, detectionTime, embeddedProcess, ephemeral, failSpan, freeAt, heartbeat, heartbeatDetection, idleSpan, isExitCode, isExitSignal, isMessageShape, keyedLimiter, kubernetes, limitRule, limiterMachine, limiterProblems, limiterStateOf, localDev, longestRetryAfter, matchesMessage, mayReissue, memoryStore, nodeProcess, platformClock, processPortOf, queueDrain, realClock, recoverSpan, redact, redeliverOnce, replay, retryAfterSeconds, service, stageRank, watchdog };
389
+ export { type Allowed, type Backoff, type Binding, type Bound, type Cancel, type Change, type Check, type CheckFn, type CheckMetrics, type CheckVerdict, type Claim, type Clock, type Compressed, type Compression, type Controller, type ControllerBuilder, type CooperativeFloor, type Coupling, DURATION_FRACTIONS, type Decision, type DelayBand, type Dependency, type DependencyHandle, type DurationHistogram, EPISODES_KEPT, EXIT_SIGNALS, type EmbeddedProcess, type Episode, type Epochs, type Evaluated, type ExitCodes, type ExitSignal, type ExitTrigger, FORMAT, type FailsBy, type Finding, type Gate, type Health, type HealthMetrics, type HealthServer, type HealthVerdict, type Heartbeat, type HeartbeatOptions, type HostStream, type Idempotency, type Inequality, type IterationContext, type Jitter, type JobAction, type JobContext, type JobFailure, type JobSettle, type JobSource, type KeyState, type KeyedLimiter, type Lifecycle, type LifecycleEvent, type LifecycleState, type LimitPolicy, type LimitRule, type Limiter, type LimiterEviction, type LimiterSpec, type LimiterState, type LimiterStore, type LimiterStoreSpec, type MachineName, type MemoryStore, type MessageShape, type MessageTrigger, type Operation, type OperationOf, type Outcome, PROCESS_TRIGGERS, type Phase, type PhaseContext, type PhaseWork, type PlatformClockOptions, type PodTiming, type Poll, type ProbeAnswer, type ProbeKind, type ProcessHost, type ProcessPort, type ProcessTrigger, type Profile, type ProgressMarks, type QueueDrain, type QueueDrainOptions, type QueueKnobs, type Recorder, type RedeliveryPolicy, type RefusalCause, type Refused, type RepeatSignal, type RequestEnd, type RequestEvent, type RequestFn, type RequestOptions, type RequestStream, type RequestWork, type RequeueCause, type Result, type RoutePattern, type RouteRequest, type Rule, type RunOptions, SLIDING_WINDOW_MACHINE, STAGES, type Scenario, type ScenarioValue, type ScopedChange, type ServiceBuilder, type ServiceDescription, type ShutdownOptions, type Side, type SlidingWindow, type Span, type Stage, type StartOptions, type Statistic, type Step, type StoreFailure, TOKEN_BUCKET_MACHINE, type TakeRequest, type TakeStep, type Term, type TimerMark, type TimerMarks, type Timing, type TokenBucket, type Transport, type TransportImpl, type UnitRequest, type Verdict, type Watchdog, type WatchdogSpec, type WorkerKnobs, type WorkerLoop, type WorkerLoopOptions, asDebounce, asWindow, backoffBand, backoffDelay, backoffProblems, cooperativeAbortAt, daemon, decide, detectionTime, embeddedProcess, ephemeral, failSpan, freeAt, heartbeat, heartbeatDetection, idleSpan, isExitCode, isExitSignal, isMessageShape, keyedLimiter, kubernetes, limitRule, limiterMachine, limiterProblems, limiterStateOf, localDev, longestRetryAfter, matchesMessage, mayReissue, memoryStore, nodeProcess, platformClock, processPortOf, queueDrain, realClock, recoverSpan, redact, redeliverOnce, replay, retryAfterSeconds, service, stageRank, watchdog };
296
390
  //# sourceMappingURL=index.d.ts.map
package/build/index.js CHANGED
@@ -2318,8 +2318,74 @@ const verdictOf = (rule, bind, evaluated) => {
2318
2318
  const all = inequalities(rule);
2319
2319
  return applicable.some((index) => readsAbsent(all[index], bind)) ? "unbounded" : "holds";
2320
2320
  };
2321
+ /** This function gives the mark of a symbol: its mark at its hop, else the mark of the bare symbol. */
2322
+ const markOf = (marks, key) => marks[key] ?? marks[key.split("@")[0] ?? key];
2323
+ const STEP_EPSILON = 1e-9;
2324
+ const scaled = (seconds, k, mark) => {
2325
+ switch (mark.kind) {
2326
+ case "scalable": return seconds * k;
2327
+ case "fixed": return seconds;
2328
+ case "stepped": return Math.max(Math.ceil(seconds * k / mark.step - STEP_EPSILON), 0) * mark.step;
2329
+ case "floored": return Math.max(seconds * k, mark.floor);
2330
+ }
2331
+ };
2332
+ /**
2333
+ * This function scales each timer of a binding by k, by its mark (E10). An absent timer ("inf" or
2334
+ * "off") stays absent. A chain scales each value. The margins of the binding and the constants of
2335
+ * the rules do not scale: they are tolerances, not timers. A symbol with no mark keeps its value.
2336
+ */
2337
+ const compress = (bind, k, marks) => {
2338
+ if (!Number.isFinite(k) || k <= 0) throw new RangeError(`a compression scales time by a finite factor above 0, not ${k}`);
2339
+ const out = {};
2340
+ const unmarked = [];
2341
+ for (const [key, bound] of Object.entries(bind)) {
2342
+ if (key === "margin" || key === "margins") continue;
2343
+ const mark = markOf(marks, key);
2344
+ if (mark === void 0) {
2345
+ if (bound !== void 0) unmarked.push(key);
2346
+ out[key] = bound;
2347
+ } else if (typeof bound === "number") out[key] = scaled(bound, k, mark);
2348
+ else if (Array.isArray(bound)) out[key] = bound.map((seconds) => scaled(seconds, k, mark));
2349
+ else out[key] = bound;
2350
+ }
2351
+ const margins = {
2352
+ ...bind.margin !== void 0 ? { margin: bind.margin } : {},
2353
+ ...bind.margins !== void 0 ? { margins: bind.margins } : {}
2354
+ };
2355
+ return {
2356
+ bind: {
2357
+ ...out,
2358
+ ...margins
2359
+ },
2360
+ unmarked
2361
+ };
2362
+ };
2363
+ /**
2364
+ * This function tells which rules a compression by k changes. A compression keeps each ordering
2365
+ * when no verdict changes. A change in either direction counts: a violation that the compression
2366
+ * hides is as wrong as one that it makes. The unmarked symbols are the symbols that the rules read.
2367
+ */
2368
+ const compression = (rules, bind, k, marks) => {
2369
+ const compressed = compress(bind, k, marks);
2370
+ const changed = rules.flatMap((rule) => {
2371
+ const before = verdictOf(rule, bind, evaluate(rule, bind));
2372
+ const after = verdictOf(rule, compressed.bind, evaluate(rule, compressed.bind));
2373
+ return before === after ? [] : [{
2374
+ rule: rule.id,
2375
+ before,
2376
+ after
2377
+ }];
2378
+ });
2379
+ const read = new Set(rules.flatMap(symbolsOf));
2380
+ return {
2381
+ changed,
2382
+ unmarked: compressed.unmarked.filter((symbol) => read.has(symbol))
2383
+ };
2384
+ };
2321
2385
  //#endregion
2322
2386
  //#region src/lint.ts
2387
+ /** The law binds this statistic of a distribution. */
2388
+ const LAW_STATISTIC = "p99";
2323
2389
  /** These are the identifiers of the rules that each declaration gets. */
2324
2390
  const SERVICE_RULES = [
2325
2391
  "O04",
@@ -2410,6 +2476,35 @@ const LIMITER_RULES = [{
2410
2476
  const secondsOf = (amount) => rationals.toNumber(convertExact(rationals.fromNumber(amount.value), amount.unit, "s"));
2411
2477
  /** This function gives an amount, or no bound, as the law binds it. */
2412
2478
  const boundOf = (amount) => amount.value === "infinite" ? "inf" : secondsOf(amount);
2479
+ /**
2480
+ * This function gives a value of the scenario as the law binds it, with its epoch. A distribution
2481
+ * binds its p99. A distribution without a p99 binds nothing, thus each rule that reads it depends.
2482
+ */
2483
+ const claimOf = (value) => {
2484
+ if (!("epoch" in value)) return {
2485
+ bound: boundOf(value),
2486
+ epoch: null
2487
+ };
2488
+ if ("value" in value) return {
2489
+ bound: boundOf(value.value),
2490
+ epoch: value.epoch
2491
+ };
2492
+ const statistic = value.distribution["p99"];
2493
+ return {
2494
+ bound: statistic === void 0 ? void 0 : secondsOf(statistic),
2495
+ epoch: value.epoch
2496
+ };
2497
+ };
2498
+ /** This function binds each value of a part of the scenario, at the symbol that `at` gives, and records its epoch. */
2499
+ const bindScenario = (values, at, bind, epochs) => {
2500
+ for (const [symbol, value] of Object.entries(values)) {
2501
+ const claim = claimOf(value);
2502
+ const key = at(symbol);
2503
+ epochs[key] = claim.epoch;
2504
+ if (claim.bound !== void 0) bind[key] = claim.bound;
2505
+ else delete bind[key];
2506
+ }
2507
+ };
2413
2508
  /** This function tells if a unit is a time. A unit that the package does not know is not a time. */
2414
2509
  const isTime = (unit) => {
2415
2510
  try {
@@ -2424,6 +2519,7 @@ const most = (values) => values.length === 0 ? void 0 : Math.max(...values);
2424
2519
  const serviceBindings = (svc, scenario) => {
2425
2520
  const bind = {};
2426
2521
  const from = {};
2522
+ const epochs = {};
2427
2523
  const set = (symbol, bound, said) => {
2428
2524
  if (bound === void 0) return;
2429
2525
  bind[symbol] = bound;
@@ -2446,18 +2542,25 @@ const serviceBindings = (svc, scenario) => {
2446
2542
  set("cooperative_window@process", boundOf(svc.shutdown.cooperativeWindow), "the declared cooperative window");
2447
2543
  const hold = svc.shutdown.phases.find((phase) => phase.name === "fail readiness");
2448
2544
  set("readiness_hold@process", hold === void 0 ? 0 : secondsOf(hold.cap), hold === void 0 ? "no fail readiness phase: the intake stops at once after the signal" : "the cap of the fail readiness phase, which holds while the endpoints update");
2449
- for (const [symbol, amount] of Object.entries(scenario.platform)) set(symbol, boundOf(amount), "the scenario");
2545
+ bindScenario(scenario.platform, (symbol) => symbol, bind, epochs);
2546
+ for (const [symbol, epoch] of Object.entries(epochs)) if (bind[symbol] === void 0) delete from[symbol];
2547
+ else from[symbol] = epoch === null ? "the scenario" : `the scenario, for ${epoch}`;
2450
2548
  return {
2451
2549
  bind,
2452
- from
2550
+ from,
2551
+ epochs
2453
2552
  };
2454
2553
  };
2455
2554
  /** This function gives the client knobs of a dependency at @client, and the values of the scenario at its own hops. */
2456
2555
  const dependencyBindings = (dependency, scenario) => {
2457
2556
  const bind = {};
2557
+ const epochs = {};
2458
2558
  for (const [knob, amount] of Object.entries(dependency.knobs)) bind[`${knob}@client`] = boundOf(amount);
2459
- for (const [symbol, amount] of Object.entries(scenario.dependencies?.[dependency.name] ?? {})) bind[symbol.includes("@") ? symbol : `${symbol}@dependency`] = boundOf(amount);
2460
- return bind;
2559
+ bindScenario(scenario.dependencies?.[dependency.name] ?? {}, (symbol) => symbol.includes("@") ? symbol : `${symbol}@dependency`, bind, epochs);
2560
+ return {
2561
+ bind,
2562
+ epochs
2563
+ };
2461
2564
  };
2462
2565
  /**
2463
2566
  * These are the hops of the law at which the knobs of a transport bind, by its kind. A worker does
@@ -2474,12 +2577,16 @@ const TRANSPORT_HOPS = {
2474
2577
  */
2475
2578
  const transportBindings = (transport, hop, scenario) => {
2476
2579
  const bind = {};
2580
+ const epochs = {};
2477
2581
  for (const [knob, amount] of Object.entries(transport.knobs)) {
2478
2582
  if (!isTime(amount.unit)) continue;
2479
2583
  bind[knob.includes("@") ? knob : `${knob}@${hop}`] = boundOf(amount);
2480
2584
  }
2481
- for (const [symbol, amount] of Object.entries(scenario.transports?.[transport.name] ?? {})) bind[symbol] = boundOf(amount);
2482
- return bind;
2585
+ bindScenario(scenario.transports?.[transport.name] ?? {}, (symbol) => symbol, bind, epochs);
2586
+ return {
2587
+ bind,
2588
+ epochs
2589
+ };
2483
2590
  };
2484
2591
  /**
2485
2592
  * This function gives the symbols of a limiter, and the values of the scenario for its callers.
@@ -2493,8 +2600,12 @@ const limiterBindings = (limiter, scenario) => {
2493
2600
  "retry_after@limiter": retryAfterSeconds(longestRetryAfter(limiter.policy)),
2494
2601
  "retry_after_at_cap@limiter": retryAfterSeconds(idleSpan(limiter.policy))
2495
2602
  };
2496
- for (const [symbol, amount] of Object.entries(scenario.limiters?.[limiter.name] ?? {})) bind[symbol.includes("@") ? symbol : `${symbol}@caller`] = boundOf(amount);
2497
- return bind;
2603
+ const epochs = {};
2604
+ bindScenario(scenario.limiters?.[limiter.name] ?? {}, (symbol) => symbol.includes("@") ? symbol : `${symbol}@caller`, bind, epochs);
2605
+ return {
2606
+ bind,
2607
+ epochs
2608
+ };
2498
2609
  };
2499
2610
  const SCOPED = /@(client|dependency|server)$/;
2500
2611
  const TRANSPORT_SCOPED = /@consumer$/;
@@ -2644,6 +2755,79 @@ const progressFindings = (svc) => {
2644
2755
  }
2645
2756
  return findings;
2646
2757
  };
2758
+ /** This function gives the scopes of a rule that reads these symbols: the service, each client, each consumer and worker, or each limiter. */
2759
+ const scopesOf = (svc, scenario, service, symbols) => {
2760
+ const within = (name, scoped) => {
2761
+ const bind = {
2762
+ ...service.bind,
2763
+ ...scoped.bind
2764
+ };
2765
+ for (const symbol of Object.keys(scoped.epochs)) if (!(symbol in scoped.bind)) delete bind[symbol];
2766
+ return [
2767
+ name,
2768
+ bind,
2769
+ {
2770
+ ...service.epochs,
2771
+ ...scoped.epochs
2772
+ }
2773
+ ];
2774
+ };
2775
+ return symbols.some((symbol) => LIMITER_SCOPED.test(symbol)) ? (svc.limiters ?? []).map((limiter) => within(limiter.name, limiterBindings(limiter, scenario))) : symbols.some((symbol) => TRANSPORT_SCOPED.test(symbol)) ? svc.transports.flatMap((transport) => {
2776
+ const hop = TRANSPORT_HOPS[transport.kind];
2777
+ return hop === null ? [] : [within(transport.name, transportBindings(transport, hop, scenario))];
2778
+ }) : symbols.some((symbol) => SCOPED.test(symbol)) ? svc.dependencies.map((dependency) => within(dependency.name, dependencyBindings(dependency, scenario))) : [[
2779
+ svc.name,
2780
+ service.bind,
2781
+ service.epochs
2782
+ ]];
2783
+ };
2784
+ /** This function gives the epochs of the symbols of a rule that the scenario binds. */
2785
+ const epochsOf = (symbols, epochs) => Object.fromEntries(symbols.flatMap((symbol) => symbol in epochs ? [[symbol, epochs[symbol] ?? null]] : []));
2786
+ /**
2787
+ * These are the marks of the timers of the platform (E10). Kubernetes takes the probe period, the
2788
+ * preStop sleep and the termination grace in whole seconds, thus each is stepped by 1 s. The
2789
+ * cluster sets the propagation of the endpoints, thus a compression of the service cannot scale it.
2790
+ */
2791
+ const PLATFORM_MARKS = {
2792
+ "probe_period@pod": {
2793
+ kind: "stepped",
2794
+ step: 1
2795
+ },
2796
+ "prestop_sleep@pod": {
2797
+ kind: "stepped",
2798
+ step: 1
2799
+ },
2800
+ "termination_grace@pod": {
2801
+ kind: "stepped",
2802
+ step: 1
2803
+ },
2804
+ "propagation_bound@cluster": { kind: "fixed" }
2805
+ };
2806
+ /**
2807
+ * This function tells which findings of `lint` a compression by k changes (E10). It scales the
2808
+ * bindings of each scope by the marks. Thus a harness that runs the service k times faster learns
2809
+ * the orderings that its run breaks. The unmarked symbols are the symbols of the rules with no mark.
2810
+ */
2811
+ const compressionOf = (svc, scenario, rules, k, marks) => {
2812
+ const service = serviceBindings(svc, scenario);
2813
+ const changed = [];
2814
+ const unmarked = /* @__PURE__ */ new Set();
2815
+ for (const rule of rules) {
2816
+ if (!SERVICE_RULES.includes(rule.id)) continue;
2817
+ for (const [scope, bind] of scopesOf(svc, scenario, service, symbolsOf(rule))) {
2818
+ const result = compression([rule], bind, k, marks);
2819
+ for (const change of result.changed) changed.push({
2820
+ ...change,
2821
+ scope
2822
+ });
2823
+ for (const symbol of result.unmarked) unmarked.add(symbol);
2824
+ }
2825
+ }
2826
+ return {
2827
+ changed,
2828
+ unmarked: [...unmarked]
2829
+ };
2830
+ };
2647
2831
  /**
2648
2832
  * This function applies each service rule to a declaration and a scenario. A rule applies one time
2649
2833
  * for the service, for each client, for each consumer and worker, or for each limiter. The function
@@ -2651,26 +2835,14 @@ const progressFindings = (svc) => {
2651
2835
  * Each progress check that names its loops also gets its findings (`PROGRESS_RULE`).
2652
2836
  */
2653
2837
  const lint = (svc, scenario, rules) => {
2654
- const service = serviceBindings(svc, scenario).bind;
2838
+ const service = serviceBindings(svc, scenario);
2655
2839
  const findings = [...declarationFindings(svc), ...progressFindings(svc)];
2656
2840
  for (const rule of rules) {
2657
2841
  if (!SERVICE_RULES.includes(rule.id)) continue;
2658
2842
  const symbols = symbolsOf(rule);
2659
- const scopes = symbols.some((symbol) => LIMITER_SCOPED.test(symbol)) ? (svc.limiters ?? []).map((limiter) => [limiter.name, {
2660
- ...service,
2661
- ...limiterBindings(limiter, scenario)
2662
- }]) : symbols.some((symbol) => TRANSPORT_SCOPED.test(symbol)) ? svc.transports.flatMap((transport) => {
2663
- const hop = TRANSPORT_HOPS[transport.kind];
2664
- return hop === null ? [] : [[transport.name, {
2665
- ...service,
2666
- ...transportBindings(transport, hop, scenario)
2667
- }]];
2668
- }) : symbols.some((symbol) => SCOPED.test(symbol)) ? svc.dependencies.map((dependency) => [dependency.name, {
2669
- ...service,
2670
- ...dependencyBindings(dependency, scenario)
2671
- }]) : [[svc.name, service]];
2672
- for (const [scope, bind] of scopes) {
2843
+ for (const [scope, bind, all] of scopesOf(svc, scenario, service, symbols)) {
2673
2844
  const evaluated = evaluate(rule, bind);
2845
+ const epochs = epochsOf(symbols, all);
2674
2846
  findings.push({
2675
2847
  rule: rule.id,
2676
2848
  scope,
@@ -2678,7 +2850,8 @@ const lint = (svc, scenario, rules) => {
2678
2850
  statement: rule.rule,
2679
2851
  ...rule.window !== void 0 ? { window: rule.window } : {},
2680
2852
  evaluated,
2681
- unbound: symbols.filter((symbol) => bind[symbol] === void 0)
2853
+ unbound: symbols.filter((symbol) => bind[symbol] === void 0),
2854
+ ...Object.values(epochs).some((epoch) => epoch !== null) ? { epochs } : {}
2682
2855
  });
2683
2856
  }
2684
2857
  }
@@ -3671,6 +3844,6 @@ const MACHINES = {
3671
3844
  watchdog: WATCHDOG_MACHINE
3672
3845
  };
3673
3846
  //#endregion
3674
- export { DECLARATION_RULE, DURATION_FRACTIONS, EPISODES_KEPT, EXIT_MACHINE, EXIT_SIGNALS, FORMAT, GATE_MACHINE, LATCH_MACHINE, LIMITER_RULES, MACHINES, POLLER_MACHINE, PROCESS_TRIGGERS, PROGRESS_RULE, SERVICE_RULES, SLIDING_WINDOW_MACHINE, STAGES, TOKEN_BUCKET_MACHINE, WATCHDOG_MACHINE, WINDOW_MACHINE, asDebounce, asWindow, backoffBand, backoffDelay, backoffProblems, capKnob, cooperativeAbortAt, daemon, decide, detectionTime, drainMachine, embeddedProcess, ephemeral, evaluate, failSpan, freeAt, heartbeat, heartbeatDetection, idleSpan, inequalities, isExitCode, isExitSignal, isMessageShape, keyedLimiter, kubernetes, limitRule, limiterMachine, limiterProblems, limiterStateOf, lint, localDev, longestRetryAfter, matchesMessage, mayReissue, memoryStore, nodeProcess, platformClock, processPortOf, queueDrain, realClock, recoverSpan, redact, redeliverOnce, replay, retryAfterSeconds, service, serviceBindings, stageRank, symbolsOf, verdictOf, watchdog, workerLoop };
3847
+ export { DECLARATION_RULE, DURATION_FRACTIONS, EPISODES_KEPT, EXIT_MACHINE, EXIT_SIGNALS, FORMAT, GATE_MACHINE, LATCH_MACHINE, LAW_STATISTIC, LIMITER_RULES, MACHINES, PLATFORM_MARKS, POLLER_MACHINE, PROCESS_TRIGGERS, PROGRESS_RULE, SERVICE_RULES, SLIDING_WINDOW_MACHINE, STAGES, TOKEN_BUCKET_MACHINE, WATCHDOG_MACHINE, WINDOW_MACHINE, asDebounce, asWindow, backoffBand, backoffDelay, backoffProblems, capKnob, compress, compression, compressionOf, cooperativeAbortAt, daemon, decide, detectionTime, drainMachine, embeddedProcess, ephemeral, evaluate, failSpan, freeAt, heartbeat, heartbeatDetection, idleSpan, inequalities, isExitCode, isExitSignal, isMessageShape, keyedLimiter, kubernetes, limitRule, limiterMachine, limiterProblems, limiterStateOf, lint, localDev, longestRetryAfter, matchesMessage, mayReissue, memoryStore, nodeProcess, platformClock, processPortOf, queueDrain, realClock, recoverSpan, redact, redeliverOnce, replay, retryAfterSeconds, service, serviceBindings, stageRank, symbolsOf, verdictOf, watchdog, workerLoop };
3675
3848
 
3676
3849
  //# sourceMappingURL=index.js.map