Skip to content

EPBDS-16211 Phase 6 step 2: dissolve the engine/form-modules import cycle - #69

Merged
AlexSamBY merged 1 commit into
masterfrom
phase6-cycle
Sep 23, 2026
Merged

AlexSamBY merged 1 commit into
masterfrom
phase6-cycle

Conversation

@AlexSamBY

Copy link
Copy Markdown
Member

§9.3 step 2 has two halves. This is the cycle half only — the pure-logic extraction (showIf,
payload assembly, data processing into utils) is not started, and the plan now says so rather than
reading as one finished item.

The cycle

modules/form/utils.js imported errorsProcessing from engine/utils and
clearErrorsMap/formsStorage from engine/rules, while the engine imported storedTouched and
withForm back. Both sides reached across for registries that happened to be declared on the other.

The three registries — formsStorage, errorsMap, storedTouched — move to a new
src/core/state/ layer both may import, and are re-exported from where they were declared, so
no call site moves.

errorsProcessing deliberately did not move, which is where this departs from the item's
wording. It reads a meta node and writes the error map — engine business. A neutral module would
have put engine logic below the layer that uses it. The engine hands it to withForm as a
callback instead, through the options bag that already existed.

What the cycle was hiding — a real defect, not a test artifact

engine/rules.js registered six FIELD.TYPE names (AutoSubmit, Data, Icon, Image, Popup,
TableCells) that engine/mapper.js dispatches on — while rules.js imports mapper.js. The
resolver depended on a constant its own importer installed
, and the cycle guaranteed the importer
always loaded.

Cut the cycle, and anything loading the mapper without rules.js gets a switch comparing against
undefined for those six: six documented views silently unreachable. Two mapper tests failed within
a minute of the cut. The constants now sit with the other thirty in modules/variables/fields.js;
the generated view reference is byte-identical and the generator's declaring-sites tripwire was
updated to match.

The layer is enforced, not intended

src/core/state/** has its own no-restricted-imports entry, and utils gained a ban on importing
it, so the stack still reads one way:

utils  <  state  <  components  <  modules  <  engine

Verified by adding an illegal import and watching lint go red, then removing it. §9.9-H5 is where a
layer rule sat dead because nothing ever checked that it fired.

Two tests changed shape rather than being repointed

  • utils.form-lifecycle mocked the engine modules only because of the cycle. It now mocks the
    registry where it lives, through getters: both let bindings are REPLACED by their owners, and a
    plain property hands the test the object from before the replacement.
  • utils.field-lifecycle asserted "refreshes meta errors" by counting a second
    getRegisteredFields() call that the real errorsProcessing happened to make — a side effect of
    the collaborator standing in for the collaboration. It now names the callback.

Gates

All 17 green by exit code in one clean run, plus the example-manifest contract re-run after staging.
Published static/all.css unchanged at sha256 dcdb0a40….

🤖 Generated with Claude Code

…ycle

§9.3 step 2 has two halves. This is the cycle half; the pure-logic extraction
(`showIf`, payload assembly, data processing into utils) is NOT started, and
the plan now says so rather than reading as one finished item.

THE CYCLE. `modules/form/utils.js` imported `errorsProcessing` from
`engine/utils` and `clearErrorsMap`/`formsStorage` from `engine/rules`, while
the engine imported `storedTouched` and `withForm` back. Both sides reached
across for registries that happened to be declared on the other side, so the
three of them — `formsStorage`, `errorsMap`, `storedTouched` — move to a new
`src/core/state/` layer that both may import, and are re-exported from where
they were declared so no call site moves.

`errorsProcessing` deliberately did NOT move, which is where this departs from
the item's wording. It reads a meta node and writes the error map: engine
business. A neutral module would have put engine logic BELOW the layer that
uses it. The engine hands it to `withForm` as a callback instead, through the
options bag that was already there, and `withFormSetup` forwards it.

WHAT THE CYCLE WAS HIDING, AND IT IS A REAL DEFECT. `engine/rules.js`
registered six `FIELD.TYPE` names — AutoSubmit, Data, Icon, Image, Popup,
TableCells — that `engine/mapper.js` dispatches on, while `rules.js` imports
`mapper.js`. The resolver depended on a constant its own importer installed,
and the cycle guaranteed the importer always loaded. Cut the cycle and anything
loading the mapper without `rules.js` gets a `switch` comparing against
`undefined` for those six: six documented views silently unreachable. Two
mapper tests failed within a minute. The constants now sit with the other
thirty in `modules/variables/fields.js`, the generated view reference is
byte-identical, and the generator's declaring-sites tripwire was updated.

THE LAYER IS ENFORCED, NOT INTENDED. `src/core/state/**` has its own
`no-restricted-imports` entry and `utils` gained a ban on importing it, so the
stack still reads one way: utils < state < components < modules < engine.
Verified by adding an illegal import and watching lint go red, then removing
it — §9.9-H5 is where a layer rule sat dead because nothing checked it fired.

Two tests changed shape rather than being repointed. `utils.form-lifecycle`
mocked the engine modules only because of the cycle; it now mocks the registry
where it lives, with getters, because both `let` bindings are REPLACED by their
owners and a plain property hands the test the object from before the
replacement. `utils.field-lifecycle` asserted "refreshes meta errors" by
counting a second `getRegisteredFields()` call that the real `errorsProcessing`
happened to make — a side effect of the collaborator standing in for the
collaboration. It now names the callback.
@AlexSamBY
AlexSamBY merged commit 60dbf1e into master Sep 23, 2026
5 checks passed
@AlexSamBY
AlexSamBY deleted the phase6-cycle branch September 23, 2026 14:01
AlexSamBY added a commit that referenced this pull request Sep 24, 2026
…s baseline (#74)

`storedTouched` and `formInitialValues` were both module-level, and they
are **the same mechanism**:
the registry remembers touches, the baseline decides when to throw them
away. Converting one without
the other would have left the clearing keyed on a global owned by
whichever form initialised last.
Both are per form now, keyed by the final-form object like the error map
before them.

Two commits on purpose: the first records the measurement that decided
the shape, the second does
the work.

### The only demonstrable symptom in this family — and it is the
baseline, not the sharing

Four scenarios failed to show any cross-document effect from the *shared
registry*: two documents
with the same field name at mount; the second re-rendering afterwards; a
fresh document opened after
another was touched; and a second document appearing beside a touched
one. The display condition is
`touchedFor(form)[name] || touched || !pristine`, and final-form's own
`touched` dominates it while
the field is mounted.

The *shared baseline* is a different story. A second document mounting
with its own `initialValues`
looked like a re-initialisation, and **wiped the first document's
touched fields and errors**.
`utils.form-lifecycle`'s *"does not let a second form reset the first
one when it initialises"* pins
it, and fails on the code it replaces.

### A third instance of #69's defect class, found by removing an import

`engine/utils.js` imported `storedTouched` from the form module.
Dropping that import broke **six
Select-reordering tests** in a suite that never mentions forms.

The cause: `engine/utils.js` compares a meta node against
`FIELD.TYPE.SELECT`, which
`modules/form/constants.js` registered — so the comparison worked only
because the engine happened
to import the form module for something else. Those five field views now
sit in
`variables/fields.js` with the rest of the vocabulary; the generated
view reference is
byte-identical and the generator's declaring-sites tripwire was updated.

**This is the third time an "unnecessary" import turned out to be
registering a constant its
consumer depends on** (§9.3 step 2 found the first two). It is recorded
in the plan as a class, not
an incident.

### Fields without an instance

A field reaches its form through the `instance` the engine gives every
field — `mapper.js:640`
passes it on every `renderField` call. A field built without one, which
happens only in tests, has
no remembered touches to consult; the date-field test now says that
explicitly instead of poking a
global.

### The registry mock is gone

It existed to spy on a `clearErrorsMap` that cleared one shared object,
and across two conversions
it fell behind the real module **twice**. The test now asks the real
registry what a form holds —
which is what the code under test does. Fourteen more harnesses stopped
resetting a global between
cases.

### Gates

All 17 green by exit code, plus the example-manifest contract after
staging. 2592 tests. Published
`static/all.css` unchanged.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
AlexSamBY added a commit that referenced this pull request Sep 24, 2026
…s own (#76)

`Decorator` wrote thirteen methods, five getters and a state shape
straight onto `Class.prototype` —
the class `rules.js` had just declared, patched in place as a side
effect of importing the module.
It now installs them on a **subclass**, so the class handed in is left
exactly as written.

### The measurement changed the shape, and the item's proposal with it

§9.3 step 5 proposes an HOC, `withUIRenderLifecycle(Component)`. For the
engine that is the wrong
instrument:

| decorator | consumers |
| --- | --- |
| `Decorator` | 1 — `UIRender` |
| `withDataKind` | 1 |
| `withFormSetup` | 1 |
| `@withTimer` | **7 components** |

There is no reuse for an HOC to provide, and an extra component layer is
precisely what has caught
this codebase out twice already — the wrapper-versus-instance
distinction in #69 and #74. Only
`@withTimer` is a genuine shared HOC, and it already cleans up
correctly.

### The load-bearing detail, found before the edit and not named in the
item

`engine/Data.js` renders nested documents through `Active.UIRender`,
which pointed at the **raw**
class — it worked only because the prototype was mutated.
`Active.UIRender` is now the layer, which
is what it always meant: the engine class *without* the form wrapper.
Three test suites reached for
`UIRender.prototype` for the same reason and read the layer through the
same channel now.

### Also measured: three of the five "call the original" guards were
dead

Instrumenting `Decorator` at decoration time:

```
componentWillUnmount:             undefined
UNSAFE_componentWillMount:        undefined
UNSAFE_componentWillUpdate:       undefined
UNSAFE_componentWillReceiveProps: function
```

Only two had a real original. With a subclass those captures read
genuine parent methods — which is
what "the original" always meant.

### Still open, and said so in the plan rather than implied

The bodies are installed on the subclass's prototype rather than written
as class members, so the
file still reads as assignment rather than declaration. Moving seven
hundred lines into member
position is a separate change with its own risks. What this one removes
is **the mutation escaping
to somebody else's class**.

### Gates

`rules.lifecycle-layer.test.js` pins both halves — the declared class is
untouched, the layer
carries everything — and fails on the code it replaces. All 17 gates
green by exit code, plus the
example-manifest contract after staging. 2599 tests. Published
`static/all.css` unchanged at
`dcdb0a40…`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
AlexSamBY added a commit that referenced this pull request Sep 24, 2026
The first of the extractions §9.3 step 2 asks for.

`showIf` was the **opening fifty-three lines of `RenderComponent`**, a
function seven hundred lines
long, and there was no way to reach it except by rendering a tree. Its
three accepted shapes, the
array-row path prefixing and the **two separate** `relativeData: false`
escapes were described in
comments and asserted nowhere.

It is now `engine/showIf.js`: a function of its arguments, reading
nothing module-level and writing
nothing. `instance` is an argument like any other — read twice, and only
to ask the engine what
`{state.xxx}` interpolates to and what the live form holds.

### It lives in the engine, not in `ui-utils-pack` as the item says

`showIf` is meta semantics rather than a general-purpose helper, and
`utils` sits **below** the
engine. Putting it there would place engine logic under the layer that
consumes it — the same reason
`errorsProcessing` stayed put when the registries moved out in #69. The
item's *"pure functions in
`ui-utils-pack`"* is read as *"pure functions, at the right layer"*, and
the plan now says so.

### The tests were mutation-checked, not trusted for passing

Sixteen direct cases, all green on the first run — which is not evidence
of anything on its own. So
four mutations of the extracted logic were applied and each had to be
caught:

| mutation | caught |
| --- | --- |
| ignore `equal`, always test truthiness | ✅ |
| drop the array-row path prefix | ✅ |
| ignore `relativeData` on a string `showIf` | ✅ |
| skip the merge with live form data | ✅ |

What says the extraction moved no *behaviour* is the 38-example DOM
snapshot, unchanged, plus the
whole suite at 2599.

### One behaviour preserved deliberately, and now stated rather than
implied

A `showIf` that is neither a string nor an object — a number, a boolean
— gates nothing. The
original fell through every branch and rendered; so does this, and a
test says so.

### Still to extract in this half

Payload assembly and the data processing in `rules.js`. Neither has
moved.

### Gates

All 17 green by exit code, plus the example-manifest contract after
staging. `test:react17` failed
once on a jest worker `SIGSEGV` with all 2598 of its tests passing — the
same infrastructure crash
seen in #72, in a different file; two clean re-runs confirm it.
Published `static/all.css` unchanged
at `dcdb0a40…`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
AlexSamBY added a commit that referenced this pull request Oct 2, 2026
## What changes

**The checklists in `docs/UPGRADE-PLAN.md` now match what the plan
records as done.** Several boxes were still open, or said "still open",
after the work had landed and been recorded elsewhere in the plan.
- Each box closed here cites the evidence that closes it.
- Each box that stays open now says why.

There is no code change. In `package.json`, only the text of lint
messages changes.

### Closed, with the record that closes them

**§9.3 step 3, de-globalizing instance state, is marked done.**
- Its header read "STARTED 2026-09-23: `Active.translate` is done, the
rest is not".
- But every item under it had been closed or measured not to leak, and
§11 retired R14 on that evidence (2026-10-01).

**One item in step 3 had no record: the translation registries.** I
checked them now, and there is nothing to convert:
- Every `localiseTranslation` call in `src/core` runs at module top
level.
- `Active.LANG`, the language the phrases are read in, is assigned
nowhere outside the tests.
- The same search found no other write to `Active` from an instance,
apart from the `translate` fallback that step 3 already records.
`renderField`, `Field` and `UIRender` are code, and the same for every
document. `passwordCheck` is set by the host.
- Each zero-hit search was proven with a control: the same pattern run
on the tests finds their writes.

**Phase 6: five boxes are ticked.**
- **State per instance:** §9.3 step 3 and R14, as above.
- **Runtime hazards:** §9.3 step 4 records "This step is now empty", and
R17 was retired on 2026-09-23.
- **The engine ⇄ form-modules import cycle:** gone since #69
(2026-09-23). Re-checked:
- No source file under `src/core/modules` imports the engine. Only its
tests do, to render whole documents.
  - No suppression of the layer rule is left in `src`.
- **Two instances on one page:** four tests cover it:
`rules.two-instances.test.js`, `rules.two-instances-errors.test.js`,
`popup-root.two-instances.test.js`, and the two-documents case in
`examples.strict-mode.test.js`.
- **StrictMode:** done 2026-09-29 (§9.3 step 7). The test has run on
virtual time since #149.

**The go/no-go on the E2/E3 tail of the TS migration.** It was decided
GO on 2026-09-30 (§9.6, Governance), and the tail is done.

**H1/H6: the pack `Tabs`.**
- #60 (2026-09-22) renamed it `StandaloneTabs`, because the two copies
are not interchangeable.
- Its one importer is the demo's `NavTabs`.
- `view: 'Tabs'` renders the engine's copy.

**H1/H6: grepping consumer metas for orphan names is moot, as
measured.**
- The mapper registered none of the twelve component files that #53
deleted.
- Eleven of the names were never views. Each renders the same "… field
does not exist!" placeholder on the commit before #53 and on master.
- The twelfth, `TabList`, resolves to the engine's copy on both.
- So whatever a consumer's metas contain, the deletion changed nothing
they render.

**Phase 1: the react-refresh dev loop, checked on 18.**
- The demo has developed against React 18.3 since #31, and the
checkpoint release on 17 was cancelled (§10). So there is no dev loop on
17 left to check.
- In the running demo, an edit to `src/demo/pages/Examples.jsx` arrived
as a hot update, and so did reverting it.
- Neither reloaded the page: a value set on `window` survived both, and
so did a number typed into a field.

**Ongoing invariant: the alias grep stays at zero.**
- It reads 0 today.
- No `ui-*-pack` alias is configured in any webpack config or in jest,
and no such package is installed. So an import of one fails to resolve.

**Two findings are marked resolved.**
- **E3's `colGroup` propType:** E5 deleted the propTypes, and the TS
type, `TableColGroupProps['colGroup']`, says `style`.
- **F1's tooltip half:** closed at F1 step 2. `omitProps` keeps `view`,
`index` and `symbol` off the bubble.

### Left open, now saying why

- **F2:** "The gate did not open: `moment` stays (2026-09-30)". Its two
boxes wait for a reopening. The F2-seam invariant now says it applies
only if F2 reopens.
- **F4:** postponed on 2026-09-30, and no longer a React 19
prerequisite. Its two boxes stay open.

### `package.json`: the layer lint's messages

Only the message text changes, in 14 messages:
- **The `core/modules` message** said "Two sites in
modules/form/utils.tsx predate this rule and carry an eslint-disable".
#69 (§9.3 step 2) removed both.
- **The `core/state` override's three messages** began "core/utils must
not import". They were copied from the utils override.
- **The layer direction every message quotes left out `core/state`.** It
now matches CLAUDE.md, and the bans configured in the overrides.

I put a probe import of the engine in `src/core/state` and another in
`src/core/modules`. Both still fail lint, with the new text, and the
probes are removed. Rewriting `package.json` through JSON leaves it
byte-identical when nothing is changed, so nothing but these 14 lines
moved.

## Gates

All exited 0, run on this branch:
- `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 number of tests is unchanged.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
AlexSamBY added a commit that referenced this pull request Oct 2, 2026
## What changes

**Five sentences in `docs/UPGRADE-PLAN.md` no longer say that the `Tabs`
duplicate is open.**
- They still said that the pack `Tabs.js` was on disk, and that the H6
duplicate was only half resolved.
- In fact #60 renamed the pack copy `StandaloneTabs` on 2026-09-22, and
§9.9-H6 records the item as closed.

| Where | It said | It now adds |
|---|---|---|
| §1, executive summary | "the pack `Tabs.js` is still present" | the
rename, and why the copies were renamed rather than merged |
| §2.6, finding 14 | "the pack `Tabs.js` survives" | the same |
| §9.2, step 1 | "`Tabs` is **still open** … the gate above still
applies" | the gate is met, and the copy was converted to hooks on
2026-09-28 (§9.3 step 6) |
| Appendix A, structure facts | "**H6 is not closed**" | H6's
`Tabs`/`TabList` item is closed, and its other renames are still open |
| Appendix C, archived snapshot | "only half reconciled" | the rename |

Each sentence is struck through and followed by what closed it, as the
plan marks its other closed items.

**How I checked that none is left:** I searched the plan for the stale
phrases:
- "H6 is not closed";
- "half resolved" and "half reconciled";
- "`Tabs` is **still open**";
- "`Tabs.js` is still" and "`Tabs.js` survives".

They occur on six lines, and on each one only inside a strikethrough.
#150 struck the sixth.

### Found, and not changed here

- **The orphan scan in Appendix B scans nothing now.** It loops over
`src/core/components/*.js`, and since the TypeScript migration the pack
has no `.js` file: it has 42 `.ts`/`.tsx` files.
  - In zsh the glob fails.
  - In bash the loop runs once, on the literal pattern.
  - Its `Tabs` example also predates the rename.
- **Appendix A's structure facts are stale beyond `Tabs`.** They still
list three things that are gone:
  - the engine ⇄ form-modules cycle, which #69 removed;
- an import of `modules` from `components/Text.js:4`, while `Text.tsx`
imports nothing from it;
  - the demo entries at the `src/` root, which H3 moved to `src/demo/`.

## Gates

All exited 0, run on this branch:
- `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.

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