Skip to content

Enforce naming, size and separation — and split diffStore - #23

Merged
mindaugaskasp merged 2 commits into
mainfrom
improvement/code-standards-enforcement
Aug 4, 2026
Merged

Enforce naming, size and separation — and split diffStore#23
mindaugaskasp merged 2 commits into
mainfrom
improvement/code-standards-enforcement

Conversation

@mindaugaskasp

Copy link
Copy Markdown
Owner

Adopts clean-code-javascript and Node.js Best Practices as cited references and turns on the parts of them the build can hold — then applies them to the largest violation in the tree.

Spec: specs/2026-08-03-code-standards-enforcement/plan.md

Zero new dependencies. Every rule is core ESLint; the one thing core cannot do (cycle detection) is a script in the existing scripts/check-*.mjs idiom.

Enforcement

naming camelcase · new-cap (capIsNew: false — 194 SCREAMING_CASE test factories are not constructors) · no-underscore-dangle scoped to src/
size max-lines-per-function 60 + max-lines 250 on src/**/*.js, matching the cap .vue already had
warnings --max-warnings 0, which promotes four vue/*-casing rules that previously exited 0
cycles scripts/check-structure.mjs against structure-baseline.json
build npm run build joins npm run check

scripts/lib/legacySize.mjs holds one exact measurement per over-cap file — permission for what exists and not one line more. Beat a cap and the entry must be deleted (check-structure.mjs fails on a stale one); --retighten can only lower a number, never raise one. A baselined cycle that disappears also fails, so removing one is proven rather than claimed.

Structure

  • diffStore 1509 → 796 lines, 54 → 27 state keys, 93 → 44 actions
  • four vertical slices under features/, each reachable only through its index.js: share 212, imageExport 236, pasteToCompare 115, configBackup 64 — all under the 250 cap
  • utils/commands.js — the single registry every menu, shortcut, palette entry, shelf chip and rail button dispatches through
  • uiStore for dialog chrome; theme folded into settingsStore
  • diffStore → tabsStore cycle removed (5 → 4 baselined)

Tests 1976 → 2015, none deleted — the eight diffStore*.test.js files followed their subjects into tests/renderer/features/.

Eight defects caught by the new guards during their own construction

Three were checks that looked enforced while matching nothing:

  1. featureStyleDirs() resolved the repo root one level short — slice CSS left the token guard silently (stylesheets fell 93 → 92)
  2. no-restricted-imports is replaced, not merged by a later block — the layering rule for stores/** dropped the renderer/main fence (hard rule 3) for every store
  3. ESLint's import matcher does not support extglob — !(index)* matched nothing, so slice privacy was decoration

Plus: npm run build was broken while 2015 tests passed (no unit test mounts App.vue); the clear command's guard silently stopped unlinking a saved diff's tab; nine commands pointed at the core for members that had moved.

Each is guarded now, and each guard was verified red→green.

Known gaps

  • diffStore is 796, not the 700 target — deliberate. The rest is the save→replace→pick gate and file-load flow, the core's own guard on its own document. Extracting them would produce a store that is mostly calls home.
  • make e2e not run (needs the Docker container). No e2e spec was edited and none references a moved member — they drive the app through its UI. Worth a run before merge.

🤖 Generated with Claude Code

mindaugaskasp and others added 2 commits August 3, 2026 23:23
…Store

Adopts clean-code-javascript + Node Best Practices as cited references and
turns on the parts of them the build can hold. Zero new dependencies: every
rule is core ESLint, and the one thing core cannot do (cycle detection) is a
script in the existing scripts/check-*.mjs idiom.

Enforcement
- naming: camelcase, new-cap (capIsNew off — 194 SCREAMING_CASE test
  factories are not constructors), no-underscore-dangle scoped to src/
- size: max-lines-per-function 60 + max-lines 250 on src/**/*.js, matching
  the cap .vue already had, with scripts/lib/legacySize.mjs holding one
  EXACT measurement per over-cap file. One added line fails; beat a cap and
  the entry must be deleted. --retighten can only lower a number.
- lint now fails on warnings (--max-warnings 0), which promotes four
  vue/*-casing rules that previously exited 0
- scripts/check-structure.mjs: import cycles against a baseline, plus stale
  legacy entries. A cycle that DISAPPEARS also fails, so removing one is
  proven rather than claimed.
- npm run build joins npm run check

Structure
- diffStore 1509 -> 796 lines, 54 -> 27 state keys, 93 -> 44 actions
- four vertical slices under features/, each reachable only through its
  index.js: share, pasteToCompare, imageExport, configBackup
- utils/commands.js is the single registry every menu, shortcut, palette
  entry, shelf chip and rail button dispatches through
- uiStore for dialog chrome; theme folded into settingsStore
- diffStore -> tabsStore cycle removed (5 -> 4 baselined)

Tests 1976 -> 2015, none deleted; the eight diffStore*.test.js files
followed their subjects into tests/renderer/features/.

Eight defects were caught by the new guards during their own construction,
three of which were checks that looked enforced while matching nothing:
featureStyleDirs resolved the repo root one level short (slice CSS left the
token guard silently), a later config block dropped the renderer/main fence
for every store, and ESLint's import matcher does not support extglob so the
slice-privacy pattern matched nothing. npm run build was also broken while
2015 tests passed — no unit test mounts App.vue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review of #23 found the unlink fix applied to one of two clear() call sites.
_loadReplacement also clears, and dropFiles routes to it PRECISELY when
diffSaved is true — no prompt, because it is saved. Dropping files onto a
saved diff left the tab claiming an entry it no longer held, so that saved
diff could not be reopened. 2015 unit tests and 309 e2e tests passed over it.

Subscribed to the action instead of calling it, so every path gets the unlink
and no future caller can forget; the core still cannot reach tabsStore.

Also from the review:
- the slice-privacy fence matched NOTHING between slices. From outside, the
  specifier carries `features/`; from inside, a sibling is
  `../imageExport/imageExportStore`, which does not — and `../*/*` cannot
  stand in because `*` also matches `..` and would ban the core. Siblings are
  now named, read from disk so a new slice is covered on creation. 11 cases
  pinned in eslintFence.test.js.
- measure() counted a trailing newline as a line, so the four caps
  --retighten wrote carried a free one. Every file cap now equals ESLint's
  own count.
- commands.test.js faked a pre-split bundle: no share/imageExport/
  configBackup, seven dead diff.* fakes, 5 of 42 handlers ever invoked, and a
  vacuous guard assertion. Rebuilt to run every action and every CLI command
  against a live-shaped bundle; verified by re-pointing share-current at the
  core and watching it fail.

Swept six orphaned comments, a typedef path left one level short by the move,
the commands.js line in the slice contract (no slice has one), and an empty
leftover styles directory.

Tests 2015 -> 2027. QA found no regressions: 309/309 e2e (307 Docker + 2
macOS-gated natively), all 42 registry actions and all 96 menu items driven
with zero renderer errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mindaugaskasp
mindaugaskasp merged commit 59c1f4e into main Aug 4, 2026
2 checks passed
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