Skip to content

UN-4271 [FEAT] Frontend v1 version switch and route registry - #2433

Merged
jaseemjaskp merged 9 commits into
mainfrom
UN-4271-version-switch-route-registry
Nov 11, 2026
Merged

jaseemjaskp merged 9 commits into
mainfrom
UN-4271-version-switch-route-registry

Conversation

@jaseemjaskp

@jaseemjaskp jaseemjaskp commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

What

  • Adds the frontend v1 version switch and route registry (P0-T03, UN-4271).
  • Registry (src/v1/app/registry/): one entry per v1 page with page, v1Routes, replaces, toV0, toV1, toggle, distributions and registeredBy. This is the contract table from the spec and the shape UN-4345 [FEAT] Frontend v1 E2E harness and the frontend-v1-e2e group #2355 expects. Pages are declared in PAGES and go live by adding their key to REGISTERED_PAGES, a one-line change (D9). The registry ships empty.
  • Validation: validate.test.ts runs over the real registry and fails CI on:
    • patterns that overlap across pages (sharing a prefix is allowed, and the longest match wins);
    • a replaced v0 route with no v1 target;
    • a mapping that needs a param its source lacks;
    • a mapping whose URL lands on a route of the other version, which would redirect forever;
    • a v1 route with no toV0 mapping (so resolveUrl can always link a declared page);
    • a malformed page key, or a declared page with no component;
    • toggle on any page other than UN11 (D14) and its demo stand-in.
  • Version switch: routes/Router.jsx wraps its routes in VersionSwitch (exported from @v1).
    • Unregistered URLs render v0 unchanged.
    • Registered pages render v1 as a separate tree, app/V1App.tsx, with its own providers and error boundary. It is lazily loaded, so v1 code is fetched only when a v1 page renders.
    • v0 URLs of registered pages redirect to the mapped v1 route, keeping the query and hash.
  • Old-version preference (Human Review only):
    • Stored per user and per browser in localStorage as unstract.v1.oldVersion.<userId>. There is no backend call.
    • setOldVersion(on) (exported from @v1 for P6-T07's controls) moves the user to the mapped screen with no reload.
    • If storage can't be read, the default is v1, and a choice still lasts for the session.
  • resolveUrl(page, v1Pattern, params): returns the URL in the version the user gets, encoding raw param values. It is the interface asked for by P2-T10 global search (UN-4278).
  • Redirects keep encoded params inside their segment (%2F, %3F and %23 are re-encoded, not turned into separators).
  • The v1 error boundary reloads once on a stale chunk after a deploy, as v0's route boundaries do, and otherwise shows a Reload button.
  • Dev/test routes (VITE_V1_DEV_ROUTES=true, set only on the E2E image in ci-test.yaml):
    • Registers two demo pages: a plain page, and one shaped like Human Review with the toggle and a v1-only sub-route. Their v0 stand-ins are in Router.jsx.
    • Honours a preview override, localStorage["unstract.v1.preview"] = '["UN05"]', which renders declared but unregistered pages as v1. UN-4345 [FEAT] Frontend v1 E2E harness and the frontend-v1-e2e group #2355 needs this for page tasks that test before go-live.

Why

  • Users get v1 wherever it is complete; only Human Review keeps a way back to v0 (D14). P0-T06, P0-T09, P2-T01 and P2-T10 build on this.

How

  • Resolution (resolve.ts) is a pure function of the pathname, the live entries and the preference. It uses React Router's matchRoutes, so the most specific pattern wins.
    • Toggle page, preference unknown: v0 URLs stay on v0, so v0 can load the session.
    • Toggle page, old version chosen: v1 URLs redirect to the mapped v0 route (a v1-only sub-route goes to the nearest v0 route).
    • Every other case for a registered page: v0 URLs redirect to v1.
  • The user id comes from v0's session store via Router.jsx. v0 loads the session inside its own routes, so on a fresh load the switch uses the last user signed in on this browser (unstract.v1.lastUser). A reload of a v1 Human Review URL therefore still lands on the old version. After a new user signs in, the previous user's choice never redirects them.
  • registry/types.ts and registry/entries.ts import only each other and never read import.meta.env, so the E2E harness can import them from Playwright. Environment flags are read in live.ts.
  • VITE_DISTRIBUTION (oss, the default, cloud or onprem) drops entries for distributions the build lacks. An unknown value fails vite build (vite.config.js); at runtime it logs and falls back to oss rather than blanking the app. It is the name P0-T07's plugin manifest (UN-4286 [FEAT] Add the v1 plugin manifest, slots and build check #2394) uses. The cloud and on-prem builds don't set it yet, so every image builds as oss until P0-T07 wires them.
  • Registry validation runs in development builds and in CI (validate.test.ts), not in release builds, so its code stays out of the bundle v0 loads eagerly.
  • The public helpers live in app/api.ts, re-exported by index.ts, so v1 code and tests never import the entry itself.
  • Seam allowlist respected. The only v0 files touched are routes/Router.jsx and build/CI configuration: vite.config.js, the frontend Dockerfile and ci-test.yaml. The Dockerfile's VITE_V1_DEV_ROUTES and VITE_DISTRIBUTION build args are empty by default and declared below the install layer, so CI and release still share the cached bun install.

Can this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)

  • No functional change for users. The registry is empty and the demo routes are off in release builds, so every URL resolves to v0 and the v0 route tree renders exactly as before.
  • Because the build sets cssCodeSplit: false, v1's stylesheet (P0-T04) is now merged into the global CSS. I diffed it against a build of origin/main:
    • Every new rule is scoped under .v1 or ui:, except duplicate @font-face declarations for the Inter and Geist Mono fonts v0 already loads.
    • Tailwind's --tw-* property fallback for old browsers is also added.
    • None of these restyles v0.
  • Until P0-T06 lands, v1 pages render without auth guards. Only the dev-only demo pages exist.

Database Migrations

  • None

Env Config

  • VITE_V1_DEV_ROUTES: build-time, optional. true only for E2E and local development.
  • VITE_DISTRIBUTION: build-time, optional, defaults to oss; also a Dockerfile build arg. An unknown value fails the build.

Relevant Docs

Related Issues or PRs

  • UN-4271. Parent epic UN-4257. Builds on UN-4270 [FEAT] v1 scaffold, strict TypeScript and import boundary #2380 (P0-T01) and UN-4299 [FEAT] v1 design tokens, themes and style isolation (P0-T04) #2399 (P0-T04).

  • UN-4345 [FEAT] Frontend v1 E2E harness and the frontend-v1-e2e group #2355 (P0-T09 E2E harness): its contract/registry.ts can now re-export PAGES/DEMO_PAGES/REGISTERED_PAGES. It also needs:

    • the preference key unstract.v1.oldVersion.<userId> with value "true";
    • the last-user key unstract.v1.lastUser, holding the user id: a page load that starts on a v1 URL takes the user from it, so the Human Review round trip must seed it or open a v0 page first;
    • the preview mechanism { kind: "localStorage", key: "unstract.v1.preview" };
    • DEMO-REVIEW excluded from its "only UN11 toggles" self-test.

    All three keys can be imported without Vite: PREVIEW_STORAGE_KEY from registry/types.ts, and oldVersionStorageKey, OLD_VERSION_ON and LAST_USER_STORAGE_KEY from lib/oldVersion.ts.

    The E2E specs for the redirect and the Human Review round trip follow once it merges.

  • UN-4278 (P2-T10): the resolveUrl interface.

Dependencies Versions

  • No change

Notes on Testing

  • bun run typecheck:v1, bun run lint:v1, bun run test:v1: 115 v1 tests pass (1 deliberate skip) and all 89 boundary fixtures pass.
  • bun run typecheck, biome ci src/ and the full vitest run pass: 769 tests.
  • vite build passes with VITE_V1_DEV_ROUTES on and off, and fails with VITE_DISTRIBUTION=saas.
  • Mutation-checked: removing any of the stale-user guard, replace on the redirect, the release-build preview gate, distribution filtering, the v1 error boundary, param encoding, the redirect-loop check or the toggle check in resolveUrl fails a test.
  • Unit tests cover:
    • the resolution rule for every combination of registered, preference (on, off, unknown) and distribution;
    • the mapping in both directions for every registry entry;
    • overlap rejection;
    • preference persistence: reload, re-login, per user, refusing storage;
    • VersionSwitch redirects and the Human Review round trip.
  • Smoke-tested in Chromium against a VITE_V1_DEV_ROUTES=true dev server, with no page errors:
    • /acme/v0-demo?x=1 lands on /acme/v1-demo?x=1 with the v1 root;
    • an unregistered URL stays on v0;
    • "Switch to old version" on /acme/v1-demo-review/details lands on /acme/v0-demo-review in the same document;
    • the choice holds after a reload, and clearing it returns to v1;
    • /a%2Fb/v0-demo lands on /a%2Fb/v1-demo.

Screenshots

  • N/A (no user-visible change)

Checklist

I have read and understood the Contribution Guidelines.

jaseemjaskp and others added 3 commits November 4, 2026 11:32
Typed route registry for v1 pages (declare in PAGES, go live with one line
in REGISTERED_PAGES), validated by a unit test that fails CI on overlapping
patterns, unmapped v0 routes, missing components or a toggle outside
Human Review (D14).

VersionSwitch wraps v0's routes in Router.jsx: unregistered URLs render v0
unchanged, registered pages render v1 as their own lazily loaded tree, and
their v0 URLs redirect to the mapped v1 route. Human Review's old-version
preference is per user, per browser (localStorage); setOldVersion() moves
the user to the mapped screen without a reload.

VITE_V1_DEV_ROUTES (set only for the E2E image in ci-test.yaml) registers
demo pages that prove the switch and honours a preview override for
declared-but-unregistered pages. resolveUrl() resolves a page's v1 route
to the URL the user gets, for global search (UN-4278).
- Validation rejects a mapping whose URL lands on a route of the other
  version (an endless redirect), malformed page keys, and any v1 route
  without a toV0 mapping, so resolveUrl never returns null for a declared
  page's route.
- Redirects and resolveUrl encode param values, so %2F, %3F and %23 stay
  inside their segment instead of becoming separators.
- resolveUrl decides liveness by page key, not object identity.
- An unknown VITE_UNSTRACT_DISTRIBUTION fails `vite build`; at runtime it
  logs and falls back to oss instead of blanking the app.
- Registry validation runs only in development (CI covers release).
- The v1 error boundary reloads once on a stale chunk (as v0 does) and
  offers a Reload button.
- Dockerfile build args move below the install layer so CI and release
  share the cached install; VITE_UNSTRACT_DISTRIBUTION is settable.
- Public helpers move to app/api.ts so v1 code can import them without the
  entry.
- Tests now catch the guards mutation testing showed were unprotected
  (stale-user bounce, history replace, preview gating, distribution,
  error boundary, encoding); comments corrected.
@jaseemjaskp

Copy link
Copy Markdown
Contributor Author

Self-review (code, tests, error handling, types, comments)

I fixed everything below in 217c586.

Bugs

  • The validator accepted mappings that redirect forever. A v0 /hr mapped to a v1 /hr/* outranks its own target. It now rejects any mapping whose URL lands on a route of the other version.
  • Encoded params were decoded on redirect. /a%2Fb/... went to /a/b/.... Params are now re-encoded, and a splat is encoded per segment. The same applies to resolveUrl.
  • An unknown VITE_UNSTRACT_DISTRIBUTION threw when @v1 loaded. v0 imports @v1 eagerly, so this blanked the whole app. It now fails vite build, and at runtime it logs and falls back to oss.
  • The v1 error boundary could not recover from a stale chunk. React caches the failed lazy import. It now reloads once, using the same guard as v0's handleRouteError, and shows a Reload button.
  • resolveUrl returned null for declared non-toggle pages. toV0 is now required for every v1 route on every page.
  • resolveUrl judged liveness by object identity. It now checks the page key.
  • The Docker build args sat above bun install. CI and release could not share the cached install. They now sit below it.
  • Imports were out of order in one test (biome ci).

Tests

  • I re-ran mutation testing. Each guard that previously survived now fails a test:
    • stale-user bounce;
    • replace on the redirect;
    • the release-build preview gate;
    • distribution filtering;
    • the error boundary;
    • resolveUrl honouring the toggle on non-toggle pages;
    • encoding;
    • the loop check.
  • The per-entry mapping test now builds URLs with generatePath semantics, so a valid splat target doesn't fail it.

Comments: I corrected the inaccurate ones: toV0 usage, the backstop scope in Router.jsx, when to add page components, resolveUrl semantics, and the README's notes on preview reload and distributions.

Not changed

  • Logging an invalid registry in release builds. The release build skips validation instead. CI runs it over the real registry, and keeping it out of production keeps the validator out of the bundle v0 loads eagerly.
  • Branded or literal-union types for page keys and patterns. The fields mirror the contract the E2E harness consumes. Page-key format is now checked by the validator and by the preview parser instead.
  • Moving chosen into a closure in the old-version store. It's internal and harmless.

@jaseemjaskp
jaseemjaskp marked this pull request as ready for review November 4, 2026 07:25
@jaseemjaskp
jaseemjaskp requested a review from a team as a code owner November 4, 2026 07:25
@greptile-apps

greptile-apps Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

via Greptile

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no new actionable issues were found.

Summary

Adds a registry-driven switch between v0 and a lazily loaded v1 tree. The release registry remains empty.

  • Registered URLs can render in a separate v1 app while other routes stay in v0.
  • Human Review can remember when a user chooses the old version.
  • Callers can get a URL for the version of a page they will open.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  URL[Browser URL] --> Switch[VersionSwitch]
  Registry[Live registry and user choice] --> Switch
  Switch -->|Unregistered or old version| V0[v0 routes]
  Switch -->|Mapped URL| Redirect[Replace URL; keep query and hash]
  Redirect --> Switch
  Switch -->|v1 route| Boundary[v1 error boundary]
  Boundary --> App[Lazy V1App]
Loading

Reviews (7) · Last reviewed commit: "fix(frontend-v1): address the version sw..." · Reviewed by Greptile

Comment thread frontend/src/v1/pages/demo/index.tsx Resolved
…(UN-4271)

- Validation rejects a v1 and a v0 route of the same page that match the
  same URL (identical patterns excepted). This covers the redirect loop
  the placeholder-URL check caught and the case it missed: a param equal
  to a static segment (/old/settings -> /new/settings -> /settings).
- The demo review page encodes orgName in its links, so an encoded org
  name stays one segment.
- Use a Set for the param lookup (Sonar S7776).
@ritwik-g

ritwik-g commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Hi Jaseem, this is from P2-T10 (Global search), which consumes resolveUrl. The signature and behaviour match what you described on UN-4271 (comment 47676). Encoding, null for an undeclared page or unknown pattern, the UN11 old-version path, and query-safe results all check out in probe tests.

Two things that would help consumers:

  1. A missing param throws from generatePath (resolve.ts:73, Missing ":id" param), and an empty-string param silently builds a wrong URL ({ orgName: "" } → /workflows). Could resolvePageUrl return null for both, matching "null means it can't build a URL"? If you'd rather keep the throw, a line in the JSDoc is enough.
  2. Can you confirm a page is added to PAGES once its v1 routes exist, before go-live? Until then resolveUrl returns null for every UN/LW key, so we're keeping a v0 fallback in Search.

@ritwik-g

ritwik-g commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up from Global search (P2-T10, UN-4278), and it applies to the app shell rail (P2-T01) too.

resolveUrl returns a v0 URL only for a page that's declared. Pages are declared only once their v1 routes exist, and the registry ships empty. That leaves a gap: from the first page going live until the last one, the rail and search can't link to any page that has no v1 yet. P2-T01 requires that case to work ("A destination whose page has no v1 yet opens the v0 page", with the route registry as the source). P2-T10 requires it too ("v1 or v0 as the version switch decides").

Could the registry hold a v0 home URL per page key, e.g. the API deployments page → /:orgName/api? resolveUrl would return that URL when the page isn't declared yet. That gives the rail, search and menus one source, and none of them keeps its own v0 table. If you'd rather solve it another way, tell us and we'll adapt.

…h-route-registry

# Conflicts:
#	frontend/src/v1/README.md
@jaseemjaskp jaseemjaskp added the main-only Stays on main: not backported to a release line label Nov 10, 2026
The feedback components from main say to mount the Toaster once inside the
v1 root; V1App is that root. It sits inside ThemeProvider (toasts portal into
the themed #v1-portal) and outside Suspense/Routes, so it stays mounted while
a page loads and across v1 route changes.

The VersionSwitch test checks notify() shows a toast on a v1 page and falls
back to the console once the user moves to a v0 screen.

@Deepak-Kesavan Deepak-Kesavan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated review

Automated review by Unstract PR review kit (Claude Code). These are starting points, not verdicts: the maintainer decides what to act on, closing any of them is a legitimate outcome, and nothing here blocks the merge. Each finding was checked by an independent verifier; anything tagged [unverified] had its facts confirmed but is a judgement call.

4 inline comment(s).

P0-T09 harness contract (#2355), checked against this head

  • Registry importable outside Vite: yes. registry/types.ts and registry/entries.ts are plain data with relative imports, so contract/registry.ts can re-export PAGES, DEMO_PAGES, REGISTERED_PAGES and the types. The PR's PageKey also allows DEMO-…, which #2355's own type doesn't.
  • Dev/test preview of an unregistered page: yes. It uses { kind: "localStorage", key: "unstract.v1.preview" }, holds a JSON array of declared keys, is read once at load, and is honoured only with VITE_V1_DEV_ROUTES=true. A cookie isn't needed, so the cookie encoding in previewValue doesn't apply. The key can't be imported (see the live.ts comment).
  • Preference key and demo routes: yes, with one difference. The key is per user, unstract.v1.oldVersion.<userId> = "true", not one fixed key. The helpers in lib/oldVersion.ts (oldVersionStorageKey, OLD_VERSION_ON, LAST_USER_STORAGE_KEY) can be imported. The demo routes are /:orgName/v1-demo and /:orgName/v1-demo-review (plus /details), and their v0 stand-ins are /:orgName/v0-demo and /:orgName/v0-demo-review. The E2E image enables them in ci-test.yaml. Because of the VersionSwitch.tsx comment, a round trip in an API-login context also needs unstract.v1.lastUser set.
  • v1 root marker: yes. ThemeProvider, which V1App mounts, renders data-ui-version="v1" on the .v1 root. That matches V1_ROOT_SELECTOR as it is.

Spec acceptance includes E2E for the redirect and the Human Review round trip. The PR body says those specs follow once #2355 merges, and #2355 is waiting on this PR. Noting the ordering here only so it's visible.

review-pr-bot:review

Comment thread frontend/src/v1/app/VersionSwitch.tsx Resolved
Comment thread frontend/src/v1/app/registry/live.ts Resolved
Comment thread docker/dockerfiles/frontend.Dockerfile Resolved
- resolvePageUrl/resolveUrl return null when a :param of the pattern is
  missing or empty, instead of generatePath throwing or dropping the segment.
  A final splat may still be omitted.
- PREVIEW_STORAGE_KEY moves to registry/types.ts (re-exported from live.ts)
  so the E2E harness can import it without Vite; the types.ts header now
  lists lib/oldVersion.ts as harness-importable too.
- checkEntry splits into small rule functions (SonarCloud S3776: cognitive
  complexity 21 > 15); the two mapping-coverage loops share one helper.
  Problems and their order are unchanged.
- README and a test record that a load starting on a v1 URL has no userId
  until a v0 page loads (the session loads in v0's PersistentLogin), so the
  old-version toggle relies on unstract.v1.lastUser until P0-T06.
@jaseemjaskp

Copy link
Copy Markdown
Contributor Author

@ritwik-g thanks for checking this from the Search side.

  1. Fixed in e798148: resolveUrl now returns null when a :param of the pattern is missing or empty, instead of throwing or building a wrong URL. A trailing * can still be left out. Pages are added to PAGES once their v1 routes exist, before go-live, so until then the v0 fallback in Search is right.
  2. A v0 home URL per page key makes sense, so the rail, Search and menus share one source. It's beyond this PR's scope, so I'll raise it as a follow-up ticket and link it here.

@jaseemjaskp

jaseemjaskp commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor Author

@ritwik-g follow-up for point 2 is UN-4647: a v0 home URL per page key in the registry, with resolveUrl falling back to it while a page is undeclared.

@Deepak-Kesavan Deepak-Kesavan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated re-review of e798148 (Unstract PR review kit, Claude Code). All four earlier threads are answered and resolved. One new SonarCloud finding, inline.

🤖 Unstract PR review kit (Claude Code) · re-review of e798148

@vishnuszipstack vishnuszipstack left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review of PR 2433 — UN-4271 Frontend v1 version switch and route registry

Mode: INITIAL. Reviewed head e79814879873aa3049f0f4ea71c3c4b336fc32bb, merge base 119835db1ac02fbb69ce277f66d77c1eb216034a, base freshened (base_fetch: ok), 28 files, 2400 lines (matches GitHub). Rubric: unstract plugin v2.1.80.
Signals: lenses 2, 4 and 13 raised (React lifecycle in VersionSwitch/V1App, an auth-adjacent change because a new subtree renders outside v0's guards, tests added), lenses 11 and 18 raised (Dockerfile build args, CI workflow).
Security deep-dive: done
boundary-sweep: 3 sinks traced, clean (the two console.error lines carry the build's own env value and a registry-problem list; the v1 fallback shows a fixed string).

Verdict — APPROVE WITH NITS

Summary — Critical: 0 · High: 0 · Medium: 4 · Low: 13 · Lenses run: 19/19 (1 not covered, 4 N/A; see checklist).

The design is sound and well tested where it matters: the v1 suite passes (52 files, 527 passed, 1 skipped on purpose, coverage 97% lines), typecheck:v1 and lint:v1 pass, and mutating the per-user masking, the toggle rule, the session fallback, the layout-effect sync, writeLastUser, replace and the sign-out guard each fails a test. The registry ships empty, so nothing below is reachable by users today. The Mediums are worth fixing before the first page registers.

Findings

Posted inline on the diff; index:

    1. [Medium] [Lens 2, 3] — The v1 error boundary never resets when navigating between routes of the same page — frontend/src/v1/app/VersionSwitch.tsx:83
    1. [Medium] [Lens 7, 11, 18] — No build passes VITE_UNSTRACT_DISTRIBUTION, so cloud and on-prem images build as oss and the distribution filter does nothing [ON-PREM UX] — docker/dockerfiles/frontend.Dockerfile:55
    1. [Medium] [Lens 13] — resolveUrl, the public link builder, has no test, and its docstring contradicts the switch during a user change — frontend/src/v1/app/api.ts:35
    1. [Medium] [Lens 13] — The boundary's stale-chunk reload, the reason the boundary exists, is never exercised — frontend/src/v1/app/VersionSwitch.tsx:60
    1. [Low] [Lens 13, 16] — The Router wiring is untested, and the E2E flag ships with no E2E consumer — frontend/src/routes/Router.jsx:174
    1. [Low] [Lens 13] — load()'s fallbacks are untested — frontend/src/v1/app/registry/live.ts:91
    1. [Low] [Lens 2] — Before the session loads, the previous user's "old version" choice redirects a different user, and the round trip loses the sub-route — frontend/src/v1/app/VersionSwitch.tsx:113
    1. [Low] [Lens 2] — Registry patterns are matched ahead of all v0 routes, but validated only against each other — frontend/src/v1/app/registry/resolve.ts:42
    1. [Low] [Lens 4] — Going live is described as a one-line change, but v1 renders outside PersistentLogin and RequireAuth — frontend/src/v1/README.md:121
    1. [Low] [Lens 3] — setOldVersion returns true and says it persists even when the write failed — frontend/src/v1/stores/oldVersion.ts:55
    1. [Low] [Lens 2] — The screen is blank while the v1 chunk loads — frontend/src/v1/app/VersionSwitch.tsx:84
    1. [Low] [Lens 16] — Comments say v1's styles load only with a v1 page; with cssCodeSplit: false they are in the global stylesheet — frontend/src/v1/app/V1App.tsx:3
    1. [Low] [Lens 16] — The README says CI rejects a v1 and v0 route of one page matching the same URL; the validator allows identical ones — frontend/src/v1/README.md:124
    1. [Low] [Lens 16] — The Router comment says "registered"; the switch uses the wider live set — frontend/src/routes/Router.jsx:176
    1. [Low] [Lens 7] — The old-version contract for the E2E harness needs two keys, but the hand-off names one — frontend/src/v1/lib/oldVersion.ts:9
    1. [Low] [Lens 15] — selectOldVersion is unused — frontend/src/v1/stores/oldVersion.ts:87
    1. [Low] [Lens 13] — Console spies are restored only when the test passes — frontend/src/v1/app/VersionSwitch.test.tsx:217

Lens checklist

  1. Spec & intent — Clean
  2. Architectural fit & precedent — See findings #1, #7, #8, #11
  3. Correctness & edge cases — See findings #1, #10
  4. Security — See findings #9
  5. Data integrity & migrations — N/A — no server data or schema; the localStorage keys are covered under lens 7
  6. Concurrency — Clean
  7. API & contract compatibility — See findings #2, #15
  8. Reliability & resilience — Clean
  9. Performance & cost — Clean
  10. Observability — Clean
  11. Operational safety — See findings #2, #5
  12. LLM/agent-specific — N/A — no model, prompt or agent path
  13. Testing — See findings #3, #4, #5, #6, #17
  14. Dependencies & build — N/A — no dependency or lockfile change
  15. Code quality — See findings #16
  16. Doc & comment accuracy — See findings #12, #13, #14
  17. Cross-document rule consistency — Not covered — merge-result sweep unavailable: git 2.34 here, the script needs 2.38
  18. On-prem deployment experience — See findings #2
  19. Scaling & workload lifecycle — N/A — no worker or scaling config touched

Assessed by me rather than an agent: lenses 4, 5, 6, 7 (consumer search, with the type-design agent), 8, 11, 12, 14, 15 (pinned gate), 17, 18, 19.

  • Lens 4: the deep-dive's find pass returned no candidates (redirect targets stay same-origin and re-encode params; the preview override is build-gated and validated; the preference is a UI flag).
  • Lens 6: the only ordering hazard is the layout-effect user sync, and the lifecycle trace found its guard holds.
  • Lens 8: reloadOnce is bounded by a 10-second window in sessionStorage, so a missing chunk cannot reload-loop.
  • Lens 11: the Dockerfile ARGs sit below the install layer and default empty, and no release workflow sets VITE_V1_DEV_ROUTES; the CI image is loaded, not pushed.
  • Lens 15: biome ci on src/v1, Router.jsx and vite.config.js reports 0 errors and 5 warnings, all on lines that are pre-existing or only re-indented (verticalsRouter && verticalsRouter(), vite.config.js imports). typecheck:v1, lint:v1 and tsc --noEmit pass.
  • Lens 17: the distribution list is stated twice (vite.config.js and types.ts DISTRIBUTIONS) and the two agree; the merge-result sweep could not run.

Open questions for the author

  • Will the cloud and on-prem builds set VITE_UNSTRACT_DISTRIBUTION, or does P0-T07 replace it before the first restricted entry (#2)?
  • Is P0-T06 sequenced before any go-live task (#9)?
  • Should a second user on a shared browser be redirected by the first user's preference before their session loads (#7)?
  • Is a splat pattern part of the #2355 harness contract? patterns.ts accepts a final *, and #2355's fillPattern does not handle one.

Assumptions made

  • The UN-4271 ticket and the Confluence spec were not opened; intent is judged from the PR body and the code.
  • The cloud-repo workflows were read from the local unstract-cloud checkout (last commit 2026-11-02), not from its remote.
  • Behaviour was verified under happy-dom and by reading React Router 6.30 sources, not in a real browser.

cost: agents=8 wall=08:23 lenses-examined=14 of 19 files=28 lines=2400

Comment thread frontend/src/v1/app/VersionSwitch.tsx Resolved
Comment thread docker/dockerfiles/frontend.Dockerfile Resolved
Comment thread frontend/src/v1/app/api.ts Resolved
Comment thread frontend/src/v1/app/VersionSwitch.tsx Resolved
Comment thread frontend/src/routes/Router.jsx Resolved
Comment thread frontend/src/v1/README.md Resolved
Comment thread frontend/src/routes/Router.jsx Resolved
Comment thread frontend/src/v1/lib/oldVersion.ts Resolved
Comment thread frontend/src/v1/stores/oldVersion.ts Resolved
jaseemjaskp and others added 2 commits November 11, 2026 10:32
- The v1 error boundary clears a failure when the pathname changes, as
  v0's backstop does with resetKeys; it stays keyed by page.
- Rename VITE_UNSTRACT_DISTRIBUTION to VITE_DISTRIBUTION, the name P0-T07's
  plugin manifest (UN-4286, #2394) uses. README and Dockerfile say the cloud
  and on-prem builds do not set it yet.
- Tests: resolveUrl (choice, last user, declared-not-live, undeclared), the
  boundary's stale-chunk reload and its ten-second window, the boundary
  reset, load()'s OSS fallback and DEV throw, a numeric and an empty user id.
- README: registering before P0-T06 ships an unguarded page; the same-page
  overlap rule allows identical routes.
- Docstrings and comments: setOldVersion may last for the session only;
  resolveUrl reads the store when called; v1 styles are in the global
  stylesheet (cssCodeSplit: false); Router says "live", names P0-T09.
- Remove unused selectOldVersion; drop redundant vi.restoreAllMocks (the
  config sets restoreMocks); toHaveLength for the storage check (S5906).
@github-actions

Copy link
Copy Markdown
Contributor

Frontend Lint Report (Biome)

✅ All checks passed! No linting or formatting issues found.

@jaseemjaskp

Copy link
Copy Markdown
Contributor Author

@vishnuszipstack thanks for the review. Each finding is answered in its thread, and 850ebfa has the fixes. Your open questions:

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

Unstract test results

Per-group results

Status Group Tier Passed Failed Errors Skipped Duration (s)
⚪ e2e-agent-kv e2e 0 0 0 1 0.0
✅ e2e-api-deployment e2e 3 0 0 0 11.6
✅ e2e-coowners e2e 1 0 0 0 0.9
✅ e2e-etl e2e 1 0 0 0 17.6
✅ e2e-login e2e 2 0 0 0 0.7
✅ e2e-prompt-studio e2e 1 0 0 0 8.6
✅ e2e-smoke e2e 2 0 0 0 0.8
✅ e2e-workflow e2e 1 0 0 0 17.5
✅ frontend unit 664 0 0 0 11.9
✅ frontend-v1-unit unit 539 0 0 1 12.8
✅ integration-backend integration 1710 0 0 26 72.8
✅ integration-connectors integration 1 0 0 7 7.2
✅ integration-workers integration 181 0 0 1 48.4
❌ ui e2e 0 1 0 0 0.0
✅ unit-agent-kv-schema unit 143 0 0 0 1.3
✅ unit-backend unit 2183 0 0 1 43.8
✅ unit-connectors unit 119 0 0 0 8.5
✅ unit-core unit 289 0 0 0 2.5
✅ unit-filesystem unit 1 0 0 0 3.0
✅ unit-platform-service unit 22 0 0 0 1.9
✅ unit-rig unit 139 0 0 0 4.0
✅ unit-runner unit 12 0 0 0 2.1
✅ unit-sdk1 unit 1358 0 0 0 36.5
✅ unit-tool-sidecar unit 8 0 0 0 1.2
✅ unit-workers unit 1675 0 0 2 108.8
✅ unit-x2text-service unit 116 0 0 0 3.3
TOTAL 9171 1 0 39 427.4

Critical paths

⚠️ Critical paths not yet covered

  • workflow-execution-fan-out — Multi-file workflow execution fans out to file-processing workers and rejoins. (declared coverage: no groups declared)
✅ Covered critical paths
  • auth-login — covered by e2e-login
  • adapter-register-llm — covered by integration-backend
  • workflow-author — covered by integration-backend
  • co-owner-manage — covered by integration-backend, e2e-coowners
  • workflow-create-execute — covered by e2e-workflow
  • api-deployment-provision — covered by integration-backend
  • api-deployment-auth — covered by integration-backend
  • api-deployment-run — covered by e2e-api-deployment
  • mcp-server-auth — covered by integration-backend
  • mcp-platform-auth — covered by integration-backend
  • platform-key-whoami — covered by integration-backend
  • prompt-studio-author — covered by integration-backend
  • prompt-studio-fetch-response — covered by e2e-prompt-studio
  • connector-register-test — covered by integration-backend
  • pipeline-etl-execute — covered by e2e-etl
  • usage-aggregate-read — covered by integration-backend
  • usage-token-tracking — covered by e2e-api-deployment
  • callback-result-delivery — covered by e2e-api-deployment

@jaseemjaskp
jaseemjaskp merged commit ed416ff into main Nov 11, 2026
29 of 30 checks passed
@jaseemjaskp
jaseemjaskp deleted the UN-4271-version-switch-route-registry branch November 11, 2026 05:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

main-only Stays on main: not backported to a release line

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants