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.
- package/dist/advisory/engine.js +16 -6
- package/dist/advisory/snapshot.js +1 -0
- package/dist/advisory/types.d.ts +24 -0
- package/dist/advisory/types.js +22 -0
- package/dist/format/rows.js +4 -1
- package/dist/version.js +1 -1
- package/dist/web/src/components/PaceBar.d.ts +2 -1
- package/dist/web/src/components/PaceBar.js +4 -1
- package/dist/web/src/state.d.ts +5 -0
- package/dist/webAssets.js +1 -1
- package/dist/webHtml.js +1 -1
- package/package.json +1 -1
- package/web/dist/assets/{index-oqxiN-gi.js → index-BFBVZ1r_.js} +1 -1
- package/web/dist/index.html +1 -1
package/dist/advisory/engine.js
CHANGED
|
@@ -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
|
|
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 ||
|
|
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
|
|
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,
|
package/dist/advisory/types.d.ts
CHANGED
|
@@ -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
|
package/dist/advisory/types.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/format/rows.js
CHANGED
|
@@ -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.
|
|
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) {
|
package/dist/web/src/state.d.ts
CHANGED
|
@@ -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;
|