feat: replace twelve thresholds with one sensitivity control that shows its limits (S8) - #85
Conversation
fd4aed8 to
57aab84
Compare
|
…ws its limits (S8) Settings is one page, five groups, no tabs: sensitivity, digest, excluded from results, connected systems, appearance. It was a second mode of the watchlist component before this; it is its own route now, and the watchlist is a watchlist. Option 10b, and the two rejected alternatives are worth naming because both look reasonable from a distance. Twelve per-metric thresholds: every number honest, and nobody could say what any of them would do to tonight's digest. No thresholds at all: nothing to get wrong, and no answer to "why am I being told this" other than "the product decided" — but the digest's threshold clause is the reason a reader trusts the line, and it needs a setting behind it to be true. So one control, three positions, and the limits it resolves to are printed beneath it in the digest's own words. Not equivalent numbers: the same string, from `digestLimit`, which the digest's threshold clause also reads. `sensitivity.test.ts` builds a real digest and asserts the screen shows what the message wrote, character for character, so a reworded unit fails the build rather than reaching one reader and not the other. No position resolves the savings gate to 0 — at 0 there is no limit the reader set, so the clause is withheld and there is nothing to display, which is why "Everything" is 1 ms rather than off. A site with hand-tuned thresholds maps to the nearest position and is told once, in the digest footer, in the position's own words. Cleared only when a message that carried it completes, so a failed send leaves the sentence owed rather than spent. Silently discarding someone's configuration is worse than the configuration was; silently replacing it is the same failure with a nicer result. Excluded from results is one list over pages AND checks, because applicability is one concept and four screens for it is how a reader ends up with an exclusion they cannot find. Every row keeps its last reading, struck through — struck through says "not counted", an empty cell would say "never measured" — and its reason. Excluding now asks for that reason rather than carrying the migrated one, which is what the concept always required. Deleted, at f37b156: the monitoring-tolerance panel and its twelve fields, the per-page threshold route, mutation, stored overrides and store action, and the dead CSS for all of it. The page-detail calibration panel and the watchlist severity levels were already gone — S3 removed the first and nothing carried the second — so there was nothing to delete there and nothing is given a new home. There is no threshold UI outside /settings, and `sensitivity.test.ts` asserts it structurally rather than promising it in a comment. Appearance is canonical on this screen and reachable at 320px. The sidebar footer control stays as a shortcut and collapses with the rest of the sidebar footer on a narrow viewport; that is correct behaviour, and `settings-reachability.test.ts` is what keeps it correct. The registry's banned-vocabulary allowlist loses watchlist/page.tsx: the copy that earned the entry moved to /settings and became the applicability concept. Also removes six duplicate `const PERSON` declarations in three digest test files. F5 and R2 each independently repaired F4's `actor` → `by` rename over S7's fixtures, and the merge applied both, so main has not type-checked since 96964f9. Not S8's breakage and not S8's to have caused; kept here because the branch cannot be green without it and because deleting a redeclaration is not a judgement anyone needs to make twice. The surviving comment in each file is F5's, which says why the fixture needs a caller at all rather than only that it does.
57aab84 to
296106d
Compare
Rebased onto 96964f9 — superseding my earlier noteSince this PR opened, F5 (#83) and R2 (#86) both merged. Three things changed for S8, and one of them found a bug in The F4 × S7 repair is no longer mine — droppedR2 shipped the same fix. My repair commit is gone; this branch is now a single feature commit on top of current
|
Depends on F1, F3, S3 — all merged. Base:
f37b1569c7e4c286a7407bddd3945cb2b922f8f8.Option 10b: one sensitivity control that displays the limits it resolves to. Twelve per-metric thresholds and no-thresholds-at-all were both considered and rejected — the first gives you every number honestly and no way to say what any of them does to tonight's digest; the second has no answer to "why am I being told this" other than "the product decided", and the digest's threshold clause is the reason a reader trusts the line.
The default savings gate moves from 0 ms to 250 ms.
DEFAULT_PERFORMANCE_THRESHOLDSis now the Normal position, read fromsensitivity.tsrather than restated.Existing sites will see fewer findings in the digest and in the list: anything with a measured saving below 250 ms now folds. That is a real loss of lines, and it is deliberate. A digest line that said "above the limit you set" against a gate of 0 was claiming something false — there was no limit and the reader set nothing — so
thresholdOfwithheld the whole clause. Every position now resolves to a limit the reader can see, which is what makes the sentence true.Sites that had tuned their thresholds by hand are not silently moved to 250 ms: they map to the nearest position and are told, once. See Migration below.
What the screen is
Five groups, in the order a reader needs them, and the order is an argument. Sensitivity first because it is the only setting that changes what the product says. Digest second because it is how it says it. Excluded third because it is the answer to "why am I not seeing X". Connected systems fourth because it is infrastructure. Appearance last because it is the only one not about the site.
Settings was a second
modeof the watchlist component before this — one file, onemodeprop, two pages sharing a header and nothing else. It is its own route now and the watchlist is a watchlist.The limits are the digest's, not a second copy
digestLimitindigest-copy.tshas exactly two readers: the digest's threshold clause, and the row under the control.sensitivity.test.tsbuilds a real digest at each position and asserts the line containsabove the ${shown} you set— so a reworded unit fails the build rather than reaching one reader and not the other (rule 20; rule 21 for asserting against the other half of the decision instead of a literal).No position resolves the savings gate to 0. At 0 the gate is off, the clause is withheld, and there is nothing to display — a position that resolves to nothing cannot be shown. "Everything" is 1 ms, which is every measurement
formatImpactcan write.Migration
A site whose thresholds match no position was configured by hand. Both dishonest answers were available: drop it (their site quietly starts reporting different things) or keep it (a settings screen that cannot show the state it is in). It is mapped to the nearest position and the reader is told once, in the digest footer, in the position's own words — never the numbers, which are what they no longer have a control for.
Cleared only when a message that carried it completes, so a failed send leaves the sentence owed rather than spent.
The distance metric divides by the span the three positions actually cover, not by each field's legal range:
regressionFlooris legal 1–100 but sensitivity only moves it 90–100, and measuring against the range would let a field the control barely touches outvote one it swings across.Excluded from results
One list over pages and checks, because applicability is one registry concept and four screens for it is how a reader ends up with an exclusion they cannot find — the failure the audit recorded when the agent tab hid evidence without saying why.
Every row keeps its last reading, struck through. Struck through says "not counted"; an empty cell would say "never measured". A row with no reading says so in words rather than showing 0 (rule 18). A check standing for several pages shows the worst reading one of them produced, never a tally (rule 19).
Excluding now asks for a reason and stores it (
AgentIgnoreSettings.reasons), which is what applicability always required. Records written by the old unlabelled toggle read asUNLABELLED_EXCLUSION_REASON— the definition of the state that toggle put them in, following the precedentnativeElements.tsalready set for its retiredsuppresseddisposition, now stated once invocabulary.tsand read by both.Include is offered only where the change can be kept. A case-page exclusion is derived with nowhere to persist to until F5, so that row renders without the button rather than with a dead one — the same rule
CasePagesstates for the same concept.Deleted, at f37b156
WatchPage.performanceThresholdOverrides, store action, seed override, andeffectivePerformanceThresholdslayering.normalizeStateerases any stored override on read — a stored value with no writer would otherwise sit there waiting for someone to re-add a reader.NumberStepper/ToleranceField/SettingTooltip/SettingHeaderand ~150 lines of dead CSScase-detail.tsx/settingssensitivity.test.tsasserts it structurally over the wholesrctree, rather than promising it in a commentAppearance at 320px
Canonical here. The sidebar footer keeps its copy as a shortcut and collapses with the rest of the sidebar footer on a narrow viewport — correct behaviour, and only correct because this screen exists.
settings-reachability.test.tsis what keeps it correct: it checks the route is in the collapsed sidebar's nav, that no narrow-viewport block hides a.settings-*container, that.settings-group__headstacks (side by side at 320px the three-segment control gets ~90px, which is not a control), and that the page has nomin-width.Options stay Auto / Light / Dark.
APPEARANCE_LABELand the pre-paint script own those values; renaming them to a parenthetical would be a rule 20 defect with the added risk of drifting from the script R1 found had no test.Registry
vocabulary.json's banned-vocabulary allowlist losessrc/app/(app)/watchlist/page.tsx. The Ignore/Suppress copy that earned the entry moved to/settingsand became the applicability concept; the watchlist itself never used those words. The list may only shrink, and it did.Not done, by decision
docs/visual-inventory.mdstill describes the deleted panels. It is a dated snapshot ("Inventory date: August 4, 2026") and S3 set the precedent of leaving it. Rewriting a record of what the app looked like on a date falsifies it rather than updating it. If it is meant to be current rather than historical, that is a separate call about what the file is for.Validation
npm run lint,npm run typecheck,npm test -- --run(100 files, 1229 tests),npm run build, andnpx --yes npm@10.9.7 ci --dry-run --ignore-scripts --no-audit --no-fundall pass. No dependency or lockfile change.New tests:
sensitivity.test.ts,settings-copy.test.ts,settings-exclusions.test.ts,settings-reachability.test.ts.digest-cadence.test.ts's "is stated, not stored — the setting is S8's" flips: the slot has a producer now, and what it asserts instead is that the setting landed as one switch, on one route, with no per-field variants.