instar 1.3.1244 → 1.3.1245

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.
@@ -3744,6 +3744,11 @@
3744
3744
  .sub-quota-pct { opacity:.65; }
3745
3745
  .sub-quota-track { height:8px; border-radius:4px; background:var(--border,#2a2a2a); overflow:hidden; }
3746
3746
  .sub-quota-fill { height:100%; background:linear-gradient(90deg,#4caf80,#3a9); }
3747
+ /* Severity bands (dashboard/subscriptions.js quotaSeverity): <75% ok, 75-89% warn, >=90% critical.
3748
+ Colour is redundant with the always-present "N% used" text, never the only signal. */
3749
+ .sub-quota-fill.sub-quota-ok { background:linear-gradient(90deg,#4caf80,#3a9); }
3750
+ .sub-quota-fill.sub-quota-warn { background:linear-gradient(90deg,#f59e0b,#e0900a); }
3751
+ .sub-quota-fill.sub-quota-critical { background:linear-gradient(90deg,#f87171,#ef4444); }
3747
3752
  .sub-pending { border:1px solid var(--border,#2a2a2a); border-radius:10px; padding:12px 16px; margin-bottom:10px; }
3748
3753
  .sub-pending-head { display:flex; justify-content:space-between; align-items:baseline; gap:12px; }
3749
3754
  .sub-pending-label { font-weight:600; }
@@ -202,18 +202,45 @@ function el(doc, tag, cls, text) {
202
202
  return node;
203
203
  }
204
204
 
205
+ // Quota-bar severity thresholds. A single green bar made a 92%-consumed account
206
+ // read the same as a 4%-consumed one at a glance, so the bar communicated width
207
+ // only. These bands give it colour as well: comfortable / getting tight / about
208
+ // to wall. PRESENTATION ONLY — nothing routes, sheds, or swaps on this class;
209
+ // placement and load-shedding keep reading the numeric quota they always read.
210
+ export const QUOTA_WARN_PCT = 75;
211
+ export const QUOTA_CRITICAL_PCT = 90;
212
+
213
+ /** Severity band for an already-clamped 0–100 integer. */
214
+ export function quotaSeverity(usedPct) {
215
+ const used = clampPct(usedPct);
216
+ if (used >= QUOTA_CRITICAL_PCT) return 'critical';
217
+ if (used >= QUOTA_WARN_PCT) return 'warn';
218
+ return 'ok';
219
+ }
220
+
221
+ // Severity → STATIC class literal. The map is the reason the class attribute stays
222
+ // a fixed literal (the file's safety contract): the severity is chosen from a
223
+ // closed set of three, never interpolated from data.
224
+ const QUOTA_FILL_CLASS = Object.freeze({
225
+ ok: 'sub-quota-fill sub-quota-ok',
226
+ warn: 'sub-quota-fill sub-quota-warn',
227
+ critical: 'sub-quota-fill sub-quota-critical',
228
+ });
229
+
205
230
  /** A labelled quota bar. `pct` is clamped to a 0–100 NUMBER before it reaches the
206
- * only dynamic attribute (style width); the percent text is also from that number. */
231
+ * only dynamic attribute (style width); the percent text is also from that number.
232
+ * The fill also carries a severity class derived from that same clamped number. */
207
233
  export function quotaBar(doc, label, pct, resetIso, now = Date.now()) {
208
234
  const wrap = el(doc, 'div', 'sub-quota');
209
235
  const used = clampPct(pct);
236
+ const severity = quotaSeverity(used);
210
237
  const head = el(doc, 'div', 'sub-quota-head');
211
238
  head.appendChild(el(doc, 'span', 'sub-quota-label', sanitizeForDisplay(label, 'label')));
212
239
  const resetTxt = resetIso ? countdown(resetIso, now, { expiredWord: 'resetting' }) : '';
213
240
  head.appendChild(el(doc, 'span', 'sub-quota-pct', `${used}% used${resetTxt ? ` · resets in ${resetTxt}` : ''}`));
214
241
  wrap.appendChild(head);
215
242
  const track = el(doc, 'div', 'sub-quota-track');
216
- const fill = el(doc, 'div', 'sub-quota-fill');
243
+ const fill = el(doc, 'div', QUOTA_FILL_CLASS[severity]); // static literal from a closed set
217
244
  fill.style.width = `${used}%`; // safe: `used` is a clamped integer
218
245
  track.appendChild(fill);
219
246
  wrap.appendChild(track);
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": 1,
3
3
  "generatedFrom": "source-tree",
4
4
  "registrySha256": "890eec3dcf80e4388c396f43001a0bd3fabc80c88f79aa9525885d6402a6dfdf",
5
- "packageVersion": "1.3.1244",
5
+ "packageVersion": "1.3.1245",
6
6
  "guards": [
7
7
  {
8
8
  "ref": "docs/audits/phase-b/f10-triage.md",
@@ -1,5 +1,5 @@
1
1
  {
2
- "sha256": "31fa6f41d59fa409722165554609c10263f47aed73da9f99261c689342b001af",
2
+ "sha256": "7957af2c8bb2014be0ed964adc5be0a94c9be477962f55fd3869f8915640c1bd",
3
3
  "registrySha256": "890eec3dcf80e4388c396f43001a0bd3fabc80c88f79aa9525885d6402a6dfdf",
4
- "packageVersion": "1.3.1244"
4
+ "packageVersion": "1.3.1245"
5
5
  }
@@ -2,5 +2,5 @@
2
2
  "sha256": "890eec3dcf80e4388c396f43001a0bd3fabc80c88f79aa9525885d6402a6dfdf",
3
3
  "articleCount": 91,
4
4
  "generatedFrom": "docs/STANDARDS-REGISTRY.md",
5
- "packageVersion": "1.3.1244"
5
+ "packageVersion": "1.3.1245"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.1244",
3
+ "version": "1.3.1245",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-09-17T01:21:28.866Z",
5
- "instarVersion": "1.3.1244",
4
+ "generatedAt": "2026-09-19T01:17:14.478Z",
5
+ "instarVersion": "1.3.1245",
6
6
  "entryCount": 203,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": 1,
3
3
  "generatedFrom": "source-tree",
4
4
  "registrySha256": "890eec3dcf80e4388c396f43001a0bd3fabc80c88f79aa9525885d6402a6dfdf",
5
- "packageVersion": "1.3.1244",
5
+ "packageVersion": "1.3.1245",
6
6
  "guards": [
7
7
  {
8
8
  "ref": "docs/audits/phase-b/f10-triage.md",
@@ -1,5 +1,5 @@
1
1
  {
2
- "sha256": "31fa6f41d59fa409722165554609c10263f47aed73da9f99261c689342b001af",
2
+ "sha256": "7957af2c8bb2014be0ed964adc5be0a94c9be477962f55fd3869f8915640c1bd",
3
3
  "registrySha256": "890eec3dcf80e4388c396f43001a0bd3fabc80c88f79aa9525885d6402a6dfdf",
4
- "packageVersion": "1.3.1244"
4
+ "packageVersion": "1.3.1245"
5
5
  }
@@ -2,5 +2,5 @@
2
2
  "sha256": "890eec3dcf80e4388c396f43001a0bd3fabc80c88f79aa9525885d6402a6dfdf",
3
3
  "articleCount": 91,
4
4
  "generatedFrom": "docs/STANDARDS-REGISTRY.md",
5
- "packageVersion": "1.3.1244"
5
+ "packageVersion": "1.3.1245"
6
6
  }
@@ -0,0 +1,29 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ The Subscriptions dashboard's quota bars are now colour-coded by how much of each account's limit is used. Every bar previously rendered in the same green regardless of utilization, so the only difference between an account at 4% and one at 92% was the bar's length — a distinction that is hard to read at a glance across a stacked list of accounts, which is exactly where the list is read.
9
+
10
+ Each bar's fill now carries one of three severity classes derived from the same clamped 0–100 integer that already sets its width: green below 75% used, amber from 75% through 89%, red at 90% and above. The percent text that has always sat beside the bar is unchanged, so the colour is redundant with information already on screen rather than the only way to read the value.
11
+
12
+ This is presentation only. No routing, placement, load-shedding, proactive-swap or alerting decision reads the new class — those keep consuming the numeric quota they already consumed, on their own thresholds, unchanged.
13
+
14
+ ## What to Tell Your User
15
+
16
+ Your Subscriptions dashboard now shows each account's quota bar in green, amber or red depending on how much of the limit is used, instead of always green. An account that is comfortable stays green, one that is getting tight turns amber at 75%, and one that is at or near its wall turns red at 90%. Nothing about how work is routed between your accounts changed — this only makes the page easier to scan. The percentage is still written next to every bar.
17
+
18
+ ## Summary of New Capabilities
19
+
20
+ - Subscriptions quota bars are colour-coded: green (<75% used), amber (75–89%), red (≥90%).
21
+ - The colour is derived from the already-clamped percent, so a bad reading cannot produce a misleading colour — it clamps to 0% / green or 100% / red.
22
+ - Colour is never the only signal: the "N% used" text is unchanged and always present.
23
+ - No routing, shedding or alerting behaviour reads the new class.
24
+
25
+ ## Evidence
26
+
27
+ - Visual: the shipped module and the shipped CSS rendered together against the account mix from the operator's reference screenshot — 0/22/68% green, 83/89% amber, 100% red.
28
+ - Unit (`tests/unit/subscriptions-render.test.ts`, 92 passing): both sides of both band boundaries (74/75 and 89/90), severity derived from the clamped number (1000 → critical, −50 → ok, non-numeric → ok, 74.6 → warn by rounding), the fill's class attribute, and a hostile percent proving the class stays a closed-set literal.
29
+ - Mutation-checked: flipping `>=` to `>` on the warn boundary fails the 75% case, so the boundary assertions are load-bearing rather than decorative.
@@ -0,0 +1,139 @@
1
+ # Side-Effects Review — quota-bar severity colours
2
+
3
+ **Change:** `dashboard/subscriptions.js` `quotaBar()` now derives a severity band
4
+ (`ok` / `warn` / `critical`) from the already-clamped 0–100 percent and puts it on the
5
+ fill's class; `dashboard/index.html` gives each band a colour. Thresholds: ok <75,
6
+ warn 75–89, critical ≥90.
7
+
8
+ **Tier:** 1 — presentation-only, 3 files, no `src/` runtime surface, no new capability,
9
+ no migration, no irreversibility. (Note: `dashboard/` is outside
10
+ `scripts/instar-dev-precommit.js`'s `inScope()` filter, so the gate does not demand a
11
+ trace here. The artifact is produced anyway — see §9.)
12
+
13
+ ## Phase 1 — Principle check (signal vs authority)
14
+
15
+ **Does this change involve a decision point — something that gates information flow,
16
+ blocks actions, filters messages, or constrains agent behaviour?**
17
+
18
+ No. `quotaSeverity()` is a pure function from a number the renderer already holds to one
19
+ of three CSS class literals. It has no consumer other than the DOM. It does not gate,
20
+ block, filter, route, shed, alert or gate-keep anything, and nothing reads it back.
21
+
22
+ The valid-reason category is "presentation of data already computed and already
23
+ displayed." The numeric quota that placement, proactive-swap, load-shedding and the
24
+ quota brake consume is untouched, and those subsystems keep their own thresholds — this
25
+ change deliberately does not unify them (see §5).
26
+
27
+ ## 1. Over-block — what legitimate inputs does this reject that it shouldn't?
28
+
29
+ Nothing is rejected; there is no accept/reject decision. The nearest analogue is a
30
+ misleading colour. The band is computed from `clampPct()`'s output, so the reachable
31
+ inputs are exactly the integers 0–100 and the three bands partition that range with no
32
+ gap and no overlap. A non-finite or missing reading clamps to 0 and renders green — the
33
+ same "empty bar" a missing reading already rendered, so this does not invent a
34
+ reassuring colour for absent data. An account with no quota reading at all still takes
35
+ the separate `sub-account-noquota` path and draws no bar.
36
+
37
+ ## 2. Under-block — what failure modes does this still miss?
38
+
39
+ - **A stale reading still looks authoritative.** A bar polled ten minutes ago renders
40
+ the same as one polled ten seconds ago; colour does not encode freshness. This is
41
+ pre-existing (the bar itself had the same property) and out of this change's reach.
42
+ - **0% green is ambiguous** between "genuinely unused" and "clamped from an unusable
43
+ reading." Also pre-existing — the width was already 0 in both cases — and the row's
44
+ status text is what distinguishes them.
45
+ - **Colour does not distinguish 90% from 100%.** Deliberate: both mean "do not plan
46
+ work here", and a fourth band buys resolution the operator does not act on differently.
47
+ - **The 5-hour and weekly bars are banded independently.** An account green on 5-hour and
48
+ red on weekly shows both, which is correct — they are different limits — but there is
49
+ no single per-account "worst band" summary. Not attempted here.
50
+
51
+ ## 3. Level-of-abstraction fit
52
+
53
+ Right layer. The severity is a property of *this view* of the number, not of the number,
54
+ so it belongs in the renderer that owns the bar. Pushing it server-side would mean the
55
+ API asserting an operational verdict ("this account is critical") that routing does not
56
+ share, which would be a stronger claim than the data supports and would invite consumers
57
+ to treat a presentation band as authority.
58
+
59
+ A smarter gate does exist for the operational question — `QuotaTracker`, the proactive
60
+ swap's measured threshold, and the placement quota gate — and this change deliberately
61
+ does **not** feed them or read from them. It is a sibling view, not a parallel authority.
62
+
63
+ ## 4. Signal vs authority compliance
64
+
65
+ Compliant, in the strongest available form: this produces neither. It holds no blocking
66
+ authority, and it is not even a signal (nothing consumes it programmatically). The
67
+ brittle-check-with-authority failure mode this question exists to catch is structurally
68
+ unreachable — the only consumer is CSS.
69
+
70
+ The constants are exported (`QUOTA_WARN_PCT`, `QUOTA_CRITICAL_PCT`) so the tests assert
71
+ against the same numbers the code uses. That export is a hazard worth naming: a future
72
+ change that imports them into a routing decision would silently convert a presentation
73
+ band into an authority. The file comment states the boundary; if that ever happens it
74
+ should be a reviewed change, not an import.
75
+
76
+ ## 5. Interactions
77
+
78
+ - **Does it shadow another check?** No. It does not participate in any check.
79
+ - **Is it shadowed?** The `.sub-quota-fill` base rule still carries the green gradient
80
+ and the three band rules follow it with equal-or-higher specificity
81
+ (`.sub-quota-fill.sub-quota-warn` is one class more specific), so the band always wins
82
+ and the base rule remains a correct fallback if a future caller builds a fill without
83
+ a band class. No `!important` anywhere in the chain.
84
+ - **Does it double-fire or race?** No timers, no state, no async. `quotaBar()` is called
85
+ synchronously during render and is idempotent.
86
+ - **Threshold divergence is real and intentional.** The proactive swap's measured
87
+ threshold (default 80%) sits *between* this change's warn (75) and critical (90)
88
+ boundaries, so an operator can see an amber bar on an account the swap has not yet
89
+ moved off. That is honest — amber means "getting tight", not "the swap fired" — but it
90
+ is the most likely question this change generates, and it is why the two numbers are
91
+ not wired to each other.
92
+
93
+ ## 6. External surfaces
94
+
95
+ - **Visible to the operator:** yes — that is the point. The change is additive to the
96
+ existing view; no element is removed, renamed or reordered, and the percent text,
97
+ reset countdown, labels and layout are byte-identical.
98
+ - **Visible to other agents / systems:** no. No API response, no state file, no message,
99
+ no log line changes. The class is never serialized off the page.
100
+ - **Accessibility:** colour is redundant with the always-present "N% used" text, so no
101
+ information is conveyed by colour alone. The three fills stay well above the track
102
+ background in luminance on the dark theme (the dashboard has no light theme and no
103
+ `prefers-color-scheme` block, so there is no second palette to maintain). Amber
104
+ `#f59e0b` and red `#f87171` are both already in the dashboard's existing palette, so
105
+ this introduces no new colours to the page.
106
+ - **Timing / runtime conditions:** none depended on.
107
+
108
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
109
+
110
+ **Machine-local by design, with no coherence surface.** The dashboard is served as static
111
+ assets by whichever machine the operator has open, and the banding is computed in that
112
+ browser from whatever account rows that page already fetched. There is no state to
113
+ replicate, nothing to proxy on read, and no durable record that could strand on a topic
114
+ transfer.
115
+
116
+ The pool-scope account view (`GET /subscription-pool?scope=pool`) is unchanged, and rows
117
+ merged from a peer machine render through the same `quotaBar()` as local rows — so a
118
+ mixed-machine list is banded consistently rather than one machine's rows being coloured
119
+ and another's not. Because the asset ships inside the instar package, a machine still on
120
+ an older version simply renders the old all-green bars until it updates; the two versions
121
+ do not disagree about any value, only about colour. No user-facing notice is emitted, so
122
+ there is no one-voice gating question. No URL is generated.
123
+
124
+ ## 8. Rollback cost
125
+
126
+ Near-zero and immediate. Reverting the commit restores the previous rendering exactly —
127
+ there is no persisted state, no migration, no agent state to repair, and no consumer to
128
+ un-wire. A hot-fix release is not required for correctness: even left in place, a wrong
129
+ threshold only mis-colours a bar whose number is still printed next to it. An operator
130
+ who dislikes the bands before a release can neutralise them by removing three CSS rules.
131
+
132
+ ## 9. Follow-up raised, not deferred
133
+
134
+ `inScope()` in `scripts/instar-dev-precommit.js` does not cover `dashboard/`, although
135
+ the dashboard ships to every install and is the operator's primary read surface. That is
136
+ a genuine gap in the gate — a dashboard change can land with no artifact and no trace.
137
+ This artifact was produced regardless, and the gap is filed as its own item rather than
138
+ widened into this PR (changing the gate's scope is a gate change, with its own blast
139
+ radius, and belongs in a change that can be reviewed on those terms).