diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee29efa8..e8622555 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -270,13 +270,17 @@ jobs: - name: Install dependencies run: npm ci - # `--legacy-peer-deps` is deliberate, not carelessness: an installed package still declares a + # `--legacy-peer-deps` is deliberate, not carelessness: installed packages still declare a # `react ^16.8.0 || ^17.0.0 || ^18.0.0` peer, so npm would abort this install with ERESOLVE. - # 1. react-final-form 6.5.9 (19-ready line is 7.0.1 — §9.7-F4) - # There were TWO. `semantic-ui-react` was the other, and §9.7-F1 step 3½ removed it — which is - # the whole point of that step: the dependency ENTRY is what caps a host's React, not the - # bundled code (§2.6-11). One cap left, and it is a tracked upgrade. The flag overrides the - # peer check for this throwaway install only and never touches what we declare to consumers. + # 1. react-final-form 6.5.9 (19-ready line is 7.0.1 — §9.7-F4) + # 2. react-final-form-arrays 3.1.4 (19-ready line is 5.0.0 — same §9.7-F4 bump) + # There were THREE. `semantic-ui-react` was the third, and §9.7-F1 step 3½ removed it — which + # is the whole point of that step: the dependency ENTRY is what caps a host's React, not the + # bundled code (§2.6-11). Corrected 2026-09-17: this comment used to say "One cap left" and + # list only react-final-form. It was wrong — `react-final-form-arrays` is a direct dependency + # carrying the identical cap, so the flag is needed for it too, and the §9.7-F4 bump has to + # move both packages together. The flag overrides the peer check for this throwaway install + # only and never touches what we declare to consumers. # `--no-save` leaves package.json alone. - name: Install React 19 over the lock run: npm install --no-save --legacy-peer-deps react@19 react-dom@19 diff --git a/docs/SUPPORTED-PROPS.md b/docs/SUPPORTED-PROPS.md index e2fe9ea1..d9492acb 100644 --- a/docs/SUPPORTED-PROPS.md +++ b/docs/SUPPORTED-PROPS.md @@ -95,8 +95,8 @@ CSS contract: `src/style/components/table.less` hangs EVERY cell's padding off ` | Prop | Meaning | | --- | --- | | `className` | Appended last, after `table`. `TableView` builds it from the meta `styles`/`fill`/`vertical` attributes; consumer metas add `as-layout`, `no-header`, `highlight-N-last` and the sticky-column tokens through the same channel. | -| `inverted` | Dark table. Emitted as the `inverted` class, which `table.less` and `expand.less` both select on. Reached only from `ErrorTable.js` — a §9.9-H1 orphan, so the prop is kept but its fate is that deletion's to decide, not this step's. | -| `striped` | Zebra rows, emitted as the `striped` class. Same single call site as `inverted`, and also genuinely styled — which is why neither was dropped with the rest. | +| `inverted` | Dark table. Emitted as the `inverted` class, which `table.less` and `expand.less` both select on. **KEPT — decided at §9.9-H1 (2026-09-17), which is where step 1 left the call.** Its one in-repo caller was `ErrorTable.js`, and H1 deleted it, so the "Attributes at the call sites" table above no longer lists this prop — that table reports what the CODEBASE passes, not what the component accepts. The prop itself is untouched: `Table.js` still destructures it and still emits the class, the CSS still selects on it, and a consumer meta can still set it. Dropping it because our own last caller went away would have been a breaking change bought for nothing. | +| `striped` | Zebra rows, emitted as the `striped` class. Same story as `inverted` exactly: genuinely styled (`table.striped tr:nth-child(2n)`), which is why neither was dropped with the rest, and still accepted after H1 removed their shared call site. | **Consumed by every subcomponent (1).** All 6 share one implementation, so this list applies to each of them identically. @@ -108,7 +108,7 @@ CSS contract: `src/style/components/table.less` hangs EVERY cell's padding off ` **Passthrough.** `style`, `colSpan`, `scope`, `id`, `data-*` and every event handler still reach the element untouched — they always did, because Semantic did not handle them either, so they ride the rest spread exactly as before. There is no `forwardRef`: nothing in `src` passes a ref to a table element, so the parameter would have had no caller. -**Dropped (4) — the semver record.** Props semantic-ui-react handled that this implementation deliberately does not. All of them remain REACHABLE from a consumer meta: the component is rendered with open spreads (`...omitProps(props, ENGINE_PROPS, FIELD_ONLY_PROPS)`, `...props`, `...rest`), so an attribute nobody anticipated on a meta node still arrives here as a prop. That is why the component strips them explicitly and warns once per prop in development. Stripping matters because the value would otherwise reach a real element as an attribute — a string-valued one lands lowercase (`verticalAlign="top"` rendered `verticalalign="top"`) and a boolean draws React's "Received `true` for a non-boolean attribute" warning, both of them junk the DOM contract's tripwires exist to keep out. Warning matters because a meta still carrying one would otherwise never learn it stopped working, and React's own unknown-prop warning cannot be relied on: it is silent for a lowercase name. +**Dropped (4) — the semver record.** Props semantic-ui-react handled that this implementation deliberately does not. All of them remain REACHABLE from a consumer meta: the component is rendered with open spreads (`...omitProps(props, ENGINE_PROPS, FIELD_ONLY_PROPS)`, `...rest`), so an attribute nobody anticipated on a meta node still arrives here as a prop. That is why the component strips them explicitly and warns once per prop in development. Stripping matters because the value would otherwise reach a real element as an attribute — a string-valued one lands lowercase (`verticalAlign="top"` rendered `verticalalign="top"`) and a boolean draws React's "Received `true` for a non-boolean attribute" warning, both of them junk the DOM contract's tripwires exist to keep out. Warning matters because a meta still carrying one would otherwise never learn it stopped working, and React's own unknown-prop warning cannot be relied on: it is silent for a lowercase name. | Prop | Why it is gone | | --- | --- | @@ -123,12 +123,12 @@ Those four were the *published* ones — they had curated entries on this page w | Component | Attributes at the call sites | Spreads | Rendered by | | --- | --- | --- | --- | -| `Table` | `className`, `inverted`, `striped` | `...omitProps(props, ENGINE_PROPS, FIELD_ONLY_PROPS)`, `...props` | `core/components/ErrorTable.js`, `core/pages/main/components/TableView.js` | -| `Table.Header` | `className` | — | `core/components/ErrorTable.js`, `core/pages/main/components/TableView.js` | -| `Table.HeaderCell` | `className`, `colSpan`, `key`, `style` | — | `core/components/ErrorTable.js`, `core/pages/main/components/TableView.js` | -| `Table.Row` | `className`, `key` | — | `core/components/ErrorTable.js`, `core/pages/main/components/TableView.js` | -| `Table.Cell` | `className`, `colSpan`, `key`, `scope`, `style` | `...rest` | `core/components/ErrorTable.js`, `core/pages/main/components/LocalDraftTableRow.js`, `core/pages/main/components/TableView.js`, `core/pages/main/mapper.js` | -| `Table.Body` | — | — | `core/components/ErrorTable.js`, `core/pages/main/components/TableView.js` | +| `Table` | `className` | `...omitProps(props, ENGINE_PROPS, FIELD_ONLY_PROPS)` | `core/pages/main/components/TableView.js` | +| `Table.Header` | `className` | — | `core/pages/main/components/TableView.js` | +| `Table.HeaderCell` | `className`, `colSpan`, `key`, `style` | — | `core/pages/main/components/TableView.js` | +| `Table.Row` | `className`, `key` | — | `core/pages/main/components/TableView.js` | +| `Table.Cell` | `className`, `colSpan`, `key`, `style` | `...rest` | `core/pages/main/components/LocalDraftTableRow.js`, `core/pages/main/components/TableView.js`, `core/pages/main/mapper.js` | +| `Table.Body` | — | — | `core/pages/main/components/TableView.js` | | `Table.Footer` | — | — | *nothing* | `mapper.js`'s spread onto `Table.Cell` is a meta node's whole rest bag and is still unfiltered at the call site — the filter is now inside the cell, which is why it is safe. All three unfiltered boundaries on this surface are now closed inside the component: the table cell at step 1, the tooltip at step 2 part 3, and the dropdown at step 3 part 2, which strips twice — once in the wrapper and once in `Listbox` at the element. diff --git a/docs/UPGRADE-PLAN.md b/docs/UPGRADE-PLAN.md index 61aa3b9a..c7dfdc86 100644 --- a/docs/UPGRADE-PLAN.md +++ b/docs/UPGRADE-PLAN.md @@ -2,12 +2,12 @@ | | | |---|---| -| **Status** | In progress — React 17 hosted checkpoint green; Phase 0 types/CI baseline complete and packaging partially complete | +| **Status** | In progress — React 18.3 is the development and test target and the peer range admits 16.14/17/18; Phase 0 types/CI baseline complete and packaging gated in CI (`test:pack:budget`, `test:pack:consumer`, `test:pack:peers`). **Status corrected 2026-09-17:** this row read "React 17 hosted checkpoint green; Phase 0 types/CI baseline complete and packaging partially complete" and was never revised after the React 18 bump (713c1e89) or after the three pack checks became gating `ci.yml` steps. | | **Date** | 2026-07-06 | | **Re-verified** | 2026-07-21 — independent re-audit against the working tree, lockfile, build configs, a full test/lint/audit/pack run, and the npm registry; new findings indexed in §2.6 | | **Checkpoint verified** | 2026-08-07 — `master` b0b64d3: 138 suites / 1920 tests; 94.21% statements / 89.25% branches / 92.70% functions / 94.81% lines; JS/CSS lint and library/demo builds green in hosted CI | | **Current change verified** | 2026-08-10 — 138 suites / 1921 tests locally (the obsolete type-shim version-site case was removed and two `translate` boundary contracts were added); coverage thresholds, React 16/17/18 declaration matrix, JS/CSS lint and both builds green | -| **Audited version** | 0.34.2 (master checkpoint) | +| **Audited version** | 0.34.3 — `package.json`, the published `eis-ui-render` on npm and tag `v0.34.3` (2026-08-12, commit 3a5fb741, on `release/v0.34`) all agree; `master` HEAD 43acc3e3 is 41 commits past that tag and carries no tag of its own. **Figure corrected 2026-09-17:** this row read "0.34.2 (master checkpoint)", one patch behind both the tree and the registry; other "0.34.2" references in this plan are stale for the same reason. | | **Scope** | React 17/18 upgrade path, React 19 readiness, a principles-preserving modernization roadmap, the `semantic-ui-react` exit plan (§9.7-F1), the `moment` native-replacement analysis (§9.7-F2), the project-structure analysis (§9.9), the TypeScript migration (§9.6), and the consolidated verification checklist (Appendix C) | --- @@ -16,20 +16,20 @@ The audit shows that **the path to React 18 is almost entirely unblocked**. The build toolchain is already modern (webpack 5, Jest 30, Babel 7.26, Node 24), and every runtime dependency already declares React 17/18 peer support. The lag is concentrated in four places: -1. **React itself** — the React 17 checkpoint has landed on `master`; React 18 is not yet admitted by the peer range. -2. **`@testing-library/react` 12.1.5** — peer-restricted to `react <18`; the only hard dependency blocker. +1. ~~**React itself**~~ **CLOSED 2026-09-17.** The peer range admits React 18: `peerDependencies.react` and `.react-dom` are both `^16.14.0 || ^17.0.0 || ^18.0.0`, and the dev/test toolchain resolves React 18.3.1 — landed in 713c1e89 "Develop and test against React 18.3 (#31)". The finding as it stood: **React itself** — the React 17 checkpoint has landed on `master`; React 18 is not yet admitted by the peer range. +2. ~~**`@testing-library/react` 12.1.5**~~ **CLOSED 2026-09-17.** The blocker is gone: `package.json` declares `@testing-library/react` `^16.3.2` (installed 16.3.2, peers `react`/`react-dom` `^18.0.0 || ^19.0.0`) alongside `@testing-library/dom` `^10.4.1`, upgraded in the same commit as the React 18 bump (713c1e89), exactly as §6 required. The finding as it stood: **`@testing-library/react` 12.1.5** — peer-restricted to `react <18`; the only hard dependency blocker. 3. **Legacy component patterns** — 21 source files contain class components, with `UNSAFE_*` lifecycles in 13 files, including deliberate **runtime prototype patching** in `src/core/pages/main/rules.js` and `src/core/modules/form/utils.js`. These are *not* upgrade blockers (prefixed `UNSAFE_*` methods work in React 17, 18, and 19), but they block `StrictMode`, concurrent features, and long-term maintainability. -4. **The shipped artifact itself** — the public declarations now describe the actual single callable UMD/CommonJS component and are compiled against locked React 16/17/18 type environments in both interop-default and direct-CommonJS modes; hosted CI is green, and `prepack` rejects version drift before rebuilding the library. Packaging is now gated too: assets ship once, budgets and a packed-tarball consumer smoke run in CI, and the tarball is 295 files / 7.25 MB unpacked (from 579 / 11.6 MB) with source maps kept by decision. The published CSS still leaks unscoped `html`/`body`/`*` rules into host pages. §2.6 carries the evidence and remaining gates. +4. **The shipped artifact itself** — the public declarations now describe the actual single callable UMD/CommonJS component and are compiled against locked React 16/17/18 type environments in both interop-default and direct-CommonJS modes; hosted CI is green, and `prepack` rejects version drift before rebuilding the library. Packaging is now gated too: assets ship once, budgets and a packed-tarball consumer smoke run in CI, and the tarball is 295 files / 7.25 MB unpacked (from 579 / 11.6 MB) with source maps kept by decision. ~~The published CSS still leaks unscoped `html`/`body`/`*` rules into host pages.~~ **CLOSED 2026-09-17 (§9.9-H8).** `postcss.config.js` now runs `prefixwrap('.ui-render', {ignoredSelectors: [/^\.ui-render-(.+)$/]})` with no `html`/`body`/`*` exemption, and this plan's own gate command, re-measured 2026-09-17 against the real payload `static/all.css`, returns 0 matches; landed in 8d803008 "H8: stop the stylesheet leaving its wrapper (#41)". The finding as it stood: the published CSS still leaks unscoped `html`/`body`/`*` rules into host pages. §2.6 carries the evidence and remaining gates. **Recommended target: React 18.3, reached in two checkpointed releases (17 → 18), followed by an incremental modernization program.** React 19 is a watch-item, not a target — it stays gated on the §9.7-F1 exit (see the §8 fast path). Standing decisions: -1. **`semantic-ui-react` will be exited entirely** (§9.7-F1). The audited dependency surface is far smaller than the package's reputation suggests: exactly **3 wrapper components** (`Table` — **now in-house, step 1 shipped**; `TooltipPop`; `Dropdown`) and **5 curated LESS modules** — the codebase has already been trending out of it (slider lib and react-dropzone removed recently, modal and pagination already in-house). Completing the exit also removes the main external React 19 blocker. +1. **`semantic-ui-react` will be exited entirely** (§9.7-F1). The audited dependency surface is far smaller than the package's reputation suggests: exactly **3 wrapper components** (`Table`, `TooltipPop`, `Dropdown` — **all three in-house since 2026-09-08**: `Table` at step 1, `TooltipPop` at step 2 (bbf7d607), the `Dropdown`'s inner control at step 3 (bddf7935, `Dropdown.js:4` now imports `./Listbox`); this read "`Table` — now in-house, step 1 shipped; `TooltipPop`; `Dropdown`" while the last two were still outstanding) and ~~**5 curated LESS modules**~~ **2 vendored LESS files** (**corrected 2026-09-17**: three of the five were deleted outright in 9a531871, the remaining two were compiled into `src/style/vendor/semantic-{reset,dropdown}.less` in 7c0a7512, and `semantic-ui-less` left `devDependencies` in 9f393959 — it now appears nowhere in `package.json`, `package-lock.json` or `node_modules`; the figure as it stood was "5 curated LESS modules" imported from `node_modules`) — the codebase has already been trending out of it (slider lib and react-dropzone removed recently, modal and pagination already in-house). **CLOSED 2026-09-17 — the exit is complete (§9.7-F1 steps 1–4).** `semantic-ui-react` and `semantic-ui-less` are absent from `dependencies`, `devDependencies` and `node_modules`, and the only two mentions left in `src/` are doc-comment prose, so the main external React 19 blocker went with them. The decision as it stood, forward-looking: "Completing the exit also removes the main external React 19 blocker." 2. **`moment` stays** as a peer dependency — no dayjs migration. The requested native-replacement feasibility analysis (§9.7-F2) concludes it is possible and well-bounded (~400–600 lines behind an adapter seam), but it is parked behind an explicit decision gate; the only near-term action is funneling usage through a single internal adapter module. 3. **The published JavaScript API stays a single callable value.** `require('eis-ui-render')` returns the function directly (there is no runtime `.default` or `.UIRender` property); TypeScript models this with `export =`, which also supports a default import when module interop is enabled. Named interfaces and type aliases remain available through the merged namespace, but a named `UIRender` value export is not restored. Script-tag/global typing remains tied to the separate UMD-support gate in §10. -The **project-structure analysis** (§9.9) found: the documented `ui-*-pack` alias system is dead (zero imports in the codebase — CLAUDE.md was stale on this; fixed alongside this plan), the engine lives under an app-boilerplate-era `core/pages/main/` path, and 12 orphan components (9 direct, plus the `ErrorTable`/`Square`/pack-`TabList` cluster reachable only from other orphans) plus a dead `style/unused/` tree can simply be deleted. The workstream re-homes the engine to `core/engine/` ahead of the §9.3 decomposition, isolates the demo, and locks layer direction in with lint. +The **project-structure analysis** (§9.9) found: the documented `ui-*-pack` alias system is dead (zero imports in the codebase — CLAUDE.md was stale on this; fixed alongside this plan), the engine lives under an app-boilerplate-era `core/pages/main/` path, and 12 orphan components (9 direct, plus the `ErrorTable`/`Square`/pack-`TabList` cluster reachable only from other orphans) plus a dead `style/unused/` tree ~~can simply be deleted~~ **— deleted 2026-09-17 (§9.9-H1).** Those files are no longer in the tree, so the counts here are a historical record rather than files to go looking for; the engine `TabList` under `core/pages/main/` survives (the mapper resolves it) and the pack `Tabs.js` is still present, so the pack/engine duplication is only half resolved. The workstream re-homes the engine to `core/engine/` ahead of the §9.3 decomposition, isolates the demo, and locks layer direction in with lint. §9.6 defines a **full TypeScript migration** (infra step → utils/contract → components/modules riding other workstreams → engine last → guarded public-API switchover), which **retires the `prop-types` runtime dependency** as its exit criterion (E5 — React 19 ignores propTypes anyway, and the shapes currently ship in the production bundle). The plan also adds **Appendix C** — a consolidated checklist of every verification it depends on, so no check lives only in review discussions. @@ -48,9 +48,9 @@ Because `react`/`react-dom` are webpack **externals** and npm **peer dependencie | Tests | Jest 30 + `jest-environment-jsdom` 30 | ✅ current, React-18-ready | | Node | engines `>=18` (relaxed 2026-08-18, see §10), `.nvmrc` = 24 | ✅ current | | Lint | ESLint 8 + `eslint-config-react-app` 7 | ✅ `npm run lint:js` exits 0 errors / 0 warnings with `--max-warnings 0` and runs in CI; the 22 warnings were triaged in Phase 0.9 (one was a real defect, R18) and the 14 zero-reference devDependencies are gone | -| Styling | LESS 3.13 (pinned for the semantic-ui-less + `less-plugin-functions` toolchain), PostCSS 8, stylelint 16 | ✅ works; LESS pin is a separate watch-item (§9.8) | +| Styling | LESS 4 (declared `^4.4.2`, resolved 4.9.1), `less-plugin-functions` 1.0 kept deliberately via `scripts/less-plugin-compat.js`, PostCSS 8, stylelint 16 | ✅ works; ~~LESS pin is a separate watch-item (§9.8)~~ **CLOSED 2026-09-15** — unpinned in be02e239 "§9.8: unpin LESS — 3.13 → 4.x, output byte-identical (#48)". The row as it stood: "LESS 3.13 (pinned for the semantic-ui-less + `less-plugin-functions` toolchain)" — and that stated reason was itself wrong, as R8 records: the anchors were ours (`javascriptEnabled` at `_variables.less:23`, `round()` at `_variables.less:264`), not semantic's. | | Types | Hand-written public API types in `src/library/types/`, emitted via `tsconfig.build.json` (declaration-only) | ✅ describes the direct callable UMD/CommonJS export; emitted declarations compile with `skipLibCheck: false` against locked `@types/react` 16/17/18 consumers using both interop-default and direct `import = require` (§2.6-1, Phase 0.6) | -| CI / publish gates | GitHub Actions workflow for pull requests and `master` pushes | ⚠️ hosted CI is green for JS/CSS lint, coverage and both builds; `prepack` checks version sync and rebuilds the library. Pack budgets, packed-consumer smoke, asset deduplication and the source-map decision remain — Phase 0.7 | +| CI / publish gates | GitHub Actions workflow for pull requests and `master` pushes | ✅ hosted CI is green for JS/CSS lint, coverage and both builds; `prepack` checks version sync and rebuilds the library. ~~Pack budgets, packed-consumer smoke, asset deduplication and the source-map decision remain — Phase 0.7~~ **CLOSED 2026-09-17** (this verdict carried ⚠️ until then): `ci.yml` runs `test:pack:budget`, `test:pack:consumer` and `test:pack:peers` as gating steps; `scripts/check-package-budget.js` caps the `dist/static` re-export stubs (`RE_EXPORT_MAX_BYTES`) and fails on a duplicated asset; and §10 records the source-map call as "Decided (2026-08-10): ship". | ### 2.2 Dependency compatibility matrix @@ -58,7 +58,7 @@ Peer ranges verified against `package-lock.json` (resolved versions), not npm me | Package | Resolved | Declared React peer | React 17 | React 18 | Notes | |---|---|---|---|---|---| -| `semantic-ui-react` | 3.0.0-beta.2 | `^16.8 \|\| ^17 \|\| ^18` | ✅ | ✅ | Already on the 3.x line, which removed `findDOMNode`-era internals. Being on a beta is a strategic risk (§8, §11) but an upgrade *enabler* here. **Planned full exit: §9.7-F1.** | +| ~~`semantic-ui-react`~~ | **removed** | — | — | — | **CLOSED 2026-09-17 (§9.7-F1 complete).** Absent from `dependencies`, `devDependencies` and `node_modules`, and referenced in `src/` only in doc-comment prose, so the beta risk cannot be realised — the same verdict already recorded at the struck-through R2. The row as it stood: "3.0.0-beta.2 — `^16.8 \|\| ^17 \|\| ^18` — ✅ / ✅ — Already on the 3.x line, which removed `findDOMNode`-era internals. Being on a beta is a strategic risk (§8, §11) but an upgrade *enabler* here. **Planned full exit: §9.7-F1.**" | | `react-final-form` | 6.5.9 | `^16.8 \|\| ^17 \|\| ^18` | ✅ | ✅ | React 19 peers arrive in the 7.x line (with `final-form` 5.x) — §9.7-F4. | | `final-form` | 4.20.10 | — (React-free) | ✅ | ✅ | | | `react-final-form-arrays` / `final-form-arrays` | 3.1.x | analogous | ✅ | ✅ | | @@ -67,7 +67,7 @@ Peer ranges verified against `package-lock.json` (resolved versions), not npm me | `react-markdown` | 8.0.7 | `>=16` | ✅ | ✅ | Demo only. | | `react-syntax-highlighter` | 16.1.1 | — | ✅ | ✅ | Demo only. | | `react-refresh` + webpack plugin | 0.17 / 0.5.17 | — | ✅ | ✅ | Dev only. | -| **`@testing-library/react`** | **12.1.5** | **`<18.0.0`** | ✅ | ❌ | **The single hard blocker.** Requires upgrade to 16.x together with the React 18 bump (§6). | +| **`@testing-library/react`** | **16.3.2** | **`^18.0.0 \|\| ^19.0.0`** | n/a (dev-only) | ✅ | ~~**The single hard blocker.** Requires upgrade to 16.x together with the React 18 bump (§6).~~ **CLOSED 2026-09-17:** the upgrade happened exactly as this row required — 713c1e89 "Develop and test against React 18.3 (#31)" introduced `"@testing-library/react": "^16` and `"react": "^18` in the same commit, with `@testing-library/dom` `^10.4.1` alongside. RTL is a devDependency, so host support at the React 16.14/17 floors is proven by the packed-artifact matrix (`test:pack:peers`) and the `react16-floor`/`react17-floor` fixtures rather than by RTL's own peers. The row as it stood: "**12.1.5** — **`<18.0.0`** — ✅ / ❌ — **The single hard blocker.**" | | `@testing-library/jest-dom` | 6.9.1 | — | ✅ | ✅ | | | `moment` | 2.29.4 (peer `^2.29.4` + external) | — | ✅ | ✅ | In maintenance mode upstream; **decision: keep**. The compatible peer range was widened for host resolution. Moment remains part of the public API (§2.6-12); optional native replacement is analyzed in §9.7-F2. | @@ -88,13 +88,13 @@ Original audit baseline: 257 JS/JSX files (+2 TS), 76 test files. The safety/Rea **Other findings:** -- `ReactDOM.render` — **demo entry point only** (`src/main.jsx:9`). Nothing in the published library calls it. +- ~~`ReactDOM.render`~~ — **CLOSED 2026-09-17:** re-grepped, `src/` contains no `ReactDOM.render` *call* at all; the sole occurrence of the string is a comment at `src/main.jsx:9` explaining why the demo uses the React 18 root API instead (`import { createRoot } from 'react-dom/client'` at `src/main.jsx:4`, `createRoot(...).render(...)` at `:12`). Nothing in the published library calls either. The finding as it stood: "`ReactDOM.render` — **demo entry point only** (`src/main.jsx:9`). Nothing in the published library calls it." - `createPortal` — `src/core/pages/main/components/Popup.js` (fully supported in 17/18/19). - `defaultProps` on **function components** (removed in React 19): exactly **3 occurrences** — `src/core/components/TooltipPop.js:23`, `ImageSwatch.js:27`, `Image.js:27`. - `.propTypes` assigned in 33 files; `prop-types` imported by 40 (fine in 17/18; validation removed entirely in React 19 — no crash; retirement: §9.6-E5). - Legacy Babel decorators (`@babel/plugin-proposal-decorators`, `legacy: true`) used in 7 files — a single `@withTimer` decorator (`Carousel`, `Counter`, `Expand`, `ProgressBar`, `ProgressSteps`, both `Tabs`). - Direct `moment` imports in **4 files only**: `src/core/utils/time.js:1`, `src/core/components/Text.js:6`, `InputDate.js:11`, `TextDateValue.js:2`. -- Global listeners: only `window.addEventListener('pointermove'/'pointerup')` in `src/core/components/Slider.js:134–135` (native drag handling — unaffected by React 17 event-delegation changes). +- Global listeners: **inventory corrected 2026-09-17.** The earlier "only `window.addEventListener('pointermove'/'pointerup')` in `src/core/components/Slider.js:134–135`" was true when audited, but the §9.7-F1 exit added two document-level click-outside listeners that did not exist then. A re-grep of `src/core` (non-test) now returns five registrations in three files: `Slider.js:146–147` (`pointermove`/`pointerup`, same native drag handling, line numbers drifted from the recorded 134–135), `Listbox.js:206` (`mousedown`, bddf7935) and `TooltipPop.js:369–370` (`keydown`/`click`, bbf7d607). The verdict is unchanged: each registers inside a `useEffect` guarded on open state and returns a `removeEventListener` cleanup (`Listbox.js:207`, `TooltipPop.js:371–374`), and document-level click-outside still works under React 17, whose events bubble out of the root container to `document` as before. ### 2.4 Verified non-issues (explicitly checked, absent) @@ -126,16 +126,16 @@ A second audit pass — against the working tree, the build configs, a full test | 2 | **The public `dateFormat` prop is dead end-to-end.** `rules.js:328/344` passes it into ``, but `Render.js:45` destructures and discards it; nothing feeds it into `ConfigContext` (`AppProvider` receives no props; the context declares `updateConfig` while the provider exposes `setConfig`); `TextDateValue.js` ignores its own `dateFormat` parameter. Components always see the context default `'MM-DD-YYYY'`. | §9.4 | | 3 | **A per-node error boundary already exists** — `RenderClass` has `componentDidCatch` + a `Render.onError` hook (`Render.js:65–67,131`) — but the production override at `mapper.js:680` destructures `{err, errInfo}` while the boundary emits `{error, errorInfo}`, so reports carry `undefined`. | §9.4 (extend + fix, not create) | | 4 | **Engine ↔ form-modules import cycle:** `form/utils.js:13–14` imports `errorsProcessing`, `clearErrorsMap`, `formsStorage` from `pages/main/*`, while `rules.js:3` imports `storedTouched`, `withForm` from `modules/form` — in addition to the known `Text.js:4` violation. | §9.3 step 2, §9.9-H5 | -| 5 | **Module-global mutable state is wider than first catalogued:** besides `FIELD.FUNC`/`Active.translate`/`errorHandlerFunction` — `formsStorage` (`rules.js:199`, module-level `Map`), `errorsMap` (`rules.js:204`), `formInitialValues` + `storedTouched` (`form/utils.js:23–24`); and every instance renders a fixed-id `
` (`AppWrapper.js:17`) that the modal portal resolves via global `getElementById` (`Popup.js:88`) — first instance in the DOM wins. (`FIELD.METHODS`, named in an earlier review draft, does not exist.) | §9.3 step 3, R14 | -| 6 | **Form-runtime hazards (StrictMode/longevity-relevant):** `form.subscribe()` runs on **every render** with the unsubscribe discarded (`form/utils.js:382`); `setState` during render in the Dropdown field branch (`form/utils.js:199–208`); a debounced handler lives on the **prototype**, sharing one timer across instances (`form/utils.js:571`); state is mutated in place via the documented-as-mutating `set()` before `setState` (`rules.js:1108/1150/1211/1215`); `AutoSave`'s debounce is never cancelled on unmount; `autoSubmit` creates a fresh `debounce(instance.submit)` on every render pass (`mapper.js:588`). (Checked and clean: `Slider` and `@withTimer` DO clean up — only an unmount-mid-drag edge remains.) | §9.3 step 4, §7 | -| 7 | ~~**Published CSS leaks global styles.**~~ **CLOSED 2026-09-15 (§9.9-H8).** `postcss.config.js` no longer exempts anything but `.ui-render-*`, so nothing reaches a host page: measured, the host `` computes browser defaults and the declarations that used to land on it are on `.ui-render` itself. The finding as it stood: it exempted `html`, `body`, `*` from prefixwrap, so the bundled semantic reset restyled the host page. **Figures corrected 2026-08-27:** the earlier "2× `html{`, 2× `body`, 1× `*{`" came from an anchored line-start grep, which cannot work — the published CSS is minified (38 lines, ~407 KB). A selector-level parse of `static/all.css` finds **13 unscoped occurrences across 11 rules** (`body` 6, `html` 5, `*` 2) and zero `.ui-render html`. The path was also stale: since §0.7 the real file is root `static/all.css` and `dist/static/all.css` is a 130-byte `@import` re-export. The standalone `scripts/build-css.js` uses **different** prefixwrap options, and the LESS-stage Jest test (`css.compilation.test.js` — not `css-contract.test.js`, which does not exist) runs **before** PostCSS. Now gated: `src/style/__tests__/css.pipeline.parity.test.js` pins the final CSS. | §9.9-H8, §9.5, F1 step 4 | +| 5 | **Module-global mutable state is wider than first catalogued:** besides `FIELD.FUNC`/`Active.translate`/`errorHandlerFunction` — `formsStorage` (`rules.js:199`, module-level `Map`), `errorsMap` (`rules.js:204`), `formInitialValues` + `storedTouched` (`form/utils.js:23–24`); and every instance renders a fixed-id `
` (`AppWrapper.js:17`) that the modal portal resolves via global `getElementById` (`Popup.js:88`) — first instance in the DOM wins. (~~`FIELD.METHODS`, named in an earlier review draft, does not exist.~~ **Correction 2026-09-17:** it does exist, and it belongs in the inventory above — `rules.js:1109` assigns `FIELD.METHODS = this.getCalledMethod()` per instance onto the same module-level `FIELD` object (`modules/variables/fields.js:6`) as the `FIELD.FUNC` writes on the next lines, and `rules.js:1127` spreads it into `funcConfig.fieldMethods`; it was introduced in `ae72179b`, long before this plan. Same hazard class as `FIELD.FUNC`, so §9.3 step 3 / R14 cover it too.) | §9.3 step 3, R14 | +| 6 | **Form-runtime hazards (StrictMode/longevity-relevant):** `form.subscribe()` runs on **every render** with the unsubscribe discarded (`form/utils.js:382`); `setState` during render in the Dropdown field branch (`form/utils.js:199–208`); a debounced handler lives on the **prototype**, sharing one timer across instances (`form/utils.js:571`); state is mutated in place via the documented-as-mutating `set()` before `setState` (`rules.js:1108/1150/1211/1215`); `AutoSave`'s debounce is never cancelled on unmount; `autoSubmit` creates a fresh `debounce(instance.submit)` on every render pass (`mapper.js:588`). (Checked and clean: `Slider` and `@withTimer` DO clean up — ~~only an unmount-mid-drag edge remains~~ **CLOSED 2026-09-17**: re-measured, that edge is closed as well. `Slider.js:64` holds a `dragCleanupRef`, `:68–70` invokes it from an unmount-time `useEffect`, and the cleanup registered at drag start (`:145`) removes both window pointer listeners and nulls the ref (`:136–140`), so unmounting mid-drag tears the listeners down; `@withTimer` clears timers and intervals in its patched `componentWillUnmount` (`components/utils/hocs.js:35–38`).) | §9.3 step 4, §7 | +| 7 | ~~**Published CSS leaks global styles.**~~ **CLOSED 2026-09-15 (§9.9-H8).** `postcss.config.js` no longer exempts anything but `.ui-render-*`, so nothing reaches a host page: measured, the host `` computes browser defaults and the declarations that used to land on it are on `.ui-render` itself. The finding as it stood: it exempted `html`, `body`, `*` from prefixwrap, so the bundled semantic reset restyled the host page. **Figures corrected 2026-08-27:** the earlier "2× `html{`, 2× `body`, 1× `*{`" came from an anchored line-start grep, which cannot work — the published CSS is minified (38 lines, ~407 KB). A selector-level parse of `static/all.css` finds **13 unscoped occurrences across 11 rules** (`body` 6, `html` 5, `*` 2) and zero `.ui-render html`. The path was also stale: since §0.7 the real file is root `static/all.css` and `dist/static/all.css` is a 130-byte `@import` re-export. ~~The standalone `scripts/build-css.js` uses **different** prefixwrap options~~ **(CLOSED 2026-09-17)** — re-measured, the two pipelines are identical: `scripts/build-css.js` exports `PREFIX = '.ui-render'` with `PREFIXWRAP_OPTIONS = { ignoredSelectors: [/^\.ui-render-(.+)$/] }` and `postcss.config.js` calls `prefixwrap('.ui-render', {ignoredSelectors: [/^\.ui-render-(.+)$/]})`, which `css.pipeline.parity.test.js` pins with `H8_DIVERGENT_RULES = 0`. One residual remains and it is a comment, not behaviour: the JSDoc above `PREFIXWRAP_OPTIONS` still claims the options "deliberately differ from the webpack `postcss.config.js` today: that one also exempts `html`, `body` and `*`" — false as written (same item as §9.9-H8-prefixwrap), and the LESS-stage Jest test (`css.compilation.test.js` — not `css-contract.test.js`, which does not exist) runs **before** PostCSS. Now gated: `src/style/__tests__/css.pipeline.parity.test.js` pins the final CSS. | §9.9-H8, §9.5, F1 step 4 | | 8 | ~~**Packaging is unreproducible and oversized.**~~ **CLOSED:** packaging in Phase 0.7, the build divergence on 2026-09-15. Was: 579 files / **11.6 MB unpacked**, every asset duplicated between `dist/static/*` and root `static/*` (two 407 KB copies of `all.css`), no `prepack`/`prepublishOnly`, no budgets. Now 295 files / **7.25 MB unpacked** (2.53 MB packed) with assets shipped once, `prepack` guarding version drift, and both budgets and a packed-tarball smoke in CI. Source maps ship deliberately (3.0 MB — owners' call, §10). `static/semantic.css` stays a 0-byte stub for compatibility. ~~Still open: the library and watch builds disagree.~~ **CLOSED 2026-09-15.** `webpack.watch.config.mjs` is deleted and `watch-lib` runs `webpack --watch` against `webpack.library.config.mjs`, so there is one library build and watch cannot drift from it again. Measured before the fix: watch emitted `dist/static/ui-render.css` instead of the `all.css`/`font.css`/`semantic.css` set, shipped **0** `.d.ts` against `build-lib`'s 2, and wrote no root-`static/` re-exports — every one of which a consumer `yalc`-linking a pre-release would have hit. The `output.clean` collision it also had is handled by `keep: /\.d\.ts$/`. | §9.9-H7 | | 9 | **A clean checkout does not build.** `Examples.jsx:24–25` imports `src/demo/examples/input-integer_{meta,data}.json`, which exist only as untracked files in the current working tree (8 more untracked example JSONs are unreferenced). | Phase 0.8 | -| 10 | **Babel scope correction:** the demo webpack config carries its **own inline presets** (`webpack.demo.config.mjs:37`, no explicit targets → browserslist applies there), so the root `targets: {node:'current'}` problem hits the **library and watch** builds (Jest is already env-split). Fixing the root config alone leaves the demo's duplicated preset setup in place. | Phase 0.5, §9.8 | -| 11 | **Hybrid dependency model:** everything except `react`/`react-dom`/`moment` is **both** bundled into the UMD **and** declared in `dependencies` — npm hosts install full copies of the form stack, `rc-picker` etc. that the bundle never uses. **The SUIR half is closed:** `semantic-ui-react` was the entry whose own `react ≤^18` peer capped the host's React, and §9.7-F1 step 3½ removed it. The pattern itself remains for the other packages, and `react-final-form` 6.5.9 is now the only remaining `react ≤^18` peer (§9.7-F4). | §9.7-F1 step 3½ — done, §9.7-F3 gate, §8 | +| 10 | ~~**Babel scope correction:**~~ **CLOSED 2026-09-17 (Phase 0.5, §9.8-G1).** Both halves have landed. `babel.config.js` is env-split — `const isTest = process.env.NODE_ENV === 'test'`, then `["@babel/preset-env", isTest ? { targets: { node: "current" } } : {}]` — so the build branch passes no targets and `package.json`'s `browserslist` governs the library and watch builds; and `webpack.demo.config.mjs:47–57` now passes babel-loader only `plugins: isProduction ? [] : ['react-refresh/babel']` (a `presets` grep over that file returns nothing), while `webpack.library.config.mjs:53–57` is a bare `loader: 'babel-loader'` reading the same shared config. §4 row 0.5 already records this. The finding as it stood: the demo webpack config carries its **own inline presets** (`webpack.demo.config.mjs:37`, no explicit targets → browserslist applies there), so the root `targets: {node:'current'}` problem hits the **library and watch** builds (Jest is already env-split); fixing the root config alone leaves the demo's duplicated preset setup in place. | Phase 0.5, §9.8 | +| 11 | **Hybrid dependency model:** everything except `react`/`react-dom`/`moment` is **both** bundled into the UMD **and** declared in `dependencies` — npm hosts install full copies of the form stack, `rc-picker` etc. that the bundle never uses. **The SUIR half is closed:** `semantic-ui-react` was the entry whose own `react ≤^18` peer capped the host's React, and §9.7-F1 step 3½ removed it. The pattern itself remains for the other packages, and ~~`react-final-form` 6.5.9 is now the only remaining `react ≤^18` peer~~ — **inventory corrected 2026-09-17: there are two, not one.** A scan of every installed package's `peerDependencies.react` finds both `react-final-form` 6.5.9 and `react-final-form-arrays` 3.1.4 declaring the same `^16.8.0` / `^17.0.0` / `^18.0.0` cap, and both are in our own `dependencies`. Every other react peer in the tree is open-ended (`rc-picker` and the `rc-*` packages `>=16.9.0`; `react-markdown` `>=16` and `react-syntax-highlighter` `>= 0.14.0`, which are devDependencies anyway). The remediation is unchanged — §9.7-F4 bumps the final-form packages together — but the same off-by-one wording stands at §8-FP2a and in the `react-19-advisory` comment in `ci.yml`, and both need the second package added. | §9.7-F1 step 3½ — done, §9.7-F3 gate, §8 | | 12 | **moment IS on the public API.** `README:12,18` and `docs.md:24–25` document that the library "accepts `moment` instances on its API" (with cross-copy `instanceof` caveats), and rc-picker callbacks leak moment objects outward — `onSelect` ← `onCalendarChange` (`InputDate.js:96`) plus passthrough `disabledDate`/`cellRender`/… via `{...props}` (`InputDate.js:89`). The earlier "moment never crosses the public API" claim was wrong; corrected in §9.7-F2. | §9.7-F2 | | 13 | **Quality baselines measured (2026-07-21):** tests 76 suites / 1215 tests green in 8.3 s (zero snapshots); `lint:css` clean; `npx eslint src` fails (11 errors / 28 warnings; no lint script exists); `npm audit --omit=dev` **0** vulnerabilities; full `npm audit` **20** (2 critical, 7 high — dev tooling); **13 devDependencies with zero references** in code/configs: `tsconfig-paths-webpack-plugin`, `backoff`, `history`, `html-loader`, `minimist`, `path-browserify`, `postcss-scss`, `raw-loader`, `remark-loader`, `rimraf`, `sass`, `sass-loader`, `webpack-node-externals` (verify `dot-prop-immutable` too). | Phase 0.9, §9.9-H1 | -| 14 | **Orphan set is 12, not 9:** the 9 direct orphans re-confirmed, plus `ErrorTable` (imported only by orphan `ErrorContent`), `Square` (only by orphan `Carousel`), and the pack `TabList` (mapper uses the engine copy, `mapper.js:33`). ~~Engine `tester/` fixtures are referenced by nothing.~~ — the `tester/` pair has since been deleted (§9.9-H1). | §9.9-H1, §9.2 | +| 14 | ~~**Orphan set is 12, not 9:**~~ **CLOSED 2026-09-17 (§9.9-H1).** All 12 are deleted from `src/core/components/` (`Avatar`, `Badge`, `Carousel`, `Collapse`, `ErrorContent`, `ErrorTable`, `FloatNumber`, `ImageSwatch`, `MenuButton`, `Square`, `TabList`, `Tags`) — the count is a historical record now, not a set of files to look for; `ErrorContent` imported `ErrorTable`, so those two had to go together. The engine `TabList` (`src/core/pages/main/components/TabList.js`) is untouched and is still what `mapper.js:33` resolves, and the pack `Tabs.js` survives, so the pack/engine duplication is only half resolved. Deleting `ErrorTable` also removed the only in-repo call site of `Table`'s `inverted`/`striped`; the H1 decision was to **keep** both props — `Table.js` still destructures them, the CSS still selects on them, and consumer meta can still set them — they merely no longer appear in the generated "Attributes at the call sites" table in `docs/SUPPORTED-PROPS.md`, which reports what the codebase passes, not what the component accepts. The finding as it stood: the 9 direct orphans re-confirmed, plus `ErrorTable` (imported only by orphan `ErrorContent`), `Square` (only by orphan `Carousel`), and the pack `TabList` (mapper uses the engine copy, `mapper.js:33`). ~~Engine `tester/` fixtures are referenced by nothing.~~ — the `tester/` pair has since been deleted (§9.9-H1). | §9.9-H1, §9.2 | | 15 | **Hardcoded version strings** `data-version="0.34.2"` in `AppWrapper.js:10` and `types/UIRender.tsx:76` — drift on every release; should come from `package.json` at build time. | §9.9-H6 | | 16 | **One application's field names were compiled into the engine.** *Resolved.* When a popup could not resolve its `relativePath`, `rules.js` probed the consumer's data for two literal paths belonging to a single host app and adopted whichever matched. It never matched in 1928 tests — but for data that *did* carry one of those keys it bound the popup to a table the opener had nothing to do with (the two candidates were tried in order, so a row from the second table got the first), re-creating by data shape the rebinding that commit `89bac56` removed. Deleted; an unresolved scope now warns instead of guessing, and meta states the scope either by declaring the `Popup` inside the row or via `{relativePath}` in the `popupOpen` args. Three comment/JSDoc examples naming the same fields were neutralised. | §9.3 | | 17 | **The date field is selected by `type`, and its view constant is dead.** `renders.js:38-40` overrides whatever the view switch chose whenever `type === 'date'`, so a date picker is reachable from meta as `{view: 'Input', type: 'date'}` — any `view` the mapper does not claim works, which is why grepping example metas for `"view": "Date"` finds nothing. Meanwhile `FIELD.TYPE.DATE` (`modules/form/constants.js:16`) has **zero readers** — `view: 'Date'` renders the "field does not exist" placeholder. Note `FIELD.RENDER.DATE` (`variables/fields.js:47`) is a different, live thing: a read-only date formatter. Either give the view constant a branch or delete it; the two-`'Date'` split is a docs-truth trap. The rendered picker is an rc-picker text input plus a JS overlay with no native `type="date"` attribute — the reason the §5 overlay QA item exists. | §9.9-H1, §9.9-H2 | @@ -145,9 +145,9 @@ A second audit pass — against the working tree, the build configs, a full test - Finding 1: resolved — the declaration now models the direct callable UMD/CommonJS function (`export =`), with `data`/`meta` required and the remaining runtime props optional; emitted declarations compile against locked React 16/17/18 type environments in both interop and non-interop consumer modes, and the smoke verifies the actual `require()` shape. - Findings 2 and 3: resolved (2026-08-27, §9.4) — the configuration props reach `ConfigContext` and the context API name matches the provider; the render-error report carries the failing node's meta path, the sink reads the fields the boundary emits, and a documented `onError` prop is on the public API. - Finding 6: partially resolved — the Final Form wrapper now keeps one subscription per active form and unsubscribes on replacement or unmount; the remaining runtime hazards stay open. -- Finding 8: partially resolved — `prepack` rejects version drift and rebuilds the library, and the hosted CI checkout is green; pack budgets, packed-consumer smoke, duplicated assets and the source-map decision stay open. +- Finding 8: ~~partially resolved — `prepack` rejects version drift and rebuilds the library, and the hosted CI checkout is green; pack budgets, packed-consumer smoke, duplicated assets and the source-map decision stay open.~~ **CLOSED 2026-09-17 (Phase 0.7).** Re-measured against the repo: `scripts/check-package-budget.js` and `scripts/test-packed-consumer.js` both exist and both run in CI, as `test:pack:budget` and `test:pack:consumer` (`.github/workflows/ci.yml`, plus `test:pack:peers`); assets ship once, with root `static/all.css` the only real copy and `dist/static/all.css` a 130-byte `@import` re-export; and the source-map decision is recorded in §10. This 2026-08-07 bullet contradicted §1 and Phase 0.7 in this same document — the repo backs §1/0.7. - Finding 9: resolved by including both imported `input-integer_{meta,data}.json` fixtures in tracked source; the registry contract covers all 38 registered examples. -- Finding 10: partially resolved — the root Babel test/build target split landed; folding the demo's duplicated inline presets into the shared config remains open. +- Finding 10: ~~partially resolved — the root Babel test/build target split landed; folding the demo's duplicated inline presets into the shared config remains open.~~ **CLOSED 2026-09-17 (Phase 0.5, §9.8-G1).** The fold has landed: `webpack.demo.config.mjs:47–57` gives babel-loader nothing but `plugins: isProduction ? [] : ['react-refresh/babel']`, no `presets` key is left anywhere in that file, and the comment at `:50–55` records why (a loader-level preset entry replaces rather than merges with the shared `babel.config.js`, which the library build and jest also read). Same single fix as finding 10 above and §9.8-G1, and §4 row 0.5 already describes it as completed. - Finding 13: resolved — `lint:js` exits 0 errors / 0 warnings behind `--max-warnings 0` in CI, all 22 warnings were triaged individually (one real defect, R18), and all 14 zero-reference devDependencies are removed. Audit baselines re-measured 2026-08-10: prod 0, full 26; assigning the dev-tooling burn-down owner stays an H9 governance item. - Finding 15: resolved — `npm version` synchronizes the runtime/demo literals, while `prepack` rejects drift before rebuilding; the removed legacy type shim is no longer a version site. - Current hosted checkpoint: 138 suites / 1920 tests; coverage is 94.21% statements / 89.25% branches / 92.70% functions / 94.81% lines; JS/CSS lint and both builds are green (Actions run 31175926661). @@ -158,7 +158,7 @@ A second audit pass — against the working tree, the build configs, a full test ### 3.1 Why React 18 via a React 17 checkpoint -- **React 17** is a "no new features" compatibility release. For this codebase it is nearly free (§5), and shipping it as a separate release isolates the event-system changes from the React 18 changes. If a consumer reports a regression, the bisection space is halved. +- **React 17** is a "no new features" compatibility release. For this codebase it is nearly free (§5). ~~Shipping it as a separate release isolates the event-system changes from the React 18 changes. If a consumer reports a regression, the bisection space is halved.~~ **Superseded 2026-08-18 (§10), recorded here 2026-09-17:** 17 was folded into the React 18 release, so there is no separate 17 publish — see §5 and the struck §10 gate row. No 17-only tag was ever cut: `git tag --list` (42 tags) jumps v0.34.2 → v0.34.3, and v0.34.3 sits on `release/v0.34`, not on the React 17 work. The rationale above is kept because it is why the checkpoint was structured this way. - **React 18** brings the actual behavioral changes (`createRoot`, automatic batching) and forces the RTL migration. It is the real milestone. - **React 19** is deliberately out of scope for the upgrade itself (§8): the gate is the §9.7-F1 exit — once it lands, the §8 fast path applies. The `UNSAFE_`/prototype-patching engine is *not* a 19 gate; §9.3 gates StrictMode/concurrency, not the flip. @@ -167,7 +167,7 @@ A second audit pass — against the working tree, the build configs, a full test Widen, never replace: ```jsonc -// package.json (target state after Phase 2) +// package.json (target state after Phase 2 — REACHED: verified 2026-09-17, the live manifest's peerDependencies are character-for-character this block) "peerDependencies": { "moment": "^2.29.4", "react": "^16.14.0 || ^17.0.0 || ^18.0.0", @@ -178,13 +178,13 @@ Widen, never replace: - The `16.14` floor is kept intentionally: it costs nothing (the code uses only cross-compatible APIs) and lets host applications migrate on their own schedule. - `16.14` is also exactly the version where `react/jsx-runtime` was backported, which later allows enabling the automatic JSX transform (§9.8) without dropping React 16 support. - `moment` widens from `~2.29.4` to `^2.29.4`: moment 2.30.1 is the final upstream release and what fresh host installs resolve to — the tilde range makes npm ≥7 host installs fail with `ERESOLVE` against this library. -- Dev dependencies (`react`, `react-dom` in `devDependencies`) track the highest supported checkpoint: React 17 now, then React 18 in Phase 2, with compatibility smoke coverage for retained lower peer versions (§9.5). +- Dev dependencies (`react`, `react-dom` in `devDependencies`) track the highest supported checkpoint: ~~React 17 now, then React 18 in Phase 2, with compatibility smoke coverage for retained lower peer versions (§9.5)~~ — **CLOSED 2026-09-17.** The Phase 2 end is reached: `devDependencies` carry `react` and `react-dom` `^18.3.1`, and the 16.14 / 17.0.2 ends are held by the install-only fixtures `scripts/fixtures/react16-floor` and `scripts/fixtures/react17-floor` rather than by the top-level devDeps. The retained lower peers also get more than smoke coverage: `test:react16` / `test:react17` run the **whole** suite through `jest.react16.config.js` / `jest.react17.config.js` and gate CI as the `react-16-floor` and `react-17` jobs, alongside `test:pack:peers` on the packed artifact. ### 3.3 Consumer impact - **UMD / externals consumers**: React comes from the host — no bundle change at all. The upgrade only widens what hosts are allowed to provide. -- **npm consumers**: Phase 1 peer resolution accepts React 17 hosts; the Phase 2 target adds React 18. No breaking change for React 16.14 hosts. -- **Install docs**: Phase 1 now recommends React 17 while documenting continued React 16.14 support; Phase 2 must update the recommended install commands again for React 18. +- **npm consumers**: ~~Phase 1 peer resolution accepts React 17 hosts; the Phase 2 target adds React 18.~~ **CLOSED 2026-09-17: the Phase 2 target is live.** `peerDependencies` already read `^16.14.0 || ^17.0.0 || ^18.0.0`, and resolution across the declared range is proven on the published artifact by `test:pack:peers` (`scripts/test-packed-react-matrix.js`, `DECLARED_RANGE = ['16.14.0', '17.0.2']`), which gates CI as "Smoke the packed tarball across the declared peer range". No breaking change for React 16.14 hosts. +- **Install docs**: ~~Phase 1 now recommends React 17 while documenting continued React 16.14 support; Phase 2 must update the recommended install commands again for React 18.~~ **CLOSED 2026-09-17 — already done.** `README.md:43` / `README.md:50` and `src/demo/markdowns/docs.md:35` / `:39` recommend `react@^18.0.0 react-dom@^18.0.0 moment@^2.29.4`; both peer tables (`README.md:36-37`, `docs.md:15-16`) carry `^16.14.0 || ^17.0.0 || ^18.0.0`, matching `package.json` exactly, and both files still state that 16.14 and 17 hosts remain supported (`README.md:53`). Consistent with §6 step 8 and Appendix C, which already record this. - **Hybrid dependency model (§2.6-11):** npm hosts currently install `dependencies` (SUIR, the form stack, rc-picker, …) that are *also* bundled into the UMD — dead weight in host `node_modules` and an extra peer-resolution surface. Resolving this (trim `dependencies`, externalize more, or peerize) is an owners' decision — see the gates in §10. - ~~**`engines.node >= 22` ships to consumers**~~ — **DECIDED and shipped (2026-08-18): relaxed to `>=18`.** The manifest now reads `>=18`, describing consumption of the prebuilt bundle rather than building the repo; development still uses `.nvmrc`. The finding as it stood: on `engine-strict` hosts the `>=22` floor failed installs even though Node is only a *build* requirement for this browser library. @@ -198,19 +198,19 @@ Widen, never replace: |---|---|---| | 0.1 | Record the green baseline | The original 76-suite baseline is recorded; the current hosted checkpoint runs 138 suites plus both builds, with counts and coverage retained for later comparison. | | 0.2 | Close test gaps around `rules.js` critical flows | Priority order: initial data processing / normalization (`utils.js` error mapping), `showIf` evaluation, validation + error propagation into fields, actions (`submit` payload assembly, `addData` / `removeData`, upload/download), re-render on `data` prop change. These are exactly the paths sensitive to React 18 batching. | -| 0.3 | Example smoke harness | A Jest suite that mounts **every** meta/data pair from `src/demo/examples/` and asserts render without throwing. This doubles as the seed for contract tests (§9.5). | +| 0.3 | Example smoke harness | A Jest suite that mounts **every** meta/data pair from `src/demo/examples/` and asserts render without throwing. This doubles as the seed for contract tests (§9.5). **Both §9.5 layers have since shipped (2026-08-27):** layer 1, the full-DOM snapshots in `src/demo/examples/__tests__/examples.dom-contract.test.js`, and layer 2, `examples.behavior-contract.test.js` plus `UIRender.listbox-behavior` / `UIRender.overlay-behavior` / `UIRender.table-behavior` over the shared harness `src/demo/testing/mountExample.js` — see §9.5, which records both. | | 0.4 | CI on every PR | JS/CSS lint, coverage, `build-lib` and the demo build run with the same scripts used locally. The hosted `master` run is green. | | 0.5 | Babel targets env-split | **Completed.** The root config splits test (`node: current`) from build (browserslist) targets, and the demo no longer carries its own copy: `webpack.demo.config.mjs` keeps only `react-refresh/babel` in development and takes presets plus legacy decorators from `babel.config.js`, the same config the library build and jest use. Measured with `loadPartialConfig`, the inline entry was not merely duplicated — a loader-level preset **replaces** the shared entry for the same identifier, so the demo was silently discarding the config's own preset-env options. Proof the consolidation is inert: the emitted demo bundles are byte-for-byte identical before and after, and they retain arrow functions, optional chaining, `const`/`let` and `class`, which only survive because browserslist targets (chrome 103 … safari 18.5) are in effect — with no targets, preset-env would down-level everything to ES5. Closes R6 and §2.6-10. | | 0.6 | Public API & types baseline | **Completed.** The legacy `window._mountUIRender` class shim was replaced by declarations for the actual direct callable UMD/CommonJS function (§2.6-1): `data`/`meta` are required, runtime-supported props are optional, `translate` keeps its string-to-string localization contract while non-string renderer values bypass it unchanged, and no nonexistent instance/ref API, `.default` property or named value export is promised. The CI workflow compiles emitted `dist/*.d.ts` with `skipLibCheck: false` in isolated consumers against locked `@types/react` 16, 17 and 18 (+19 at the flip), using both interop-default and direct-CommonJS imports; it also verifies the built runtime export. The matrix is green locally in this change. **This corrected contract is the golden baseline for §9.6-E4.** Closes the types half of R15. | -| 0.7 | Packaging gate | **Completed.** `npm version` synchronizes runtime/demo version literals; `prepack` rejects version drift and runs `build-lib` including `gen-ts`. Assets now ship **once**: the root `static/` payload holds the real stylesheets, fonts and images (it is what hosts copy to their web root, since `FILE.PATH_IMAGES` resolves to `/static/images/`), and `dist/static/*.css` are `@import` re-exports so bundler imports of the dist path keep resolving. `test:pack:budget` enforces file count, unpacked/packed size, per-file caps, required paths, the re-export stubs and a duplicate-asset guard against the published manifest; `test:pack:consumer` packs, extracts and server-renders the tarball in a throwaway consumer holding only react/react-dom/moment, resolving every stylesheet `@import` chain and `url()` target. Both run in CI. Measured: **295 files / 7.25 MB unpacked / 2.53 MB packed**, down from 579 / 11.6 MB. Source maps ship by decision (§10). Closes the remaining packaging half of R15. | +| 0.7 | Packaging gate | **Completed.** `npm version` synchronizes runtime/demo version literals; `prepack` rejects version drift and runs `build-lib` including `gen-ts`. Assets now ship **once**: the root `static/` payload holds the real stylesheets, fonts and images (it is what hosts copy to their web root, since `FILE.PATH_IMAGES` resolves to `/static/images/`), and `dist/static/*.css` are `@import` re-exports so bundler imports of the dist path keep resolving. `test:pack:budget` enforces file count, unpacked/packed size, per-file caps, required paths, the re-export stubs and a duplicate-asset guard against the published manifest; `test:pack:consumer` packs, extracts and server-renders the tarball in a throwaway consumer holding only react/react-dom/moment, resolving every stylesheet `@import` chain and `url()` target. Both run in CI. **Figures corrected 2026-09-17:** the earlier measurement read "295 files / 7.25 MB unpacked / 2.53 MB packed". Re-measured 2026-09-17 with `npm run test:pack:budget`, the gate reports **299 files / 6.44 MB unpacked / 2.39 MB packed** and passes on all three budgets (299/330 files, 6.44/8.00 MB unpacked, 2.39/3.00 MB packed) — the file count rose by four while the unpacked payload fell, still down from the 579 / 11.6 MB starting point. Source maps ship by decision (§10). Closes the remaining packaging half of R15. | | 0.8 | Repo completeness | Everything imported by tracked code must be tracked. The two `input-integer` fixtures imported by `Examples.jsx:24–25` are included in the current checkpoint; the fresh hosted checkout now makes future tracked-file drift fail fast. | | 0.9 | Lint & security baseline | **Completed.** `lint:js` now exits **0 errors / 0 warnings** and carries `--max-warnings 0`, so a new warning fails CI. All 22 warnings were triaged individually, not blanket-silenced: **18 were genuine cleanups** (dead destructures and imports, a misplaced `eslint-disable` that silenced nothing, explicit `return undefined` in two sentinel getters, a useless escape and rename, `default-case` answered with the anchored `// no default` hatch that `eslint-config-react-app` actually looks for, and `alt` restated after the spread so the a11y rule stays live on that line); **4 are documented suppressions** where the rule is wrong or the fix is riskier than the warning (two `exhaustive-deps` guard reads that would clobber user input if listed, the deliberate loose comparison in `hasObjKeys`, and the `no-loop-func` renderer closure). Three warnings turned out to be **real defects** — see §11 R18. Audit baselines re-measured on 2026-08-10: `npm audit --omit=dev` **0**, full audit **26** (2 critical, 12 high — all dev tooling; was 20 on 2026-07-21, and the burn-down owner is still an H9 governance item). All **14** zero-reference devDependencies removed (the 13 plus the unverified `dot-prop-immutable`), dropping the installed dev tree from 1443 to 1429 packages. | **Implementation status (2026-08-10):** - Completed: **all of Phase 0 (0.1–0.9)** — automated baseline and critical-flow contracts (0.1–0.4); Babel targets consolidated into one shared config (0.5); truthful callable-export public types plus the locked React 16/17/18 interop/CommonJS consumer matrix (0.6); asset deduplication, pack budgets and the packed-tarball consumer smoke, with source maps kept by decision (0.7); repo completeness for the imported input-integer fixtures (0.8); a zero-warning lint gate, re-measured audit baselines and the 14-package devDependency sweep (0.9). -- Open decisions carried forward (owners' calls, not blockers): `engines.node >= 22` in the published manifest; whether the four demo screenshots in `static/images` (~1.1 MB of the 7.25 MB) belong in the tarball, since only unpublished demo markdowns reference them but consumer meta could name any file in that folder; and the burn-down owner for the 26 dev-tooling audit findings. All three sit in the §10 gate table. -- Deferred by design, with the evidence now recorded: the `no-loop-func` renderer closure in `transforms.js` shares one `_data` binding across every renderer it builds (§9.3), and the library and watch builds still disagree (§9.9-H7). +- Open decisions carried forward (owners' calls, not blockers): ~~`engines.node >= 22` in the published manifest~~ — **CLOSED 2026-08-18 (recorded here 2026-09-17): decided and shipped, relaxed to `>=18`**; `package.json` now reads `"engines": { "node": ">=18" }`, the §10 gate row is struck and §3.3 above records the same; whether the four demo screenshots in `static/images` (~1.1 MB of the 7.25 MB) belong in the tarball, since only unpublished demo markdowns reference them but consumer meta could name any file in that folder; and the burn-down owner for the 26 dev-tooling audit findings. All three sit in the §10 gate table. +- Deferred by design, with the evidence now recorded: the `no-loop-func` renderer closure in `transforms.js` shares one `_data` binding across every renderer it builds (§9.3), and ~~the library and watch builds still disagree (§9.9-H7)~~ — **CLOSED 2026-09-17 (§9.9-H7).** They can no longer disagree: `package.json` now defines `"watch-lib": "npm run gen-ts && webpack --watch --mode production --config webpack.library.config.mjs"`, the same config file `build-lib` uses, and `webpack.watch.config.mjs` no longer exists — `git log --diff-filter=D` shows it deleted in 95268a4e ("fix watch-lib: same config as the published build (#49)"). **Exit criteria:** CI green on the React 17 baseline **from a clean checkout**; `rules.js` critical flows covered; example smoke harness in place; Babel build targets honor browserslist; published types describe the real component and compile against `@types/react` 16/17/18; `prepack` + pack budgets enforced; `lint` script green; audit baselines recorded. **All met** — Phase 0 is closed; the outstanding items are the owners' decisions listed above, not engineering work. @@ -227,11 +227,11 @@ Widen, never replace: 1. `npm i -D react@17.0.2 react-dom@17.0.2` (dev deps only). 2. Peers → `"react": "^16.14.0 || ^17.0.0"` (same for `react-dom`); widen `moment` to `^2.29.4` (§3.2). 3. Full test run + example smoke + manual demo QA. -4. Update install docs; changelog entry; ship as a checkpoint release. +4. ~~Update install docs; changelog entry; ship as a checkpoint release.~~ **CLOSED 2026-09-17.** Install docs updated (`README.md:43,50,53`; `src/demo/markdowns/docs.md:35,39,42`); the changelog entry landed in `src/demo/markdowns/changelog.md` under "#### Compatibility" (line 295 onward: "host applications may use React 16.14, 17 or 18. Hosts on 16 or 17 need change nothing") — there is deliberately no root `CHANGELOG.md`, per the §10 gate; and ~~ship as a checkpoint release~~ is struck by the §10 decision of 2026-08-18, under which 17 ships as part of the React 18 release (see the exit criteria below). -`@testing-library/react` 12 stays (its `react <18` peer admits 17). `ReactDOM.render` in the demo stays (fully supported in 17). +~~`@testing-library/react` 12 stays (its `react <18` peer admits 17). `ReactDOM.render` in the demo stays (fully supported in 17).~~ **Superseded by Phase 2 (§6), recorded 2026-09-17.** Both were the right Phase 1 calls and have since been executed past: `package.json` now carries `@testing-library/react` `^16.3.2`, and `src/main.jsx:4,12` mounts through `createRoot` rather than `ReactDOM.render`. -**Automated checkpoint (2026-08-07):** React/React DOM 17.0.2, additive React 16.14/17 peer ranges and Moment `^2.29.4` are on `master`. Hosted CI is green: 138 suites / 1920 tests; coverage is 94.21% statements / 89.25% branches / 92.70% functions / 94.81% lines; JS/CSS lint and both builds pass. The manual QA checklist below is now worked through in a real browser, including the overlay-ordering items; react-refresh, the yalc smoke and the release decision remain open. +**Automated checkpoint (2026-08-07):** React/React DOM 17.0.2, additive React 16.14/17 peer ranges and Moment `^2.29.4` are on `master`. Hosted CI is green: 138 suites / 1920 tests; coverage is 94.21% statements / 89.25% branches / 92.70% functions / 94.81% lines; JS/CSS lint and both builds pass. The manual QA checklist below is now worked through in a real browser, including the overlay-ordering items; ~~react-refresh, the yalc smoke and the release decision remain open.~~ **Corrected 2026-09-17:** only the react-refresh check is still open (Appendix C still shows it ☐). The yalc smoke is satisfied by `test:pack:consumer`, which packs the tarball, copies it into an isolated consumer under the OS temp directory with only react, react-dom and moment linked in, and server-renders it; it gates CI as "Smoke the packed tarball" — the same substitution the exit criteria below already record. The release decision was taken on 2026-08-18 (§10): 17 is folded into the React 18 release, so no separate 17 publish was ever due. ### React 17 behavioral changes, mapped to this codebase @@ -251,18 +251,22 @@ container while the bundled dependencies keep their own native `document` listen `@semantic-ui-react/event-stack`, rc-picker via `rc-util`), and click-outside logic depends on that order. - [x] **Popup open + click-outside close** — portal-based modal with dimmer opens from `popupContent`, - closes on a dimmer click, dimmer removed. *Tooltip hover-open not exercised — small remaining gap.* + closes on a dimmer click, dimmer removed. ~~*Tooltip hover-open not exercised — small remaining gap.*~~ **CLOSED 2026-09-17.** Hover-open is now covered in both legs: jsdom in `src/core/components/__tests__/TooltipPop.behavior.test.js`, and real Chromium hover in `e2e/harness.tooltip.pw.js` and `e2e/corpus.tooltip.pw.js` (with `e2e/tooltip.touch.pw.js` for the touch path); the markup side is pinned by `src/style/__tests__/css.tooltip-contract.test.js`. - [x] **Dropdown open/select/close, click-outside close** — selecting changes the value; a click outside - closes the menu **without** committing a selection. *Multi-select has no demo example (`multiple: true` - appears only in the upload variants), so it stays covered by the jsdom contracts only.* + closes the menu **without** committing a selection. ~~*Multi-select has no demo example (`multiple: true` + appears only in the upload variants), so it stays covered by the jsdom contracts only.*~~ **CLOSED 2026-09-17 — the gap is moot, not worked:** `multiple` was REMOVED from the public API by §9.7-F1 step 3. `src/core/components/Dropdown.js` lists it in `DROPPED_PROPS` alongside `search` and `allowAdditions`, `src/core/components/Listbox.js` records the same removal, and §9.7-F1 (plan line 723) documents the maintainers' decision and the evidence behind it — so there is no multi-select behavior left for a demo example to exercise. - [x] **Date/time picker (rc-picker overlay)** — the picker lives in the `tableForm` example (`src/demo/examples/data_component.js`). Overlay opens on focus, a day click writes the formatted value, and a click outside closes the overlay. Selected by `type: 'date'`, **not** by a `view` string (§2.6-17); the rendered DOM is an rc-picker text input plus a JS overlay with no native `type="date"` attribute, which is precisely why this item mattered for React 17. - [x] **Tabs, Collapse, Expand** — tab switch swaps panel content; example rows expand and collapse. - *Carousel autoplay is moot: `Carousel` is not registered in `mapper.js` and is one of the §2.6-14 - orphans slated for deletion under §9.9-H1, so no meta can render it.* + ~~*Carousel autoplay is moot: `Carousel` is not registered in `mapper.js` and is one of the §2.6-14 + orphans slated for deletion under §9.9-H1, so no meta can render it.*~~ **CLOSED 2026-09-17.** The + deletion landed: `src/core/components/Carousel.js` is gone — §9.9-H1 removed all 12 §2.6-14 orphans — + so there is no Carousel left to exercise. The pack `Collapse` (`src/core/components/Collapse.js`) went + in the same batch; it was never registered in `mapper.js` either, and the expand/collapse behaviour + verified here is the live `Expand` / `ExpandList` path, which the deletion did not touch. - [x] **Table: sorting, pagination, inline edit rows** — sorting cycles asc → unsorted → desc on a header whose table declares `sorts` (`TableView.js` gives a header `onClick` only then; the `adminCosts` table inside the `all` example's Admin Expenses section is the one that does), verified by both the @@ -278,8 +282,8 @@ container while the bundled dependencies keep their own native `document` listen pass surfaced: `currencyCode`/`onDataChanged` reaching the DOM through SUIR's Dropdown, and react-markdown's `inline` prop reaching `` in two demo components. -**Not closable from the demo, needs an owner:** the yalc smoke in a consuming application, a react-refresh -check, and the §10 decision on whether React 17 ships as its own release. +**Not closable from the demo, needs an owner:** ~~the yalc smoke in a consuming application~~, a react-refresh +check, ~~and the §10 decision on whether React 17 ships as its own release~~. **Two of the three are CLOSED (recorded 2026-09-17):** the yalc smoke was satisfied by `test:pack:consumer`, which the exit criteria below already record as the equivalent-and-stronger evidence, and the §10 decision was taken on 2026-08-18 — 17 ships folded into the React 18 release. Only the react-refresh check still needs an owner (Appendix C ☐). **Exit criteria — met (2026-08-20).** CI green; QA checklist worked through in a real browser (above); `dist/` builds. ~~release published~~ struck by the §10 decision: 17 ships as part of the React 18 release, so nothing is published at the end of this phase. ~~yalc smoke in a consuming app~~ satisfied by equivalent and stronger evidence, no separate host required: `test:pack:consumer` installs the packed artifact into an isolated consumer that has only the three peers available and server-renders it, and the demo on localhost exercises the same `dist/` through the real engine. The original criterion already said "if one is available"; recording what was actually run instead of leaving a box unticked. **Phase 1 is closed.** @@ -327,7 +331,7 @@ check, and the §10 decision on whether React 17 ships as its own release. - **Steps 1, 3-5 landed together** (they cannot be split — RTL ≥13 requires React ≥18): React and React DOM 18.3.1, RTL 16 with an explicit `@testing-library/dom` 10 peer, `@types/react`/`@types/react-dom` 18, peers widened to `^16.14.0 || ^17.0.0 || ^18.0.0`. The type-consumer matrix was repointed — `@types/react` is 18 now, so the 16 and 17 slots come from locked aliases — and all six interop/CommonJS combinations still pass. - **The RTL 12→16 migration cost two test files, not the 1-2 weeks budgeted here.** Both failures shared the predicted cause: under `createRoot` a state update outside `act()` is no longer flushed before the assertion. `ProgressBar` drove its own `setTimeout` via `jest.runAllTimers()`; `Slider` dispatched a raw `MouseEvent` to reach `onPointerDown`. No product code was involved. **140 suites / 1946 tests, zero `act` warnings.** -- **Step 1's `defaultProps` prediction was right, and scanning jest output would have missed it:** the suites mock `console.error`, so the warnings were swallowed and the output looked clean. A probe with a recording spy showed React 18.3 warns for both `TooltipPop` and `Image`; both moved to default parameters (`Image` now forwards `decoding`/`loading` explicitly). `ImageSwatch` keeps its `defaultProps` — it is an orphan nothing renders, so it cannot warn; deletion is §9.9-H1's job. One test asserted `TooltipPop.defaultProps.delay`, i.e. the mechanism rather than the behaviour, and was rewritten against what Semantic actually receives. +- **Step 1's `defaultProps` prediction was right, and scanning jest output would have missed it:** the suites mock `console.error`, so the warnings were swallowed and the output looked clean. A probe with a recording spy showed React 18.3 warns for both `TooltipPop` and `Image`; both moved to default parameters (`Image` now forwards `decoding`/`loading` explicitly). ~~`ImageSwatch` keeps its `defaultProps` — it is an orphan nothing renders, so it cannot warn; deletion is §9.9-H1's job.~~ **CLOSED 2026-09-17.** §9.9-H1 deleted `src/core/components/ImageSwatch.js`, so that third `defaultProps` site was resolved by deletion rather than conversion; the sentence as it stood on 2026-08-20 is struck through. One test asserted `TooltipPop.defaultProps.delay`, i.e. the mechanism rather than the behaviour, and was rewritten against what Semantic actually receives. - **Step 2 done:** `src/main.jsx` mounts via `createRoot`, so the demo renders with automatic batching rather than 17-compat mode. - **Step 6, the batching pass, found no regression.** The unit suite now runs under `createRoot` (batching in effect) and is green. In the browser, on the demo: the form flow — date picker overlay, validation clearing, both submit branches, `addData` committing a row while the draft clears, `removeData` taking the right row — all behave as they did on 17; and the cascading Select, the most batching-sensitive path in the library because its reset calls `onChange` from inside an effect, correctly reset Product from `Alpha` to `Delta` when Category changed, with the dependent table following. No console errors. `flushSync` was not needed anywhere. - **Step 7:** the risky interactive views (Dropdown, Popup, rc-picker, Tabs, Table sorting/pagination/inline edit) were exercised on 18 through the §5 checklist. The two console warnings the demo does emit are React Router v6 future-flag notices — demo-only, unrelated to React 18. @@ -348,15 +352,15 @@ check, and the §10 decision on whether React 17 ships as its own release. *Console volume is small and misleading.* React aggregates per lifecycle kind and dedupes per component, so the demo produced **one** warning (`Expand, Tabs`) and a form-heavy meta produced **three** (one per lifecycle: `UNSAFE_componentWillReceiveProps` naming `Expand, InputNative, TableView, Tabs, UIRender, WithForm`; `UNSAFE_componentWillMount` and `UNSAFE_componentWillUpdate` naming `UIRender`). The count to plan against is the components, not the messages. -*The work item is 29 `UNSAFE_*` call sites across 14 files*, one of which (`Carousel`) is an orphan that cannot warn and is deleted under §9.9-H1: +~~*The work item is 29 `UNSAFE_*` call sites across 14 files*, one of which (`Carousel`) is an orphan that cannot warn and is deleted under §9.9-H1:~~ **UPDATED 2026-09-17** — §9.9-H1 landed, so the two orphan rows below no longer exist in the tree. *The work item is now 26 `UNSAFE_*` call sites across 12 files* (`Carousel`'s 1 site and `Collapse`'s 2 went with the deleted files; the pre-deletion count is struck through above): | Sites | File | Note | |---|---|---| | 12 | `pages/main/rules.js` | the engine, including the prototype-patching machinery — §9.3 step 5 is exactly this | | 4 | `modules/form/utils.js` | `WithForm` plus patched lifecycles | -| 2 | `components/Collapse.js` | | +| ~~2~~ 0 | ~~`components/Collapse.js`~~ | **DELETED 2026-09-17** under §9.9-H1 — orphan, never registered in `mapper.js`; these 2 sites no longer exist. | | 1 each | `pages/main/components/{Tabs,TableView}.js`, `modules/form/views/AutoSave.js`, `components/utils/interactions.js`, `components/{Tabs,ProgressSteps,ProgressBar,InputNative,Expand,Counter}.js` | leaf props→state derivations, mechanically convertible (§2.3) | -| 1 | `components/Carousel.js` | orphan — not registered in `mapper.js`, deleted under H1 | +| ~~1~~ 0 | ~~`components/Carousel.js`~~ | orphan — not registered in `mapper.js`; **DELETED 2026-09-17** under §9.9-H1, so this site no longer exists. | Six of those components were observed warning in practice (`Expand`, `Tabs`, `InputNative`, `TableView`, `UIRender`, `WithForm`); the rest are live code that simply needs the right prop change to surface. Outside StrictMode, React 18.3 emits **zero** warnings for any of them — the renamed `UNSAFE_*` forms are fully supported, which is why the upgrade did not need this work. @@ -372,9 +376,9 @@ Sequencing: StrictMode becomes the *acceptance criterion* of workstream §9.3 (e | Legacy context (removed in 19) | ✅ none | Zero `contextTypes` declarations. **Observed side effect of the removal (2026-08-20):** React 18 invoked every function component as `Component(props, secondArg)`, where `secondArg` was the frozen `emptyContextObject` (`{}`) returned by `getMaskedContext`; 19 passes literal `void 0`. Impact was **test-only** — four assertion sites across three test files pinned that second argument with a trailing `expect.anything()` (the other two of the five suites failing on 19 were the warning-text ones in the row below). All are now version-agnostic: assert `mock.calls[0][0]`, never the positional pair, because `toHaveBeenCalledWith(props)` fails on **arity** against a real `(props, undefined)` call. No `src/` code reads a second parameter, and the `key={typeof index !== 'object' ? index : undefined}` guard at `src/core/ui-render/Render.js:47` exists precisely to keep that legacy `{}` out of the `key` slot when React (rather than a mapper) calls `Render`. | | `findDOMNode` (removed in 19) | ✅ none in `src/` | Moot for SUIR once §9.7-F1 steps 1–3 land. | | `ReactDOM.render` (removed in 19) | 🔶 fixed by Phase 2 | Demo entry only. | -| `defaultProps` on function components (ignored in 19) | 🔶 3 occurrences | `TooltipPop.js:23`, `ImageSwatch.js:27`, `Image.js:27` → convert to default parameters (done in Phase 2 — §6 step 1). `ImageSwatch` is an orphan slated for deletion (§9.9-H1). | +| `defaultProps` on function components (ignored in 19) | ~~🔶 3 occurrences~~ ☑ **none left — CLOSED 2026-09-17** | `TooltipPop.js:23`, `ImageSwatch.js:27`, `Image.js:27` → convert to default parameters (done in Phase 2 — §6 step 1). ~~`ImageSwatch` is an orphan slated for deletion (§9.9-H1).~~ **CLOSED 2026-09-17:** `TooltipPop` and `Image` were converted in Phase 2 and `src/core/components/ImageSwatch.js` was deleted by §9.9-H1, so none of the three sites survive — the file list above is the historical record of where they were. | | `propTypes` (validation removed entirely in 19) | 🔶 40 importing files | No crash — silently ignored. Fully resolved by §9.6-E5: propTypes are deleted per TS conversion and the `prop-types` dependency is removed at the end. **Observed on 19:** the two `is marked as required` warnings that 18 emits for `formProps` and `instance` (attached by `withFormSetup`, `src/core/modules/form/utils.js:483–484`) simply vanish. They report a **real unmet prop contract** that 19 silences — the disappearance is loss of a diagnostic, not a fix, so E5 must not read it as the contract being met. | -| Dev warning text changed in 19 (no component stack, no `Warning:` prefix) | 🔶 **new finding** | React 18's dev `printWarning` appended `getStackAddendum()` as an extra `%s` argument and prefixed `"Warning: "`; 19 emits the bare message. Consequence: any assertion — or console-probe allowlist — that matches a **component name or file path inside warning text** changes meaning across versions and is unreliable as a gate; match the message body only. This is the same recording-spy technique that caught the `defaultProps` warnings in Phase 2 (§6 step 1 notes), so it applies to the audit method, not just to committed tests. | +| Dev warning text changed in 19 (no component stack, no `Warning:` prefix) | ~~🔶 **new finding**~~ ☑ **CLOSED 2026-09-17.** Match-on-message-body is implemented and documented at both console probes: `src/demo/pages/__tests__/Examples.registry-and-rendering.test.js` (comment above `EXPECTED_CONSOLE_ERROR_PATTERNS`; all 9 allowlist entries are message-body fragments) and `src/demo/pages/__tests__/NestedDataKind.interactions.test.js`. | React 18's dev `printWarning` appended `getStackAddendum()` as an extra `%s` argument and prefixed `"Warning: "`; 19 emits the bare message. Consequence: any assertion — or console-probe allowlist — that matches a **component name or file path inside warning text** changes meaning across versions and is unreliable as a gate; match the message body only. This is the same recording-spy technique that caught the `defaultProps` warnings in Phase 2 (§6 step 1 notes), so it applies to the audit method, not just to committed tests. | | `UNSAFE_*` lifecycles | ✅ still supported in 19 | But StrictMode-hostile; §9.3 is the prerequisite for StrictMode/concurrent adoption on 19, not for the 19 flip itself. | | **`semantic-ui-react` React 19 support** | ❌ **external blocker** | npm `latest` is still 2.1.5 (~3 years stale); the 3.x line lives as betas; React 19 compatibility is an open upstream issue ([Semantic-Org/Semantic-UI-React#4510](https://github.com/Semantic-Org/Semantic-UI-React/issues/4510)). **RESOLVED — the exit shipped (§9.7-F1).** `Table`, `TooltipPop` and `Dropdown` are in-house, no file in `src` references the package, and step 3½ removed the dependency entry, which is the half that mattered for hosts: SUIR's own peers capped host React while it remained (§2.6-11). Independent of upstream, which never fixed it. The `react-19-advisory` job has been green since the swap; it stays advisory until the phase that owns React 19 widens our own peer range. | | **Form-stack peers stop at `^18`** (installed: `react-final-form` 6.5.9, `final-form` 4.20.10, `*-arrays`) | 🔶 **resolved upstream, bump planned** | The 19-ready line exists: `react-final-form` 7.0.1 + `final-form` 5.0.1 + `final-form-arrays` 4.0.1 + `react-final-form-arrays` 5.0.0 declare `react … \|\| ^19` (see [react-final-form#1043](https://github.com/final-form/react-final-form/issues/1043)). Coordinated 4-package major bump — plan in §9.7-F4; best executed while still on React 18. | @@ -391,9 +395,9 @@ Sequencing: StrictMode becomes the *acceptance criterion* of workstream §9.3 (e **Fast path to 19.** The earliest viable point is **right after Phase 5a** (SUIR JS exit), without waiting for Phase 6: 1. ~~Phase 5a complete **including the removal of `semantic-ui-react` from `dependencies`**~~ — **DONE.** F1 steps 1-3 took `Table`, `TooltipPop` and `Dropdown` in-house and step 3½ removed the dependency entry, which is the half that caps hosts (the npm peer chain, not the bundled code, §2.6-11). Gate 1 of this fast path is met, and `react-19-advisory` has been green since the swap; -2. form-stack major bump done (§9.7-F4 — can run in parallel any time after Phase 3). **Gates 1 and 2 are coupled, not independent:** `react-final-form` 6.5.9 declares `react ^16.8.0 || ^17.0.0 || ^18.0.0` itself, what was a **second host cap alongside SUIR's** and is now the ONLY one — declaring `^19` while it is installed publishes a peer set npm cannot satisfy. The rff 7 bump is therefore a hard prerequisite of the flip, at the behavioral cost measured in §9.7-F4; +2. form-stack major bump done (§9.7-F4 — can run in parallel any time after Phase 3). **Gates 1 and 2 are coupled, not independent:** `react-final-form` 6.5.9 declares `react ^16.8.0 || ^17.0.0 || ^18.0.0` itself, what was a **second host cap alongside SUIR's** and is now one of the two that remain (**inventory corrected 2026-09-17:** this read "and is now the ONLY one". Re-measured installed `peerDependencies.react`: `react-final-form-arrays` 3.1.4 sits in `dependencies` too and declares the same `^16.8.0 || ^17.0.0 || ^18.0.0`, so **two** installed packages cap a host's React, not one — the `react-19-advisory` comment in `.github/workflows/ci.yml` repeats the same "one cap left" error. The coupling and the conclusion are unaffected, since `react-final-form-arrays` 5.0.0 is part of the same coordinated bump) — declaring `^19` while it is installed publishes a peer set npm cannot satisfy. The rff 7 bump is therefore a hard prerequisite of the flip, at the behavioral cost measured in §9.7-F4; 3. `defaultProps` sites fixed (Phase 2) and automatic JSX runtime enabled (Phase 4); -4. bump `@testing-library/react` to ≥16.1 (the first RTL with `react ^19` peers), add `react@19` to the dev/CI matrix, run the full regression (contract suite + example QA), then widen peers **additively**: `^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0` — the floor stays, hosts on older React are unaffected. +4. ~~bump `@testing-library/react` to ≥16.1 (the first RTL with `react ^19` peers)~~ — **DONE (re-verified 2026-09-17).** `^16.3.2` has been installed since 713c1e89 (2026-08-27, which moved it off `^12.1.5`) and declares `react ^18.0.0 || ^19.0.0` itself; Appendix C already ticks this sub-gate. The rest of gate 4 is still open: add `react@19` to the dev/CI matrix (today only the advisory, `continue-on-error` `react-19-advisory` leg installs it, with `--legacy-peer-deps`), run the full regression (contract suite + example QA), then widen peers **additively**: `^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0` — the floor stays, hosts on older React are unaffected. Do not promise 19 support to consumers before those gates are green. @@ -407,7 +411,7 @@ Every workstream below is a series of small, independently shippable, reversible ### 9.2 Workstream A — Class → hooks migration (leaf-first) -**Motivation:** 21 class components, most carrying `UNSAFE_componentWillReceiveProps` that is a simple props→state derivation. Hooks versions are smaller, StrictMode-safe, and testable. +**Motivation:** ~~21 class components~~, most carrying `UNSAFE_componentWillReceiveProps` that is a simple props→state derivation. Hooks versions are smaller, StrictMode-safe, and testable. **Count superseded 2026-09-17 (§9.9-H1):** the deletion took **4 class components** out of this population — `Carousel`, `Collapse`, and the *two* declared in `Square.js` — so the figure above predates it. The exact replacement number is deliberately not written here: "21" was measured with a methodology this plan never recorded (the tree then held 39 `class … extends Component` declarations across 24 files, 35 across 21 files now), so re-derive it against a stated rule when Workstream A actually starts rather than inheriting an unreproducible count. What is firm is the delta and the `UNSAFE_*` figure below, both measured. **Rules of engagement:** - One component per PR. Refactor only components with existing tests (write them first otherwise). @@ -417,8 +421,8 @@ Every workstream below is a series of small, independently shippable, reversible **Suggested order (dependency- and risk-sorted):** -1. Leaf presentational, mechanical conversions: `Expand`, `Counter`, `ProgressBar`, `ProgressSteps`, `InputNative`, `Tabs` (components pack; gate `Tabs` on the H6 duplicate audit first). `Carousel` and `Collapse` turned out to be orphans (§9.9-H1) — **delete instead of migrating**; `Square` is already a function component, and the pack `TabList` joined the orphan list (mapper uses the engine copy — §2.6-14). -2. Function-component `defaultProps` → default parameters: **done in Phase 2** (`TooltipPop`, `Image` — §6 step 1; React 18.3 warns on them); the third site, `ImageSwatch`, is an orphan resolved by deletion (§9.9-H1). +1. Leaf presentational, mechanical conversions: `Expand`, `Counter`, `ProgressBar`, `ProgressSteps`, `InputNative`, `Tabs` (components pack; gate `Tabs` on the H6 duplicate audit first). ~~`Carousel` and `Collapse` turned out to be orphans (§9.9-H1) — **delete instead of migrating**; `Square` is already a function component, and the pack `TabList` joined the orphan list (mapper uses the engine copy — §2.6-14).~~ **CLOSED 2026-09-17** for those four: `Carousel`, `Collapse`, `Square` and the pack `TabList` are deleted, so the two `UNSAFE_*` migrations `Carousel` and `Collapse` carried are off this workstream permanently and the engine `TabList` (`src/core/pages/main/components/TabList.js`, the copy `mapper.js` uses) is untouched. `Tabs` is **still open**: the pack copy `src/core/components/Tabs.js` survived the deletion, so the H6 duplicate is only half resolved and the gate above still applies. +2. Function-component `defaultProps` → default parameters: **done in Phase 2** (`TooltipPop`, `Image` — §6 step 1; React 18.3 warns on them); ~~the third site, `ImageSwatch`, is an orphan resolved by deletion (§9.9-H1).~~ **CLOSED 2026-09-17** — `ImageSwatch` is deleted, so the third site is resolved and this step has nothing left in it. 3. Page-level: `pages/main/components/Tabs`, `TableView`, `LocalDraftTableRow`. 4. Module-level: `AutoSave`, `ToggleField`, `asInputDateField`, `Upload` views. 5. **Not in this workstream:** `rules.js`, `form/utils.js`, `mapper.js`, `Render.js` → §9.3. @@ -434,7 +438,7 @@ Every workstream below is a series of small, independently shippable, reversible 1. **Characterize:** contract tests from §9.5 must cover every documented meta capability before any surgery. 2. **Extract pure logic and break the import cycle:** data processing, error mapping, `showIf` evaluation, payload assembly → pure functions in `ui-utils-pack` with direct unit tests (much of `utils.js` already leans this way). This is also where the engine↔form-modules cycle dissolves — `errorsProcessing`, `formsStorage`, `clearErrorsMap`, `storedTouched` (§2.6-4) move into a neutral module imported by both sides. 3. **De-globalize instance state:** the engine writes instance-bound state into module globals — `Active.translate` (`rules.js:245`), the module-level `errorHandlerFunction` (`rules.js:241–243`), and ~12 action handlers bound to `this` on the shared `FIELD.FUNC` registry (`rules.js:616–1081`, resolved at render time through `transforms.js`). Today two `UIRender` instances on one host page silently hijack each other's actions/translate, and StrictMode's double-invocation exercises exactly these writes — a decomposition that skipped this step would still fail the workstream's own acceptance gate. Inventory all module-global mutable state — `Active.*`, `FIELD.FUNC`, `errorHandlerFunction`, translation registries, plus `formsStorage`, `errorsMap`, `formInitialValues`, `storedTouched` (§2.6-5) — and move it to per-instance context; make the modal portal root per-instance too (today every instance renders the same fixed `id="render-popup-root"` and the portal grabs the first one in the DOM). Add a two-instances-on-one-page case to the contract suite (R14). -4. **Fix the catalogued runtime hazards (§2.6-6):** deduplicate the per-render `form.subscribe()` and keep/call its unsubscribe; move the setState-in-render Dropdown branch into an effect/derivation; make the debounced `handleChangeInput` per-instance instead of prototype-shared; stop mutating state via `set(this.state, …)` (clone or immutable update); cancel `AutoSave`'s debounce on unmount; stop re-creating the `autoSubmit` debounce every render. Each is a small, testable fix — most can ship before the full decomposition. +4. **Fix the catalogued runtime hazards (§2.6-6):** ~~deduplicate the per-render `form.subscribe()` and keep/call its unsubscribe~~ **— CLOSED, re-measured 2026-09-17** (the same fact §2.6-6a already records as done, which this list contradicted): `modules/form/utils.js:380-386` subscribes once per active form and tears the previous subscription down when the form is replaced, and `componentWillUnmount` at `:421-426` calls the stored unsubscribe and nulls both `_unsubscribeForm` and `_subscribedForm`; the file contains no other `subscribe` call. The five hazards that follow are still open; move the setState-in-render Dropdown branch into an effect/derivation; make the debounced `handleChangeInput` per-instance instead of prototype-shared; stop mutating state via `set(this.state, …)` (clone or immutable update); cancel `AutoSave`'s debounce on unmount; stop re-creating the `autoSubmit` debounce every render. Each is a small, testable fix — most can ship before the full decomposition. 5. **Replace mutation with composition:** convert prototype patching into explicit HOC/wrapper composition (`withUIRenderLifecycle(Component)`) — same behavior, but visible in the component tree and StrictMode-analyzable. 6. **Hooks form (final state):** lifecycle logic as hooks (`useUIRenderData`, `useFormIntegration`), classes retired. 7. **Acceptance for the whole workstream:** demo runs clean under `` per the §7 definition (subscriptions, cleanup, no setState-in-render, two-instance isolation). @@ -457,21 +461,21 @@ All decomposition outputs are authored in TypeScript from the start (`engine/*.t - **Contract versioning — shipped 2026-08-27:** optional root-level `metaVersion` (`"MAJOR"` or `"MAJOR.MINOR"`, current `1`), absence meaning "current". No negotiation machinery beyond the field and its documented meaning — anything more would be speculative. Made genuinely inert: as a raw passthrough it reached the DOM as `metaversion="1"` with an unknown-prop warning, so `metaToProps` now strips it and `$schema` alongside it (which is what makes an inline editor pointer usable). Both are asserted byte-identical against the same document without them, and the host's own meta object is proven untouched. - **Error boundaries per render node — shipped 2026-08-27** (`Render.js`, `metaPath.js`, `mapper.js`; contracts in `ui-render/__tests__/Render.error-reporting.test.js`, `pages/main/__tests__/mapper.error-sink.test.js`, `pages/main/__tests__/UIRender.error-hook.test.js`): the boundary was extended, not replaced. The signature mismatch is fixed (`mapper.js` read `{err, errInfo}` off a report emitted as `{error, errorInfo}`, so it printed `undefined undefined` — and to `console.log`, which is why nobody noticed); the sink now formats on the error channel. Every report carries `path`, the JSON path of the failing node, and so does the diagnostic rendered in its place: `[ui-render] render error at "items[1]" (view "Table", name "orders"): TypeError: …`. The path notation is shared with the dev-mode validator through `ui-render/metaPath.js` (the validator's private `joinPath` moved there), so both halves of §9.4 point at a node the same way. A documented `onError` prop on the public API receives the report — `{error, errorInfo, path, props, message}` — alongside the library's own sink, reached through the `instance` every node already carries rather than a module global two renderers would share; a host reporter that throws is caught. **How the path is threaded, and its limit:** position travels down a React context, not props — every prop a node carries is spread onto the resolved component and can reach the DOM (that is how `currencyCode` used to leak, fixed 2026-08-27), and the DOM baseline confirms the diagnostic adds nothing there. The path covers the `items` backbone and table cells; a node a component builds without a position (a tab's content, an icon definition) reports the closest enclosing node, and a value-definition renderer supplies a row index, so under one of those the last segment names a row rather than an `items` slot. It is exact for a failure inside the component a node resolved to, and names the parent for a failure the engine hits while preparing a node — because that is where React's boundary is, not because of the threading. - **The two non-leak markup defects the baseline recorded — fixed 2026-08-28** (`components/Expand.js`, `pages/main/components/TableView.js`; the sticky-class contract in `pages/main/components/__tests__/TableView.test.js`, zero-tripwires in `FIXED_MARKUP_JUNK`). `Expand` rendered `id={String(id)}` while `id` is optional in meta, so `String(undefined)` shipped the literal attribute `id="undefined"` — repeated on every such view in a document, which means a `