quotacap 0.0.56 → 0.0.57

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.
@@ -1,4 +1,4 @@
1
- import { MIN_PACE_SPAN_DAYS, BLEND_K, RISK_MARGIN, GATE_TOL } from "./types.js";
1
+ import { MIN_PACE_SPAN_DAYS, BLEND_K, RISK_MARGIN, GATE_TOL, paceTube } from "./types.js";
2
2
  const DAY_MS = 86400000;
3
3
  /**
4
4
  * Pace from a single reading.
@@ -113,6 +113,7 @@ export function computeAdvisory(q, recent, baseline, now = new Date(), exhausted
113
113
  recentRate: null,
114
114
  baselineRate: null,
115
115
  aheadOfElapsed: null,
116
+ closeToReset: false,
116
117
  avgPace,
117
118
  burnMeasured: false,
118
119
  paceSource: "unknown",
@@ -136,6 +137,7 @@ export function computeAdvisory(q, recent, baseline, now = new Date(), exhausted
136
137
  recentRate: null,
137
138
  baselineRate: null,
138
139
  aheadOfElapsed: null,
140
+ closeToReset: false,
139
141
  avgPace,
140
142
  burnMeasured,
141
143
  paceSource,
@@ -151,6 +153,9 @@ export function computeAdvisory(q, recent, baseline, now = new Date(), exhausted
151
153
  }
152
154
  // Red needs a deadband margin and a cumulatively-ahead position on a
153
155
  // verified clock; a missing or estimated clock caps at Watch.
156
+ // The on-track tube widens with time left: a near-full finish reads on
157
+ // track while the reset is days away, and the shell out to RISK_MARGIN
158
+ // stays Watch so a small miss still warns near the reset.
154
159
  const elapsed = elapsedPctOf(q, now);
155
160
  const estimated = q.resetsAtEstimated === true;
156
161
  const gate = elapsed !== null && !estimated && q.weeklyPct >= elapsed * (1 - GATE_TOL);
@@ -158,14 +163,17 @@ export function computeAdvisory(q, recent, baseline, now = new Date(), exhausted
158
163
  const wastePct = Math.max(0, remaining - burnRate * daysLeft);
159
164
  const daysToExhaust = burnRate > 0 ? remaining / burnRate : Infinity;
160
165
  const over = daysToExhaust < daysLeft * (1 - RISK_MARGIN);
161
- const marginal = !over && daysToExhaust < daysLeft * (1 + RISK_MARGIN);
166
+ const missFrac = Number.isFinite(daysToExhaust) ? Math.abs(daysToExhaust - daysLeft) / daysLeft : Infinity;
167
+ const insideTube = missFrac <= paceTube(daysLeft);
168
+ const inShell = !over && !insideTube && daysToExhaust < daysLeft * (1 + RISK_MARGIN);
162
169
  let status;
163
170
  if (over && gate)
164
171
  status = "at risk";
165
- else if (over || marginal)
172
+ else if (over || inShell || (insideTube && estimated))
166
173
  status = "watch";
167
174
  else
168
175
  status = "on track";
176
+ const closeToReset = status === "on track" && insideTube;
169
177
  let urgency = "on track";
170
178
  if (wastePct > 30 && daysLeft < 3)
171
179
  urgency = "burn now";
@@ -177,7 +185,7 @@ export function computeAdvisory(q, recent, baseline, now = new Date(), exhausted
177
185
  urgency = "save";
178
186
  if (estimated && urgency === "burn now")
179
187
  urgency = "use soon";
180
- return { provider: q.provider, daysLeft, remaining, idealRate, burnRate, recentRate: recent, baselineRate: baseline, aheadOfElapsed, avgPace, burnMeasured, paceSource, daysToExhaust, status, wastePct, urgency, bindingWindow, bindingRemaining, bindingDaysLeft, exhaustedAt };
188
+ return { provider: q.provider, daysLeft, remaining, idealRate, burnRate, recentRate: recent, baselineRate: baseline, aheadOfElapsed, closeToReset, avgPace, burnMeasured, paceSource, daysToExhaust, status, wastePct, urgency, bindingWindow, bindingRemaining, bindingDaysLeft, exhaustedAt };
181
189
  }
182
190
  /**
183
191
  * Known-waste sort key. `wastePct` is a flow (budget forecast to go unspent
@@ -250,10 +258,12 @@ export function recommend(quotas, _task, paceByProvider = new Map(), now = new D
250
258
  advisories,
251
259
  };
252
260
  }
253
- // 3. No meaningful candidate (e.g. all quotas at risk, watch, or exhausted).
261
+ // 3. No meaningful candidate. A hot board names the amber and red tiers.
262
+ // An on-track board with nothing spare (a finish inside the tube) does not.
263
+ const hot = advisories.some(a => a.status === "at risk" || a.status === "watch");
254
264
  return {
255
265
  use: "none",
256
- reason: "all quotas at risk, watch, or exhausted",
266
+ reason: hot ? "all quotas at risk, watch, or exhausted" : "no avoidable waste",
257
267
  wastePct: 0,
258
268
  idealRate: 0,
259
269
  recommendationBasis: "none",
@@ -17,6 +17,7 @@ function normalizeAdvisory(adv) {
17
17
  recentRate: adv.recentRate != null && Number.isFinite(adv.recentRate) ? adv.recentRate : null,
18
18
  baselineRate: adv.baselineRate != null && Number.isFinite(adv.baselineRate) ? adv.baselineRate : null,
19
19
  aheadOfElapsed: typeof adv.aheadOfElapsed === "boolean" ? adv.aheadOfElapsed : null,
20
+ closeToReset: adv.closeToReset === true,
20
21
  avgPace: adv.avgPace !== null && Number.isFinite(adv.avgPace) ? adv.avgPace : null,
21
22
  daysToExhaust: adv.daysToExhaust !== null && Number.isFinite(adv.daysToExhaust) ? adv.daysToExhaust : null,
22
23
  wastePct: adv.wastePct !== null && Number.isFinite(adv.wastePct) ? adv.wastePct : null,
@@ -11,6 +11,23 @@ export declare const MIN_PACE_SPAN_DAYS: number;
11
11
  export declare const BLEND_K = 0.5;
12
12
  export declare const RISK_MARGIN = 0.15;
13
13
  export declare const GATE_TOL = 0.1;
14
+ /** Weekly quota window. "Far from reset" in the on-track tube is this long. */
15
+ export declare const PACE_WINDOW_DAYS = 7;
16
+ /**
17
+ * Narrowest on-track tube, as a fraction of the time still left. At the
18
+ * reset, a finish within 5% of it still reads on track.
19
+ */
20
+ export declare const TUBE_FLOOR = 0.05;
21
+ /**
22
+ * Half-width of the on-track tube, as a fraction of `daysLeft`.
23
+ *
24
+ * The tube grows from `TUBE_FLOOR` at the reset to `margin` once about 4.7
25
+ * days remain, and never wider than `margin`. A forecast inside it is close
26
+ * enough to a full finish to read on track. The shell between the tube and
27
+ * `margin` stays Watch, so a small miss still warns near the reset. Cap risk
28
+ * stays outside `margin` on the early side.
29
+ */
30
+ export declare function paceTube(daysLeft: number, margin?: number): number;
14
31
  export type Urgency = "burn now" | "use soon" | "slow down" | "save" | "on track";
15
32
  export type BurnStatus = "at risk" | "watch" | "on track" | "unknown";
16
33
  export type PaceSource = "recent" | "window-average" | "unknown";
@@ -27,6 +44,13 @@ export interface Advisory {
27
44
  recentRate?: number | null;
28
45
  baselineRate?: number | null;
29
46
  aheadOfElapsed?: boolean | null;
47
+ /**
48
+ * True when the forecast finish sits inside the horizon tube around the
49
+ * reset and the verdict is on track. The badge stays On track even if
50
+ * usage is slightly ahead of elapsed time. False for a clear leftover,
51
+ * a Watch shell, and an estimated clock.
52
+ */
53
+ closeToReset: boolean;
30
54
  /**
31
55
  * Window-average pace (used % ÷ days elapsed), always computed when the
32
56
  * window start is known — independent of `burnRate`, which is the forecast
@@ -8,3 +8,25 @@ export const MIN_PACE_SPAN_DAYS = 6 / 24;
8
8
  export const BLEND_K = 0.5;
9
9
  export const RISK_MARGIN = 0.15;
10
10
  export const GATE_TOL = 0.10;
11
+ /** Weekly quota window. "Far from reset" in the on-track tube is this long. */
12
+ export const PACE_WINDOW_DAYS = 7;
13
+ /**
14
+ * Narrowest on-track tube, as a fraction of the time still left. At the
15
+ * reset, a finish within 5% of it still reads on track.
16
+ */
17
+ export const TUBE_FLOOR = 0.05;
18
+ /**
19
+ * Half-width of the on-track tube, as a fraction of `daysLeft`.
20
+ *
21
+ * The tube grows from `TUBE_FLOOR` at the reset to `margin` once about 4.7
22
+ * days remain, and never wider than `margin`. A forecast inside it is close
23
+ * enough to a full finish to read on track. The shell between the tube and
24
+ * `margin` stays Watch, so a small miss still warns near the reset. Cap risk
25
+ * stays outside `margin` on the early side.
26
+ */
27
+ export function paceTube(daysLeft, margin = RISK_MARGIN) {
28
+ if (!(daysLeft > 0) || !(margin > 0))
29
+ return TUBE_FLOOR;
30
+ const grown = TUBE_FLOOR + margin * (daysLeft / PACE_WINDOW_DAYS);
31
+ return Math.min(margin, grown);
32
+ }
@@ -159,7 +159,8 @@ export function railCells(ps, now) {
159
159
  }
160
160
  // Shared STATE vocabulary (locked badge mapping): any exclusion reads
161
161
  // Not reporting; at-risk status beats urgency, then watch, then unknown;
162
- // on-track rows ahead of cumulative elapsed time revive Ahead of pace;
162
+ // on-track rows ahead of cumulative elapsed time revive Ahead of pace,
163
+ // except a finish inside the horizon tube, which stays On track;
163
164
  // urgency maps to the remaining pace words.
164
165
  export function stateWord(ps) {
165
166
  if (ps.exclusionReason !== null)
@@ -183,6 +184,8 @@ export function stateWord(ps) {
183
184
  return "Watch";
184
185
  if (adv.status === "unknown")
185
186
  return "Measuring";
187
+ if (adv.status === "on track" && adv.closeToReset === true)
188
+ return "On track";
186
189
  if (adv.aheadOfElapsed === true)
187
190
  return "Ahead of pace";
188
191
  switch (adv.urgency) {
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // generated by scripts/build-embed.mjs — do not edit
2
- export const VERSION = "0.0.56";
2
+ export const VERSION = "0.0.57";
@@ -4,7 +4,8 @@ export type PaceBadge = "Not reporting" | "Cap risk" | "Watch" | "Behind pace" |
4
4
  /**
5
5
  * Pace-state badge from the mapping table. Exclusion always wins; a blocked
6
6
  * month next; otherwise status "at risk" wins, then "watch", then "unknown";
7
- * on-track rows ahead of cumulative elapsed time read Ahead of pace;
7
+ * on-track rows ahead of cumulative elapsed time read Ahead of pace,
8
+ * except a finish inside the horizon tube, which stays On track;
8
9
  * otherwise urgency decides. No client thresholds.
9
10
  */
10
11
  export declare function paceBadge(provider: ProviderView): PaceBadge;
@@ -3,7 +3,8 @@ import { ageDuration } from "../state.js";
3
3
  /**
4
4
  * Pace-state badge from the mapping table. Exclusion always wins; a blocked
5
5
  * month next; otherwise status "at risk" wins, then "watch", then "unknown";
6
- * on-track rows ahead of cumulative elapsed time read Ahead of pace;
6
+ * on-track rows ahead of cumulative elapsed time read Ahead of pace,
7
+ * except a finish inside the horizon tube, which stays On track;
7
8
  * otherwise urgency decides. No client thresholds.
8
9
  */
9
10
  export function paceBadge(provider) {
@@ -26,6 +27,8 @@ export function paceBadge(provider) {
26
27
  return "Watch";
27
28
  if (advisory.status === "unknown")
28
29
  return "Measuring";
30
+ if (advisory.status === "on track" && advisory.closeToReset === true)
31
+ return "On track";
29
32
  if (advisory.aheadOfElapsed === true)
30
33
  return "Ahead of pace";
31
34
  switch (advisory.urgency) {
@@ -15,6 +15,11 @@ export interface AdvisoryView {
15
15
  baselineRate?: number | null;
16
16
  /** Cumulative position versus elapsed time; null on excluded/unknown branches. */
17
17
  aheadOfElapsed?: boolean | null;
18
+ /**
19
+ * Forecast finish sits inside the horizon tube and the verdict is on track.
20
+ * Absent on advisories from a daemon older than the tube.
21
+ */
22
+ closeToReset?: boolean;
18
23
  /** Window-average pace (used % ÷ days elapsed); null before the window start is known. */
19
24
  avgPace: number | null;
20
25
  burnMeasured: boolean;