From 66214532406e7450f1a418c2c2ca6b72791b6695 Mon Sep 17 00:00:00 2001 From: Aliaksandr Samuseu Date: Thu, 17 Sep 2026 10:24:47 +0200 Subject: [PATCH] EPBDS-16211 make the plan tell the truth: 121 corrections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A verification sweep read all 1329 lines of docs/UPGRADE-PLAN.md and checked every trackable claim against the repository. The document was wrong in both directions, and the plan is what the next person reads before touching this code, so both directions matter. 104 STALE MARKERS — work presented as open that had in fact shipped. Most of it because a PR fixed something and never came back to the plan. Phases 1, 2 and 3 are all complete and all three read as pending; the status row still described a "React 17 hosted checkpoint" months after the React 18 bump. 16 FALSE CLAIMS — assertions the repository contradicts. The ones that could mislead someone into a decision: - "react-final-form is the ONLY remaining React ≤18 cap" — it is not. react-final-form-arrays 3.1.4 is a direct dependency carrying the identical cap. Stated in three places, including the comment in ci.yml that justifies --legacy-peer-deps. Corrected there too: the §9.7-F4 bump has to move both packages together (arrays 5.0.0 requires rff ^7.0.1, verified against the registry). - Appendix C ticks "React 19 flip: peers widened to include ^19". They are not widened; package.json still reads ^16.14.0 || ^17.0.0 || ^18.0.0. A reader could publish that claim to a consumer. - The pack measurements (295 files / 7.25 MB / 2.53 MB) were stale in five places. Re-measured with the repo's own gate: 299 / 6.44 MB / 2.39 MB — more files, less weight, after the semantic-ui exit. - The plan's own correction saying FIELD.METHODS "does not exist" is wrong. It exists at rules.js:1109 and :1127, and it is precisely the hazard class finding 5 catalogues: a per-instance write onto a module-global. - Every module size in Appendix A was understated. Render.js is 223 lines, not the recorded 133. Phase 6 is scoped against those numbers. - "No legacy React APIs anywhere in src" is ticked while Upload.js:34 calls event.persist(); §2.4 records this correctly, Appendix C never did. The global-listener inventory missed the two document listeners the SUIR exit added (Listbox.js:206, TooltipPop.js:369). Nothing is deleted. Every correction supersedes the old text and keeps it visible, in the style the document already uses, so a reader can see what changed and why. 13 candidate corrections were dropped — two because the re-check refuted them and the plan was right as written, the rest as duplicates already covered by an adjacent edit. The version bump is NOT part of this: the release stays the maintainers' call. --- .github/workflows/ci.yml | 16 ++- docs/UPGRADE-PLAN.md | 229 +++++++++++++++++++++------------------ 2 files changed, 131 insertions(+), 114 deletions(-) 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/UPGRADE-PLAN.md b/docs/UPGRADE-PLAN.md index 61aa3b9a..8be5a614 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,16 +16,16 @@ 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. @@ -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,13 +126,13 @@ 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 | @@ -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,10 +251,10 @@ 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 @@ -278,8 +278,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.** @@ -374,7 +374,7 @@ Sequencing: StrictMode becomes the *acceptance criterion* of workstream §9.3 (e | `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). | | `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 +391,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. @@ -434,7 +434,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 +457,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 `