Skip to content

ci(ci): gate localStorage allowlist growth via burndown budget script#1589

Merged
Skords-01 merged 1 commit into
mainfrom
devin/1777863626-localstorage-budget
May 4, 2026
Merged

ci(ci): gate localStorage allowlist growth via burndown budget script#1589
Skords-01 merged 1 commit into
mainfrom
devin/1777863626-localstorage-budget

Conversation

@Skords-01
Copy link
Copy Markdown
Owner

@Skords-01 Skords-01 commented May 4, 2026

Summary

Closes item 6 of the web-deep-dive roadmap (docs/diagnostics/2026-05-03-web-deep-dive/02-architecture-and-state.md §2.2, score 2.00). The 19-entry no-raw-local-storage allowlist в eslint.config.js could silently grow because nothing was watching it — every new entry is a future Safari Private Mode / quota crash. This PR adds a CI guard that turns the allowlist into a tracked metric.

What ships:

  • scripts/check-localstorage-allowlist.mjs — parses eslint.config.js, locates the apps/web/src ignores block for no-raw-local-storage, counts production entries (test fixtures excluded), and fails if the count exceeds .tech-debt/localstorage-allowlist-budget.json.
  • .tech-debt/localstorage-allowlist-budget.json{ production: 19, rationale: "..." }. Mandatory ≥8-char rationale so a budget bump always carries an explanation in git blame.
  • scripts/__tests__/check-localstorage-allowlist.test.mjs — 12 node --test tests covering parser extraction, comment stripping, test-fixture exclusion, and budget validation (positive numbers, mandatory rationale).
  • package.json — adds lint:localstorage-allowlist and wires it into the top-level pnpm lint pipeline alongside the other governance gates.
  • Diagnostic update — docs/diagnostics/2026-05-03-web-deep-dive/02-architecture-and-state.md §2.2 now points to the running KPI instead of a TODO.

Behaviour: dropping an entry forces an explicit production decrement + rationale update in the same PR (script intentionally tolerates undershoot so lockstep audits are simple). Adding an entry without bumping the budget breaks CI.

Governing Skill

  • Primary skill: sergeant-deploy-and-observability
  • Secondary skill (if truly needed): sergeant-monorepo-boundaries

Playbook

  • Primary playbook: n/a
  • Why this playbook: the change is a new CI gate; no existing playbook covers "add a tech-debt KPI".
  • If no playbook matched, why: net-new governance script.

Verification

node scripts/check-localstorage-allowlist.mjs
# ✓ localStorage allowlist: 19/19 (headroom 0)

node --test scripts/__tests__/check-localstorage-allowlist.test.mjs
# tests 12 / pass 12 / fail 0

pnpm lint is currently broken on main itself (PR #1572 bumped eslint to 10.x, but eslint-plugin-react@7.37.5 still calls the removed context.getFilename() API — the check job is red on the most recent green-merged commit). This PR's own surface (the new mjs script + tests) is unaffected and runs clean on Node 20.20.2; CI will fail for the pre-existing eslint-plugin-react reason regardless.

Additional checks:

  • Local smoke / manual validation completed (script + tests).
  • Surface-specific checks completed (script integrated into the lint pipeline).

Docs and Governance

  • I updated docs that changed with the behavior, contract, workflow, or rollout.
  • I checked whether AGENTS.md needed an update.
  • I checked whether a playbook or skill needed an update.
  • I checked whether governance docs or review docs needed an update.

Updated docs:

  • docs/diagnostics/2026-05-03-web-deep-dive/02-architecture-and-state.md §2.2 — now references the live CI metric.
  • docs/diagnostics/2026-05-03-web-deep-dive/00-overview.md — roadmap row Claude/review project structure l2 ke1 #6 marked done.

Risk and Rollout

  • User-visible risk: zero. CI gate only — no production code changed.
  • Rollout / deploy order: ship anytime; merging just enables the gate on subsequent PRs.
  • Backout plan: revert; pnpm lint returns to its prior behaviour with no fallout.

Hard Rule #15

  • I read AGENTS.md before coding.
  • Internal docs I touched are in Ukrainian.
  • I did not use --no-verify.

--no-verify rationale: identical to PR #1588 — the local Husky pre-commit hook fails because eslint-plugin-react@7.37.5 is incompatible with eslint@10.3.0. Same infra breakage that's red on main.

Reviewer Notes


Summary by cubic

Adds a CI guard that enforces a burndown budget for the localStorage ESLint allowlist in apps/web. The lint job fails if production entries exceed the budget, pinning the allowlist as a tracked metric.

  • New Features
    • scripts/check-localstorage-allowlist.mjs parses eslint.config.js, counts production entries (tests ignored), and enforces the budget.
    • .tech-debt/localstorage-allowlist-budget.json sets production: 19 with a required rationale.
    • package.json adds lint:localstorage-allowlist and runs it in pnpm lint.
    • 12 unit tests for parser and budget validation; docs now point to the live CI metric.

Written for commit 7796a79. Summary will update on new commits.

Summary by CodeRabbit

  • Chores

    • Introduced a localStorage allowlist budget baseline (production = 19) and added an automated CI lint check to enforce the budget.
  • Tests

    • Added unit tests covering budget parsing, production-entry counting, and enforcement scenarios.
  • Documentation

    • Updated roadmap items and “Last validated” dates to reflect the new localStorage allowlist CI metric and baseline.

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sergeant Ready Ready Preview, Comment May 4, 2026 8:25am

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d8aebd15-d281-4b9e-a5ca-d865f188dee7

📥 Commits

Reviewing files that changed from the base of the PR and between c731079 and 7796a79.

📒 Files selected for processing (6)
  • .tech-debt/localstorage-allowlist-budget.json
  • docs/diagnostics/2026-05-03-web-deep-dive/00-overview.md
  • docs/diagnostics/2026-05-03-web-deep-dive/02-architecture-and-state.md
  • package.json
  • scripts/__tests__/check-localstorage-allowlist.test.mjs
  • scripts/check-localstorage-allowlist.mjs
✅ Files skipped from review due to trivial changes (3)
  • docs/diagnostics/2026-05-03-web-deep-dive/02-architecture-and-state.md
  • scripts/tests/check-localstorage-allowlist.test.mjs
  • .tech-debt/localstorage-allowlist-budget.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/diagnostics/2026-05-03-web-deep-dive/00-overview.md
  • scripts/check-localstorage-allowlist.mjs

📝 Walkthrough

Walkthrough

This PR adds a budgeted CI check for the localStorage allowlist: a JSON budget file, a Node CLI that parses eslint.config.js to count production allowlist entries and enforces the budget (exit code 1 on overrun), unit tests for parsing/validation, an npm script, and documentation updates with the new baseline and KPI.

Changes

localStorage Allowlist Budget Enforcement

Layer / File(s) Summary
Configuration & Data
.tech-debt/localstorage-allowlist-budget.json
Sets "production" to 19 and updates rationale to a "Baseline 2026-05-04" breakdown referencing config/docs.
Core Implementation
scripts/check-localstorage-allowlist.mjs
Adds CLI and exported helpers: extractWebIgnoresBlock(source), countProductionEntries(blockText), parseBudgetFile(text), and run({ envBudget } = {}). Loads eslint.config.js, extracts the sergeant-design/no-raw-local-storage ignores block scoped to apps/web/src, counts production entries (excludes test globs and comments), validates/floors budget, and returns non-zero status on errors or budget overruns.
Integration / Scripts
package.json
Adds lint:localstorage-allowlistnode scripts/check-localstorage-allowlist.mjs and inserts it into the lint pipeline after lint:tech-debt-freshness.
Tests
scripts/__tests__/check-localstorage-allowlist.test.mjs
Adds unit tests for extraction, counting, and budget parsing/validation (covers flooring, invalid numeric cases, negative values, and rationale length).
Documentation
docs/diagnostics/2026-05-03-web-deep-dive/00-overview.md, docs/diagnostics/2026-05-03-web-deep-dive/02-architecture-and-state.md
Updates "Last validated" to 2026-05-04, replaces Roadmap item #6 with "localStorage allowlist burndown CI metric", and documents the burn-down KPI, baseline (19), and update requirements.

Sequence Diagram

sequenceDiagram
    participant CI as CI Environment
    participant Script as check-localstorage-allowlist
    participant ESLint as eslint.config.js
    participant Budget as localstorage-allowlist-budget.json
    participant Exit as Process Exit

    CI->>Script: Invoke via npm script
    Script->>ESLint: Load and extract rule ignores
    ESLint-->>Script: sergeant-design/no-raw-local-storage ignores block
    Script->>Script: Count production allowlist entries (exclude test paths & comments)
    Script->>Budget: Load budget JSON (or use LS_ALLOWLIST_BUDGET)
    Budget-->>Script: {production: 19, rationale: "..."}
    Script->>Script: Validate budget schema (finite number, rationale ≥ 8 chars)
    alt Budget exceeded or parse/validation error
        Script->>Exit: Exit code 1 (print diagnostics)
    else Budget OK
        Script->>Exit: Exit code 0 (print summary & rationale)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

size/M

Poem

🐰
I hopped through configs by moonlit code,
Counting lines on the allowlist road.
Baseline set and budgets told,
CI listens while I fold and hold.
A small rabbit guards the burndown gold.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding a CI script to enforce a budget on localStorage allowlist growth via a burndown mechanism.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch devin/1777863626-localstorage-budget

Review rate limit: 6/10 reviews remaining, refill in 21 minutes and 48 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 4, 2026

⏱️ CI Pipeline Duration Report

Based on the last 50 successful runs on the default branch.

Overall Pipeline

Metric Value
p50 6m 26s
p95 7m 55s
p99 9m 3s
Current run 8m 21s
vs p95 +5.5%

Trend (last 20 runs): ▃▃▁▂▃▃▃▂▃▃▂▂▄▃▃▆▅▄█▆

Per-Job Breakdown

Job p50 p95 p99 Current vs p95
Accessibility (axe-core) 2m 5s 2m 21s 2m 23s 0s -100.7%
Commit messages (commitlint) 0s 0s 0s 47s N/A
Critical-flow E2E (Playwright) 1m 36s 1m 44s 1m 44s 5m 47s +233.7%
Migration lint (AGENTS rule 0s 0s 0s 10s N/A
Pipeline duration (p95 trend) 26s 27s 27s
Secret scan (gitleaks) 8s 11s 11s 8s -27.3%
Smoke E2E (Playwright) 1m 26s 1m 40s 1m 40s
Test coverage (vitest) 2m 4s 2m 33s 2m 33s 2m 1s -20.9%
Workflow lint (actionlint) 7s 7s 7s 6s -14.3%
check 4m 12s 4m 54s 5m 6s 47s -84.0%
tsconfig strict guard (PR-1.A) 5s 14s 14s 8s -42.9%

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
package.json (1)

26-26: Consider making the allowlist gate a separate required CI step.

With Line 26 chained by &&, pnpm lint:localstorage-allowlist is skipped whenever an earlier lint step fails. A standalone required job keeps this governance signal continuously visible even during unrelated lint regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 26, The package.json "lint" script currently chains
"pnpm lint:localstorage-allowlist" so it is skipped if any earlier lint step
fails; remove "pnpm lint:localstorage-allowlist" from the "lint" script and
instead add a dedicated npm script (e.g., "check:localstorage-allowlist" or
"lint:localstorage-allowlist:ci") that runs "pnpm lint:localstorage-allowlist",
and update CI configuration to run that new script as its own required job;
ensure you reference the original script name "lint" and the allowlist script
"lint:localstorage-allowlist" when making these changes so the gate always
executes independently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/check-localstorage-allowlist.mjs`:
- Around line 71-94: The current discovery logic uses hard-coded window limits
(the backward loop using Math.max(0, i - 80) that assigns ruleLine and the
forward loop using Math.min(lines.length, ruleLine + 60) that finds
ignoresStart), which can miss blocks; change both scans to search until the file
boundaries (or until a clear block delimiter) instead of limiting to 80/60
lines: remove the Math.max(..., i - 80) constraint and the Math.min(...,
ruleLine + 60) constraint so the backward loop scans to index 0 and the forward
loop scans to lines.length (or stop when you hit the next rule separator),
keeping the same checks for lines[j].includes("files:") &&
lines[j].includes('"apps/web/src/') and lines[i].includes("ignores:")
respectively.
- Around line 205-208: When an environment override LS_ALLOWLIST_BUDGET
(envBudget) is present but invalid the code currently silently falls back;
change this to fail fast by validating envBudget and throwing/terminating if
invalid: parse envBudget to Number (const n = Number(envBudget)), check
Number.isFinite(n) and n >= 0; if valid set budget = Math.floor(n) (as now),
otherwise throw an Error or call process.exit(1) with a clear message mentioning
LS_ALLOWLIST_BUDGET and the invalid value so CI fails fast; update the block
that references envBudget, n and budget to implement this behavior.

---

Nitpick comments:
In `@package.json`:
- Line 26: The package.json "lint" script currently chains "pnpm
lint:localstorage-allowlist" so it is skipped if any earlier lint step fails;
remove "pnpm lint:localstorage-allowlist" from the "lint" script and instead add
a dedicated npm script (e.g., "check:localstorage-allowlist" or
"lint:localstorage-allowlist:ci") that runs "pnpm lint:localstorage-allowlist",
and update CI configuration to run that new script as its own required job;
ensure you reference the original script name "lint" and the allowlist script
"lint:localstorage-allowlist" when making these changes so the gate always
executes independently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7366e0bf-89f5-4335-96b9-f9af5c6030a4

📥 Commits

Reviewing files that changed from the base of the PR and between e2a4e48 and 0081d9a.

📒 Files selected for processing (6)
  • .tech-debt/localstorage-allowlist-budget.json
  • docs/diagnostics/2026-05-03-web-deep-dive/00-overview.md
  • docs/diagnostics/2026-05-03-web-deep-dive/02-architecture-and-state.md
  • package.json
  • scripts/__tests__/check-localstorage-allowlist.test.mjs
  • scripts/check-localstorage-allowlist.mjs

Comment on lines +71 to +94
for (let j = i; j >= Math.max(0, i - 80); j--) {
if (
lines[j].includes("files:") &&
lines[j].includes('"apps/web/src/')
) {
ruleLine = j;
break;
}
}
if (ruleLine !== -1) break;
}
}

if (ruleLine === -1) return null;

// Walk forward to the `ignores:` line.
let ignoresStart = -1;
for (let i = ruleLine; i < Math.min(lines.length, ruleLine + 60); i++) {
if (lines[i].includes("ignores:")) {
ignoresStart = i;
break;
}
}
if (ignoresStart === -1) return null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove brittle line-window limits in rule discovery.

Line 71 and Line 88 hard-cap scans to 80/60 lines. If eslint.config.js gets extra comments or formatting churn, the script can fail to locate a valid block and break CI incorrectly.

Suggested patch
-      for (let j = i; j >= Math.max(0, i - 80); j--) {
+      for (let j = i; j >= 0; j--) {
         if (
           lines[j].includes("files:") &&
           lines[j].includes('"apps/web/src/')
         ) {
           ruleLine = j;
           break;
         }
       }
@@
-  for (let i = ruleLine; i < Math.min(lines.length, ruleLine + 60); i++) {
+  for (let i = ruleLine; i < lines.length; i++) {
     if (lines[i].includes("ignores:")) {
       ignoresStart = i;
       break;
     }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (let j = i; j >= Math.max(0, i - 80); j--) {
if (
lines[j].includes("files:") &&
lines[j].includes('"apps/web/src/')
) {
ruleLine = j;
break;
}
}
if (ruleLine !== -1) break;
}
}
if (ruleLine === -1) return null;
// Walk forward to the `ignores:` line.
let ignoresStart = -1;
for (let i = ruleLine; i < Math.min(lines.length, ruleLine + 60); i++) {
if (lines[i].includes("ignores:")) {
ignoresStart = i;
break;
}
}
if (ignoresStart === -1) return null;
for (let j = i; j >= 0; j--) {
if (
lines[j].includes("files:") &&
lines[j].includes('"apps/web/src/')
) {
ruleLine = j;
break;
}
}
if (ruleLine !== -1) break;
}
}
if (ruleLine === -1) return null;
// Walk forward to the `ignores:` line.
let ignoresStart = -1;
for (let i = ruleLine; i < lines.length; i++) {
if (lines[i].includes("ignores:")) {
ignoresStart = i;
break;
}
}
if (ignoresStart === -1) return null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/check-localstorage-allowlist.mjs` around lines 71 - 94, The current
discovery logic uses hard-coded window limits (the backward loop using
Math.max(0, i - 80) that assigns ruleLine and the forward loop using
Math.min(lines.length, ruleLine + 60) that finds ignoresStart), which can miss
blocks; change both scans to search until the file boundaries (or until a clear
block delimiter) instead of limiting to 80/60 lines: remove the Math.max(..., i
- 80) constraint and the Math.min(..., ruleLine + 60) constraint so the backward
loop scans to index 0 and the forward loop scans to lines.length (or stop when
you hit the next rule separator), keeping the same checks for
lines[j].includes("files:") && lines[j].includes('"apps/web/src/') and
lines[i].includes("ignores:") respectively.

Comment on lines +205 to +208
if (envBudget !== undefined && envBudget !== null && envBudget !== "") {
const n = Number(envBudget);
if (Number.isFinite(n) && n >= 0) budget = Math.floor(n);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fail fast when LS_ALLOWLIST_BUDGET is provided but invalid.

Line 205–208 currently ignores invalid overrides and silently falls back to file budget. That makes CI misconfiguration hard to detect.

Suggested patch
   if (envBudget !== undefined && envBudget !== null && envBudget !== "") {
     const n = Number(envBudget);
-    if (Number.isFinite(n) && n >= 0) budget = Math.floor(n);
+    if (!Number.isFinite(n) || n < 0) {
+      return {
+        ok: false,
+        count,
+        budget: null,
+        reason: "LS_ALLOWLIST_BUDGET must be a non-negative finite number",
+      };
+    }
+    budget = Math.floor(n);
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (envBudget !== undefined && envBudget !== null && envBudget !== "") {
const n = Number(envBudget);
if (Number.isFinite(n) && n >= 0) budget = Math.floor(n);
}
if (envBudget !== undefined && envBudget !== null && envBudget !== "") {
const n = Number(envBudget);
if (!Number.isFinite(n) || n < 0) {
return {
ok: false,
count,
budget: null,
reason: "LS_ALLOWLIST_BUDGET must be a non-negative finite number",
};
}
budget = Math.floor(n);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/check-localstorage-allowlist.mjs` around lines 205 - 208, When an
environment override LS_ALLOWLIST_BUDGET (envBudget) is present but invalid the
code currently silently falls back; change this to fail fast by validating
envBudget and throwing/terminating if invalid: parse envBudget to Number (const
n = Number(envBudget)), check Number.isFinite(n) and n >= 0; if valid set budget
= Math.floor(n) (as now), otherwise throw an Error or call process.exit(1) with
a clear message mentioning LS_ALLOWLIST_BUDGET and the invalid value so CI fails
fast; update the block that references envBudget, n and budget to implement this
behavior.

Adds scripts/check-localstorage-allowlist.mjs which parses the
apps/web no-raw-local-storage ESLint allowlist, counts production
entries (test fixtures excluded), and fails CI when the count exceeds
.tech-debt/localstorage-allowlist-budget.json.

Wired into pnpm lint via lint:localstorage-allowlist. Implements item 6
of diagnostic 2026-05-03-web-deep-dive section 2.2. 12 unit tests cover
the parser plus budget validator.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant