Skip to content

fix(ui): give every js declaration a reader and gate it - #331

Merged
argszero merged 1 commit into
mainfrom
fix/unreferenced-declarations
Sep 30, 2026
Merged

argszero merged 1 commit into
mainfrom
fix/unreferenced-declarations

Conversation

@argszero

Copy link
Copy Markdown
Owner

Summary

ui/js/*.js has no equivalent of Rust's dead_code lint, and this tree had two
declarations that had lost their last reader:

declaration shape how it got there
DAY_LABELS (ui/js/app.js:982) module-level const d31641d (#32) introduced it; 68f9f70 (#86) replaced its only read point with a share.day.N key lookup and left the constant behind (value changed, too)
nowTime() (ui/js/app.js:3127) function 9a3ff3e/cfd38ac introduced it; 89963f3 (#94, "zero mock data") deleted its five call sites — four D.TRANSACTIONS.unshift and one D.RAISE_REQUESTS.unshift — and left the body

Both are drift rather than a trade-off: the read point moved in a commit that
moved a "who writes this value" rule, and the carrier did not move with it.
ui/README.md still documented nowTime() as the way new timestamps are
written, and still listed DAY_LABELS among the language-sensitive constants.

Related Issue

None — found by inspection, no issue was ever filed for it.

Changes

  • remove both declarations from ui/js/app.js
  • correct the two ui/README.md sentences that described them, and record the
    new invariant + its scope
  • bump the js/app.js cache-bust token (20260929-1 → 20260930-1)
  • new test-only gate src/js_gate.rs (registered in src/main.rs as
    #[cfg(test)] mod js_gate;), no new dependencies, no production code

The invariant. Every top-level declaration in ui/js/*.js (module-body level,
i.e. indentation ≤ 2) must occur at least twice, on identifier boundaries, across
ui/index.html + ui/js/*.js. A companion test derives the file roster from disk,
so adding a ui/js/*.js file without registering it turns the gate red instead of
silently leaving it unguarded.

Why the README and the stylesheet are deliberately not readers. Measured: with
ui/README.md counted, the gate is blind to exactly the two offenders above — the
README happens to mention both names, taking each count from 1 to 2 (A/B readings:
html+js reports 2 violations, html+js+css+README reports 0). Mentioning a name
in prose is not a read point. ui/css/style.css is excluded for the same reason in
reverse: it declares classes and never consumes JS identifiers, so counting it would
only add a false-green channel.

Scope (lexical, stated in the module doc and in ui/README.md). The gate proves
a second occurrence exists, not that the occurrence is a reachable read point —
a same-named token in a comment, a string, or other dead code still satisfies it.
It does not model JS syntax: let a = 1, b = 2 second names, destructuring names and
window.X = ... assignment-style exports are outside the roster by design (prefer a
miss over a false red). No screenshots: this changes no rendered output.

Tests

  • cargo test — 442 passed / 0 failed (baseline 436; the 6 new tests are the gate's own)
  • cargo fmt --check — clean
  • cargo clippy --all-targets -- -D warnings — clean
  • new unit tests added, plus embedded synthetic teeth:
    - the_declaration_extractor_has_teeth — declarations inside comments, strings and
    template literals do not count; a regex literal must not swallow the code after it
    - the_judge_detects_an_injected_dead_declaration — injected dead declaration is named;
    a cross-file reader and a markup reader both count
    - the_identifier_counter_respects_boundaries — fmtM is not matched by fmtMega
    - the_roster_covers_every_js_file_on_disk

A/B. Reverting ui/js/app.js to upstream/main fails exactly the new axis test, and
names both offenders: app.js:982 DAY_LABELS (1 occurrence) · app.js:3127 nowTime (1 occurrence).

One implementation note worth flagging for review: the extractor masks regex literals, and
that leg is load-bearing. esc()'s /[&<>"']/g reads as the start of a string under a naive
masker and swallows the following code — which is how nowTime() slipped past the first pass
at this gate (base yielded 130 declarations, the fixed tree 129; the difference should have
been 2).

Checklist

  • branch name follows the convention (fix/)
  • commit message uses Conventional Commits
  • single responsibility, minimal diff (5 files, +533/−10; all of it the new gate
    except ~15 lines of removals and documentation)

`ui/js/app.js` carried two module-level declarations with no reader left:
`DAY_LABELS` (its only read point became a `share.day.N` lookup in #86 while
the constant stayed behind, value changed) and `nowTime()` (#94 removed its
five call sites -- four `D.TRANSACTIONS.unshift` and one
`D.RAISE_REQUESTS.unshift` -- and left the body). Rust reports this shape via
`dead_code`; JS has no such lint, so nothing was looking.

Remove both declarations, correct the two `ui/README.md` sentences that still
described them (`nowTime()` was documented as the way new timestamps are
written; `DAY_LABELS` was listed among the language-sensitive constants), and
add `src/js_gate.rs`: every top-level declaration in `ui/js/*.js` must appear
at least twice, on identifier boundaries, across `ui/index.html` + `ui/js/*.js`.
A companion test derives the file roster from disk so a new `ui/js/*.js` cannot
slip out from under the rule.

The prose (`ui/README.md`) and the stylesheet are deliberately not readers --
measured: counting the README makes the gate blind to exactly these two
offenders, because the README happens to mention both names (1 -> 2).

Extraction masks comments, strings and regex literals. The regex leg is
load-bearing: `esc()`'s `/[&<>"']/g` otherwise reads as the start of a string
and swallows the following code, which is how `nowTime()` escaped the first
pass at this gate.

Scope: the gate is lexical. It proves a second occurrence exists, not that the
occurrence is a reachable read point, and it does not model JS syntax
(`let a = 1, b = 2` second names, destructuring names and `window.X = ...`
exports are out of the roster by design).

Tests: cargo test 442 passed / 0 failed (was 436); cargo fmt --check and
cargo clippy --all-targets -- -D warnings both clean. A/B on the pre-fix tree
(upstream/main's `ui/js/app.js`) fails exactly the new axis test, naming
`app.js:982 DAY_LABELS` and `app.js:3127 nowTime` at 1 occurrence each.
@argszero

Copy link
Copy Markdown
Owner Author

Self-review (maintainer is the author of this PR; GitHub does not allow approving
one's own PR, so this is recorded as an ordinary comment).

Checked:

  • The rule is not circular. The declaration roster is extracted from the
    scripts themselves and the reader corpus is ui/index.html + ui/js/*.js; no
    snapshot of names or counts is written down. The one written expectation is the
    file roster cross-check, which reads ui/js/ from disk so a new script fails
    the gate instead of escaping it.
  • The exclusion of ui/README.md is measured, not assumed. With the README in
    the reader corpus the gate reports 0 violations on the pre-fix tree — it would
    have shipped blind. The A/B legs are in the PR body.
  • The regex leg is load-bearing and verified by a synthetic leg rather than by
    the real corpus: esc()'s /[&<>"']/g under a naive masker swallows the code
    after it, which is precisely how nowTime() escaped the first draft. That leg
    is now pinned in the_declaration_extractor_has_teeth.
  • Teeth against the real defect. Reverting ui/js/app.js to main fails exactly
    the axis test and names both offenders (line + name + occurrence count); restoring
    it is green. No other test changes colour in either direction.
  • Scope is honest and written down in the module doc and in ui/README.md:
    the gate proves a second occurrence exists, not that it is reachable, and it does
    not model JS syntax (multiple declarators, destructuring, window.X = … exports
    are out of roster by design).
  • No production behaviour changes. src/js_gate.rs is #[cfg(test)]-only and
    pulls in no new dependency; the app.js diff is two deleted declarations plus the
    comments explaining why they are gone.

One thing I deliberately did not do: gate the ?v= cache-bust tokens, or the
README prose. The first is unowned by design in this repo, and the second is the
doc-comment-claims axis, whose settled rule here is "fix the value, do not add a
gate".

Local verification: cargo test 442/0, cargo fmt --check clean,
cargo clippy --all-targets -- -D warnings clean; CI test / fmt / clippy and
msrv both pass.

@argszero
argszero merged commit b59999b into main Sep 30, 2026
2 checks passed
@argszero
argszero deleted the fix/unreferenced-declarations branch September 30, 2026 03:43
@argszero argszero mentioned this pull request Sep 30, 2026
12 tasks done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant