Skip to content

EPBDS-16211 H5: lock the layer direction in ESLint - #64

Merged
AlexSamBY merged 1 commit into
masterfrom
h5-layer-direction
Sep 23, 2026
Merged

AlexSamBY merged 1 commit into
masterfrom
h5-layer-direction

Conversation

@AlexSamBY

Copy link
Copy Markdown
Member

Two production violations existed. One is fixed; one is the cycle Phase 6 dissolves and is now marked rather than tolerated silently.

The fix, slightly wider than the item asked

core/components/Text.js reached into core/modules/variables for ISO_8601_COMPLETE_DATE. The whole of modules/variables/date.js moved to core/utils/date.ts rather than just that constant — the three regexes are pure and belong together, and splitting them across layers to satisfy a rule would be the letter over the intent.

ISO_8601_FULL goes to the engine (allowed). Our own ISO_8601 has no callers at all — the InputDate hit a grep finds is moment.ISO_8601. Moved, not deleted: removing dead code inside a layering change would hide it.

Measured before writing any rule

Every cross-layer import in src, classified: 73 components→utils, 56 engine→components, 24 each modules→components/utils — and exactly two production violations. modules/form/utils.js now carries two individual eslint-disable lines naming §9.3 step 2, so the rule still fails on a third, and removing them later is a visible two-line deletion.

Two traps in the ESLint mechanism, both hit here

1. An override REPLACES a rule's config — it does not merge. A catch-all src/core/** placed last silently killed the three layer rules; only the demo ban survived. The tree still linted clean, so nothing said so. The catch-all now comes first and every layer entry restates both the demo ban and the semantic-ui-react guard. Without that restatement, this change would have quietly removed the SUIR guard from most of src/core.

2. patterns must be homogeneous in ESLint 8 — all strings or all objects. Mixing the existing 'semantic-ui-react/*' string with object entries makes the whole config invalid, and the error names the value, not the cause.

Tests are exempt from the layer patterns

A final src/**/__tests__/** override restores just the SUIR guard. The rule describes the architecture of shipped code; engine tests legitimately import the 38-example corpus from demo/, and the form-module test exercises the very cycle above. Five such imports, all deliberate.

Every rule was proved to fail on a planted violation

planted caught
utils → components ✓
utils → engine ✓
components → modules ✓
components → engine ✓
a third modules → engine ✓
core → demo ✓
semantic-ui-react ✓ — to confirm trap 1 had not re-emerged

A rule that cannot fail is decoration.

Not adopted

"demo → library surface only." The demo imports core directly across nearly all pages; the plan already gates that on §9.4 growing the public surface.

Verified

By exit code: lint:js, lint:css, typecheck, typecheck:contract, css:fixture:check, both docs checks, build, build-lib, build-css, test:pack:budget, jest and test:coverage all exit 0. static/all.css sha256 unchanged.

🤖 Generated with Claude Code

Two production violations existed. One is fixed, one is the cycle Phase 6
dissolves and is now marked rather than tolerated silently.

THE FIX, slightly wider than the item asked. core/components/Text.js reached
into core/modules/variables for ISO_8601_COMPLETE_DATE. The whole of
modules/variables/date.js moved to core/utils/date.ts rather than just that
constant: the three regexes are pure and belong together, and splitting them
across layers to satisfy a rule would be the letter over the intent.
ISO_8601_FULL goes to the engine (allowed); our own ISO_8601 has no callers
at all — the InputDate hit a grep finds is moment.ISO_8601. Moved, not
deleted: removing dead code inside a layering change would hide it.

MEASURED BEFORE WRITING ANY RULE. Every cross-layer import in src,
classified: 73 components->utils, 56 engine->components, 24 each
modules->components and modules->utils, and exactly TWO production
violations. modules/form/utils.js now carries two individual eslint-disable
lines naming §9.3 step 2, so the rule still fails on a THIRD and removing
them later is a visible two-line deletion.

TWO TRAPS IN THE ESLINT MECHANISM, both hit here:

  An override REPLACES a rule's config, it does not merge. A catch-all
  src/core/** placed LAST silently killed the three layer rules — only the
  demo ban survived, and the tree still linted clean, so nothing said so.
  The catch-all now comes FIRST and every layer entry restates both the demo
  ban and the semantic-ui-react guard. Without that restatement this change
  would have quietly removed the SUIR guard from most of src/core.

  `patterns` must be homogeneous in ESLint 8 — all strings or all objects.
  Mixing the existing 'semantic-ui-react/*' string with object entries makes
  the whole config invalid, and the error names the value, not the cause.

TESTS ARE EXEMPT from the layer patterns, via a final src/**/__tests__/**
override that restores just the SUIR guard. The rule describes the
architecture of SHIPPED code; engine tests legitimately import the
38-example corpus from demo/, and the form-module test exercises the cycle
above. Five such imports, all deliberate.

EVERY RULE WAS PROVED TO FAIL on a planted violation: utils->components,
utils->engine, components->modules, components->engine, a third
modules->engine, core->demo, and the SUIR guard itself — that last one to
confirm the first trap had not re-emerged. A rule that cannot fail is
decoration.

Not adopted: "demo -> library surface only". The demo imports core directly
across nearly all pages; the plan already gates that on §9.4.

Verified by exit code: lint:js, lint:css, typecheck, typecheck:contract,
css:fixture:check, both docs checks, build, build-lib, build-css,
test:pack:budget, jest and test:coverage all exit 0. CSS sha256 unchanged.
@AlexSamBY
AlexSamBY merged commit 0e7dbda into master Sep 23, 2026
5 checks passed
@AlexSamBY
AlexSamBY deleted the h5-layer-direction branch September 23, 2026 11:07
AlexSamBY added a commit that referenced this pull request Oct 2, 2026
## What changes

These are the two findings that #151 recorded and left for a change of
their own. Both are in `docs/UPGRADE-PLAN.md`.

### The orphan scan in Appendix B reads TypeScript again

**It had checked no component since the TypeScript migration.** It
looped over `src/core/components/*.js`, and the pack has no `.js` file
now: it has 42 `.ts`/`.tsx` files.
- **In zsh**, the glob fails: `no matches found:
src/core/components/*.js`, and nothing is printed.
- **In bash**, the loop runs once, on the literal pattern. It prints one
line, `198 *`: the pattern becomes `/*'`, which matches every file that
contains a `'`.

**The new command finds the pack's `.ts` and `.tsx` files** and strips
either extension. It matches an import in either quote style, and leaves
out the component's own file under either extension.

How I checked it:
- **Run as the plan prints it,** in bash and in zsh, it scans all 42
files, with the same output in both shells.
- **It reports nothing as an orphan today:** no pack file reads 0.
- **It can still fail:** a file I planted in the pack with no importer
read 0. I then deleted it.

**Its notes on reading the output are re-measured.** The old ones
described what the scan showed on 2026-09-17:
- **`index`** used to be "the only remaining 0". It now reads 5, and
none of the five is the pack's barrel: three import the engine's
`index`, and two are doc comments. The barrel itself is imported by
directory path (`from '../components'`), which the pattern cannot see.
- **The same-name trap still applies, with a current example.** `files`
reads 5, and two of the five import `modules/variables/files.ts`. The
old example, `Tabs`, went with the H6 rename.
- **A count includes the demo.** `StandaloneTabs` reads 1, and that one
importer is the demo's `NavTabs`.

### Appendix A's structure facts

Each fact that is gone is struck through and followed by the change that
removed it, as the plan marks its other closed items:
- **The engine ⇄ form-modules cycle** went with #69 (§9.3 step 2,
2026-09-23).
- **`components/Text.js:4` importing `modules`** went with #64 (H5,
2026-09-23). The layer lint rejects both of these imports now.
- **The demo entries at the `src/` root** moved to `src/demo/` with #63
(H3, 2026-09-23).

Two more fixes in the same bullet:
- **The orphan-scan result** is the re-run above. It used to say the
barrel read 0.
- **The engine `TabList`'s path** now also gives its current one,
`src/core/engine/components/TabList.tsx`. This one was not in #151's
list.

There is no code change.

## Gates

All exited 0 on this branch, `test:coverage` on its second run:
- `lint:js`, `lint:css`, `typecheck`, `typecheck:contract`;
- `docs:props:check`, `docs:views:check`, `css:fixture:check`;
- `test:env-flags`, `build`;
- `test:coverage`, thresholds included;
- `test:pack`, `test:pack:peers`, `test:types`;
- `test:react16`, `test:react17`, `test:react19`, 2854 tests each, with
no worker crash;
- `test:e2e`, 41 tests;
- the manifest contract.

**The first `test:coverage` run failed one test, and this PR is not the
cause.** `examples.strict-mode.test.js` compares each example's DOM
after an edit, with and without StrictMode. On `tableForm` the two runs
read different DOM:
- the plain run read the date picker still open, with the typed text;
- the strict run read it closed, with the value reverted.

This is the same kind of race #149 fixed, through animation frames
instead of timers:
- The picker opens, aligns and closes on animation frames, through
`rc-util/raf`. #149 left `requestAnimationFrame` on real time.
- The 400 ms the test waits after an edit are virtual, and they take 1–3
ms of real time. Whether a real frame lands inside them is chance. With
the test's own timer setup and no coverage, 2 of 3 plain runs had no
frame there, and read the picker open.
- With frames suppressed, both modes read the picker open. So the
comparison depends on whether a frame ran.

The rerun passed all 2854 tests. The fix belongs in the test, in a
change of its own.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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