Skip to content

UN-4286 [FEAT] Add the v1 plugin manifest, slots and build check - #2394

Open
athul-rs wants to merge 38 commits into
mainfrom
UN-4286-plugin-manifest
Open

athul-rs wants to merge 38 commits into
mainfrom
UN-4286-plugin-manifest

Conversation

@athul-rs

@athul-rs athul-rs commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

What

  • Plugin manifest, slots and registry for Frontend v1 (P0-T07): a plugin declares the routes and slot fillers it adds in plugins/<id>/manifest.ts; shell and pages render <Slot id="…"> from @v1/lib/plugins.
  • plugins/index.ts discovers manifests with import.meta.glob, keeps those for the current VITE_DISTRIBUTION, and builds the registry. PluginProvider hands the slot renderer to lib/ through context, so lib/ keeps importing only lib/.
  • A build check (scripts/v1-plugins.mjs) fails the build when two plugins fill a single-filler slot or claim the same route.
  • .gitignore keeps overlaid cloud plugins out of OSS commits; the frontend Docker build passes VITE_DISTRIBUTION through.

Why

How

  • A slot with no filler renders its fallback, so OSS shows nothing where a cloud plugin would sit. A filler that throws is caught per slot and reported, and the rest of the page renders.
  • skipLibCheck in src/v1/tsconfig.json now comes from the scaffold (UN-4270 [FEAT] v1 scaffold, strict TypeScript and import boundary #2380, 664d1cf), which settled the @types/chai / vitest containSubset clash. This branch merged the scaffold and takes its tsconfig, so the earlier temporary commit no longer changes anything. The scaffold's new rule against dynamic imports it cannot analyse is met by missing-import.test.ts (a template whose fixed prefix names the folder).

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. Everything is under frontend/src/v1/, which no v0 route renders. Outside it: a .gitignore rule for frontend/src/v1/plugins/*, a VITE_DISTRIBUTION build arg (defaults to OSS behaviour when unset) and a Vite plugin that only inspects src/v1/plugins.

Database Migrations

  • None.

Env Config

  • VITE_DISTRIBUTION (build time, optional). Unset means OSS.

Relevant Docs

  • frontend/src/v1/README.md, section "Plugins".

Related Issues or PRs

Dependencies Versions

  • None.

Notes on Testing

  • bun run typecheck:v1, lint:v1, test:v1: clean; 20 vitest tests and 50 node tests (the scaffold's fixtures plus this PR's build-check and CLI tests).
  • The cloud overlay tests (unstract-cloud#1896) pass 7/7 against this branch. The check runs under Bun with no dependencies installed, as the cloud CI overlay step needs.
  • Frontend CI (typecheck, vitest, biome) runs once this PR targets main, i.e. after UN-4270 [FEAT] v1 scaffold, strict TypeScript and import boundary #2380 merges. The commands above were run locally.
  • The fixture tests cover the build check: duplicate route, two fillers on a single slot, a manifest for another distribution.

Screenshots

None: no UI.

Checklist

I have read and understood the Contribution Guidelines.

🤖 Generated with Claude Code

jaseemjaskp and others added 11 commits October 26, 2026 17:51
…N-4270)

Adds frontend/src/v1/ as an isolated tree for the v1 frontend (P0-T01):

- Folder taxonomy from the spec, a public entry (src/v1/index.ts, imported
  by v0 as `@v1`) and a cloud overlay placeholder.
- src/v1/tsconfig.json: strict, no JS, unused locals/params are errors,
  `@v1` alias only. v0's tsconfig/jsconfig exclude src/v1.
- Biome overrides for the two-way import boundary and v1 bans (no `../`,
  `@/`, prop-types, antd, component CSS; noExplicitAny).
- scripts/check-v1.mjs for what Biome cannot express: TypeScript-only
  files, the folder taxonomy and import contract, PropTypes, `ant-`.
- typecheck:v1, lint:v1 and test:v1 scripts; v0 scripts are unchanged.
- test-fixtures/v1-boundary/ proves each rule fails and passes as
  specified, run with node --test so the v0 vitest suite never sees it.
- .gitignore re-includes src/v1/lib/ (the Python `lib/` rule matched it).

v0 build output, test counts, typecheck and Biome results are unchanged.
…270)

check-v1.mjs:
- Scan comments with a string/template-aware tokenizer; a `/*` or `//`
  inside a string or line comment no longer hides the imports after it.
- Read template-literal specifiers, import.meta.glob and `import(..., opts)`.
- Flag specifiers that leave src/v1 (`/`, `..`, `@/`, `src/`) itself, not
  only via Biome, which skips template literals.
- Check `@import`/`url()` and `ant-` in styles/ CSS; report symlinks;
  catch built `ant-` names; treat .test/.spec .mts/.cts as test files;
  ignore .DS_Store; drop the unused --root option.

biome.json: also ban `..`, `./../**`, `/**`, `antd/**`, `prop-types/**`.
tsconfig: skipLibCheck off so v1's own .d.ts files are checked.
.gitignore: re-include lib/ at any depth under src/v1.

Fixtures: a generated matrix over every folder-contract pair, every import
form, every outside spelling, a symlink case, and cases for each new rule;
tsc cases assert error codes; output matching excludes the command text and
spawn errors fail loudly. Comment and README corrections.
… (UN-4270)

- ci-frontend-lint: the typecheck job also runs `typecheck:v1` and
  `lint:v1`, so the strict v1 tsconfig and the scripts/check-v1.mjs checks
  (folder contract, TypeScript-only files, PropTypes, `ant-`) gate PRs now
  rather than from P0-T10.
- Pages may no longer import app/, test/ or the v1 entry, only the shared
  folders (components, shell, services, stores, hooks, lib, styles,
  plugins). Test files in a page may still import test/. Contract table,
  README and the fixture matrix updated together.
…(UN-4270)

check-v1.mjs: split the comment scanner into small helpers (cognitive
complexity 40 -> under 15), drop the nested ternary and nested template
literal, use String.raw for the import regexes, keep CLI flags in a Set.

Fixture runner:
- Cases and the expected folder contract are parsed text tables instead of
  repeated object literals (the 10% duplication).
- Sorts use an explicit localeCompare comparator.
- The contract matrix is built by a helper.
- Spawns /bin/sh by absolute path and no longer needs `git init` (Biome only
  needs the ignore file). The git check-ignore call keeps a PATH lookup, with
  the reason and a NOSONAR marker.

Fixture cases: re-exports rewritten as small functions and read-only props,
without changing what each case exercises.
…nnot see (UN-4270)

Addresses Greptile review on #2380:
- import.meta.glob([...]): every pattern in the array is checked, not only
  the first, so an allowed first pattern no longer hides a forbidden one.
- check-v1 --code also scans v0 (src/ outside v1, including the cloud
  src/plugins overlay) for template-literal imports, globs, require(),
  @/v1, src/v1, relative paths and CSS @import that reach into v1 other
  than through `@v1`. Biome still covers plain imports.

Fixtures: Greptile's example as a case, a glob array with a forbidden later
pattern, and a generated test of every v0 -> v1 form (denied and allowed).
The v0 scan reports nothing on the current tree.
v1 finds plugins at build time from src/v1/plugins/<id>/manifest.ts, which
the cloud overlay copies in. An absent plugin has no manifest, so its
slots show their fallbacks with no error; any other load or render error
is reported, and each slot has its own error boundary.

- lib/plugins: the typed slot contract, buildRegistry (distribution
  filter plus conflict checks for every distribution), <Slot> and
  pluginRoutes(). VITE_DISTRIBUTION picks oss, cloud or onprem.
- scripts/v1-plugins.mjs runs in every Vite build: a slot filled twice, a
  route defined twice or a plugin with no distribution fails the build,
  and the distribution matrix is published as plugin-matrix.json.
- v0's optional-plugin stub no longer applies under src/v1, where it
  turned a missing module into an empty object instead of an error.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The cloud workflows set it to cloud or onprem; the default, oss, keeps the
public image unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The cloud overlay copies plugins into frontend/src/v1/plugins/; ignore
them as frontend/src/plugins/* already is, keeping only the .gitkeep.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… lib through context

The scaffold allows lib/ to import only lib/, and only plugins/ may see the
overlaid manifests. plugins/index.ts now builds the registry and exports
PluginSlot and pluginRoutes(); lib/plugins keeps the contract and a Slot that
reads PluginSlot from PluginProvider, which app/ will mount. With no provider
every slot shows its fallback.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…hai clash

TEMPORARY. With skipLibCheck off, @types/chai 5.2.3 and vitest 3.2.6 both
declare Chai containSubset, so typecheck:v1 fails once any v1 test imports
vitest. Drop this commit when #2380 settles it (pin @types/chai or turn the
option on).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Comment thread frontend/scripts/v1-plugins.mjs Fixed
athul-rs and others added 3 commits October 28, 2026 12:11
…ts with esbuild

The build check now transpiles the registry and each manifest with esbuild,
so it runs on the Node versions the frontend supports. It also fails on a
folder with no manifest.ts, resolves argv[1] before the main-guard compare,
and has CLI tests. The README notes the distribution matrix.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… the check before installing dependencies

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@greptile-apps

greptile-apps Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

via Greptile

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The reviewed changes appear safe to merge within the documented scope.

Summary

Adds plugin manifests, distribution filtering, slot rendering, and a build-time conflict check.

  • V1 slots keep optional plugin content separate from the rest of the page.
  • The v1 registry exposes routes and slot fillers from distribution-specific plugin manifests.
  • Vite builds stop when a plugin manifest conflicts with the v1 app.

Reviews (12) · Last reviewed commit: "UN-4286 [FIX] Address review: check mani..." · Reviewed by Greptile

Comment thread frontend/scripts/v1-plugins.mjs Resolved
jaseemjaskp and others added 5 commits October 29, 2026 10:38
…ins in the ignore check (UN-4270)

Reported by Athul while building #2394/#2395 on this branch:
- src/v1/tsconfig.json: skipLibCheck back on, as in tsconfig.json. Off, tsc
  also checked third-party typings, and @types/chai 5.2.3 and vitest 3.2.6
  both declare `containSubset`, so any v1 test importing vitest failed
  typecheck:v1. A fixture test now imports vitest to keep this covered.
- The "no v1 file is git-ignored" check skips src/v1/plugins/<id>/: cloud
  plugins copied there are git-ignored on purpose (#2394).
…and test slot recovery in place

- v1-plugins.mjs: with no --dir, `flag + 1` was 0, so the first argument
  (the output file) was skipped and the matrix went to stdout. A fixture
  now covers the file argument with and without --dir.
- Slot.test.tsx: the "recovers in place" test wrapped the slot in a <div>,
  which remounted it, so the boundary's resetKey reset never ran. It now
  re-renders the same slot; with resetKey={id} the test fails.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
From Ali's, Athul's and Vishnu's reviews on #2380, each reproduced first:

- tsconfig/jsconfig map `@v1/*` too: once v0 imports the entry, tsc follows
  it into src/v1, where cross-folder `@v1/...` imports must resolve
  (would break `bun run typecheck` at UN-4271). Lint keeps v0 on `@v1`.
- check-v1 scanner tracks regex literals, so a `/*` inside one no longer
  hides the rest of the file; CSS blanks only block comments (`//` is URL).
- Imports are read by argument: literals, templates by their fixed prefix,
  every glob pattern, and new URL(..., import.meta.url). In v1 an import the
  guard cannot read fails closed; a computed v1 path is allowed only in
  folders that may import everything (the lazy route registry in app/).
- v0 specifiers are normalized before the check (/src/v1, @/./v1, ...).
- v1 styles may not @import antd packages.
- Biome's v0 rule matches only alias spellings of src/v1, so `uuid/v1` or
  `@/api/v1` no longer fail; relative spellings are check-v1's (in CI).
- CI runs `test:v1` (fixtures) on Node 22; setup-biome pinned to a SHA
  (SonarCloud S7637).

Fixtures assert the exact message and count per form, cover each fix, and
check that comments, JSX and look-alike class names stay quiet. Against the
previous guard, 9 of the new tests fail.
Static files (icons, logos, images) get one home, src/v1/assets/, imported
as @v1/assets/... (e.g. logo.svg?react). v1 cannot reach v0's src/assets/.
Raised by Ali on #2380: Lane I's P1-T22 needs SVG icons and provider logos,
and a .svg outside the taxonomy failed --files.

components/ui, components/patterns, shell, pages and styles may import
assets/; assets/ imports nothing but itself (room for a generated icon map).
README, contract table and the fixture matrix updated together;
v1-allowed-imports imports an SVG as a React component under typecheck:v1.
…s (UN-4270)

Greptile on #2380: `export const example = "import(name)";` failed lint:v1,
because the call matcher also ran over string contents. Static import text
in a string ('import x from "@/helpers/x"') and in templates or regexes was
read as an import too.

The comment scanner now also returns a code-only view (string, template
and regex bodies blanked, offsets kept). Statements and calls are located
there; the specifier is read from the real text at the same offset. The
import-form test gains a quiet case with import text in a string, a
template and a regex; it fails against the previous guard.
greptile-apps[bot]
greptile-apps Bot previously approved these changes Oct 29, 2026
athul-rs added a commit that referenced this pull request Nov 9, 2026
athul-rs and others added 2 commits November 10, 2026 13:56
The v1 vitest config never loads optionalPluginImports, so the old
missing-import test passed with either guard removed. The fixture calls
both resolvers: a v1 importer is never stubbed, a v0 one still is.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
jaseemjaskp added a commit that referenced this pull request Nov 11, 2026
- 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).
jaseemjaskp added a commit that referenced this pull request Nov 11, 2026
* feat(frontend-v1): version switch and route registry (UN-4271)

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).

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix(frontend-v1): address self-review of the version switch (UN-4271)

- 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.

* fix(frontend-v1): reject same-page route overlaps, encode demo links (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).

* feat(v1): mount the Toaster in the v1 root

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.

* fix(frontend-v1): address review of the version switch (UN-4271)

- 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.

* fix(frontend-v1): address the version switch review (UN-4271)

- 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).

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Resolves frontend/package.json (keep main's seam, separation, bundle and
unit-gate scripts plus this branch's plugin fixtures) and the frontend
Dockerfile (main's VITE_V1_DEV_ROUTES/VITE_DISTRIBUTION build args). Main now
passes VITE_DISTRIBUTION="" when unset, so the plugin registry treats an
empty value as oss instead of throwing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

@ritwik-g ritwik-g 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.

Thanks, the contract is clean and the cloud overlay (unstract-cloud#1896) fits it: check(dir), the matrix fields, the distribution names and the folder = id rule all match. I also merged this branch into current main locally: the only conflict is the test:v1 line in frontend/package.json (keep both unit-gate.fixtures.mjs and scripts/v1-plugins.fixtures.mjs), and main's new v1 checks (lint with warnings as errors, typecheck, separation, bundle, seam) and all 177 node fixtures pass on the merged tree.

Requesting changes for two spec rules from P0-T07 that are not met yet (OSS route clashes, reporting through the notification contract), a retry loop on failed chunk loads, the dev image never getting VITE_DISTRIBUTION, and untested distribution logic. Details inline.

Not tied to a line

  • Stale references to the removed plugins/manifest.ts: the coverage comment in frontend/vitest.v1.config.mts:42-44 still calls it "OSS's placeholder plugins/manifest.ts" (it is now the loader index.ts), and main's frontend/test-fixtures/v1-boundary/git-case.sh:20 still writes the old !frontend/src/v1/plugins/manifest.ts ignore rule. Worth updating both when resolving the conflict with main. (Fixtures still pass.)
  • Acceptance "An OSS build renders an OSS page with empty slots; a cloud build fills them" can't be shown yet since no v1 page or provider exists. Could the Jira ticket say which task carries it?

Comment thread frontend/src/v1/lib/plugins/registry.ts Resolved
Comment thread frontend/src/v1/plugins/index.ts Resolved
Comment thread frontend/src/v1/lib/plugins/Slot.tsx Resolved
Comment thread docker/dockerfiles/frontend.Dockerfile Resolved
Comment thread frontend/src/v1/plugins/index.ts Resolved
Comment thread frontend/src/v1/README.md Resolved
Comment thread frontend/src/v1/plugins/index.ts Resolved
Comment thread frontend/vite.config.js Resolved
Comment thread frontend/src/v1/README.md Resolved
athul-rs added a commit that referenced this pull request Nov 11, 2026
…2, return focus, handle a removed organisation

- Restore the v1 README's Plugins section, lost in the #2394 merges.
- A 412 with a code goes to /error?code=&domain= as v0 does; an empty body still goes to /landing.
- Closing the confirm dialog puts focus back on the organisation pill.
- A 403 on switch says access was removed, keeps Switch disabled and reloads the list; other failures are logged.
- Loading and load errors use Skeleton and InlineAlert; the dialog error uses InlineAlert.
- Switcher panel shadow-md (dropdown), confirm dialog shadow-xl (dialog).
- Log when sign-out falls back to the GET logout.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Comment thread frontend/src/v1/plugins/index.ts Resolved
…report through notify, back off failed chunks

- The build check and the runtime registry reject a plugin route on a page the
  v1 route registry declares (demo pages included), naming OSS and the plugin.
- A plugin failure is logged and raises one error notification per slot.
- A slot whose chunk failed waits 30 s before fetching it again and reports
  once until a load succeeds, so re-renders no longer refetch and re-report.
- The development image stage takes VITE_DISTRIBUTION, so a dev-live overlay
  runs as its distribution.
- plugin-matrix.json names the build's distribution; the Vite plugin is
  exported and tested (conflict fails buildStart, matrix emitted, plugin wired,
  unknown distribution fails the config).
- plugins/index.ts is in core coverage; README and headers describe the wiring
  P2-T01 still has to do; stale manifest.ts references updated.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@athul-rs

Copy link
Copy Markdown
Contributor Author

Review body: b3a667a updates the coverage comment in vitest.v1.config.mts and the ignore rule in test-fixtures/v1-boundary/git-case.sh (!frontend/src/v1/plugins/index.ts). The "OSS page with empty slots, cloud build fills them" acceptance can be shown once P2-T01 mounts the shell with PluginProvider and P2-T13's product switcher fills shell.product-switcher. That belongs with P2-T01, which wires PluginProvider.

@ritwik-g ritwik-g 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.

Re-reviewed b3a667a: 7 of the 9 threads are resolved, and the new guards are covered by tests. Two are partly done (route matching should use patternsOverlap, and README.md:188 still contradicts the matrix sentence), and there is one new issue inline on Slot.tsx:119.

Comment thread frontend/src/v1/lib/plugins/Slot.tsx Resolved
… error

The plugin route check took raw strings, so /:org/v1-demo, a case variant
or /:orgName/* passed against the OSS page /:orgName/v1-demo. buildRegistry
now takes an overlap rule; the app and the build check pass the route
registry's patternsOverlap, and the build check bundles entries.ts and
patterns.ts with esbuild under Node instead of transpiling them alone.

A render error after a successful load left the slot marked reported for the
life of the tab, so later errors there were silent. Only failed loads are
deduplicated now. The README matrix bullet is rewritten to match.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
CodeQL flagged the helper's single-occurrence replace as incomplete
sanitization. A fixed /billing rule says the same thing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

@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 2394 — UN-4286 Add the v1 plugin manifest, slots and build-time plugin check

Mode: INITIAL, at the PR's current head. Reviewed head 5cc28d787200f53196f14c238ef894310dbffb39, merge base ed416ff66d99d5bda904d1036a37a2482657361d, base freshened (base_fetch: ok), 23 files, 1460 lines (matches GitHub). Rubric: unstract plugin v2.1.80.
How this was done: a full fan-out (code reviewer, silent-failure hunter, type design, comments, lifecycle and a whole-diff cold read) ran at an earlier head c015c2537. Before posting, every finding was re-checked against this head. Findings already fixed, already raised by another reviewer, or wrong on a second read were dropped (17 dropped: 10 already raised by ritwik-g and fixed in b3a667a or 5cc28d7, 2 more fixed, 5 withdrawn). The author's own commits since c015c2537 were reviewed for new defects, and the tests were re-run here.
Security deep-dive: degraded — the vendored security prompt ran once at this head, its filter step inline instead of as independent passes; no findings at confidence >= 8.

Verdict — APPROVE WITH NITS

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

The review rounds' fixes hold, including the overlap matching and per-error reporting in 5cc28d7. One Medium is left: nothing checks the shape of a cloud manifest. The root tsconfig.json excludes src/v1, the build check strips types, and assertDeclared looks only at ids, distributions and slot keys. So a cloud manifest with an undefined slot, a non-function load or a route without path passes both repos and fails only at render or routing time. One Low is new in 5cc28d7 (plugin route paths skip patternProblem). The rest are comment accuracy, an undeclared esbuild, a hand-copied distribution list, and fixture hygiene. Tests at this head, from frontend/: the changed vitest files pass (4 files, 31/31), the node fixtures pass (183/183), and so does the Bun branch of the build check (21/21). typecheck:v1, lint:v1 and biome ci on the 17 changed files all exit 0; biome's 4 warnings and 3 infos are in vite.config.js, and only one of those lines is new here. The full test:v1, vite build and real-browser chunk retries were not run.

Unanchored findings

  • PR description is out of date (Low). The description says the scaffold's dynamic-import rule "is met by missing-import.test.ts", but 365c924 replaced that file with the resolver fixture in boundary.fixtures.mjs. It also still describes an "earlier temporary commit" for skipLibCheck, and gives "20 vitest tests and 50 node tests" from before the review rounds. It says the fixture tests cover a "duplicate route", but the plugin-vs-plugin route case is tested only in registry.test.ts; the fixtures cover plugin-vs-OSS routes. Please refresh the Testing notes against the current head.

Findings

Posted inline on the diff; index:

    1. [Medium] [Lens 7] — Cloud manifests are never type-checked, and the registry does not check their shape — frontend/src/v1/lib/plugins/registry.ts:32
    1. [Low] [Lens 3] — Plugin route paths skip patternProblem, so the overlap check can miss clashes — frontend/src/v1/lib/plugins/registry.ts:87
    1. [Low] [Lens 10] — Manifest load errors lose their cause and always blame import type — frontend/scripts/v1-plugins.mjs:65
    1. [Low] [Lens 16] — The contract comments say pages never import a plugin; the boundary check allows it — frontend/src/v1/lib/plugins/index.tsx:2
    1. [Low] [Lens 16] — plugins/index.ts header still says every import error is reported — frontend/src/v1/plugins/index.ts:2
    1. [Low] [Lens 16] — jest-dom.d.ts names v0's setup file — frontend/src/v1/test/jest-dom.d.ts:1
    1. [Low] [Lens 2] — The distribution list is copied by hand, though the route registry already exports it — frontend/src/v1/plugins/index.ts:22
    1. [Low] [Lens 14] — esbuild is used at build time but not declared — frontend/scripts/v1-plugins.mjs:35
    1. [Low] [Lens 13] — Fixture uses a fixed shared tmp path and the bare CLI run reads the real plugins folder — frontend/scripts/v1-plugins.fixtures.mjs:62

Lens checklist

  1. Spec & intent — Clean
  2. Architectural fit & precedent — See findings #7
  3. Correctness & edge cases — See findings #2
  4. Security — Not covered — the security deep-dive ran degraded (single pass, filters inline) and found nothing at confidence >= 8, which is not a clean bill
  5. Data integrity & migrations — N/A — no persisted data or schema
  6. Concurrency — Clean
  7. API & contract compatibility — See findings #1
  8. Reliability & resilience — Clean
  9. Performance & cost — Clean
  10. Observability — See findings #3
  11. Operational safety — Clean
  12. LLM/agent-specific — N/A — no model, prompt or agent path
  13. Testing — See findings #9
  14. Dependencies & build — See findings #8
  15. Code quality — Clean
  16. Doc & comment accuracy — See findings #4, #5, #6
  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 — N/A — no chart, values or on-prem build path touched
  19. Scaling & workload lifecycle — N/A — no worker or scaling config

Comment thread frontend/src/v1/lib/plugins/registry.ts Outdated
Comment on lines +32 to +55
function assertDeclared(manifests: readonly PluginManifest[]): void {
const seen = new Set<string>();
for (const m of manifests) {
if (seen.has(m.id)) {
throw new PluginConflictError(`plugin id ${m.id} is used twice`);
}
seen.add(m.id);
if (m.distributions.length === 0) {
throw new PluginConflictError(`${m.id} declares no distributions`);
}
for (const d of m.distributions as readonly string[]) {
if (!(PLUGIN_DISTRIBUTIONS as readonly string[]).includes(d)) {
throw new PluginConflictError(
`${m.id} declares unknown distribution ${d}`,
);
}
}
for (const id of Object.keys(m.slots ?? {})) {
if (!(SLOT_IDS as readonly string[]).includes(id)) {
throw new PluginConflictError(`${m.id} fills unknown slot ${id}`);
}
}
}
}

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.

1. [Medium] [Lens 7] — Cloud manifests are never type-checked, and the registry does not check their shape

  • Location: frontend/src/v1/lib/plugins/registry.ts:L32-L55
  • Only satisfies PluginManifest under tsc enforces the shape of a manifest: each slot value and route load is a loader function, and each route has a string path. The build check strips types (esbuild transform under Node, a direct import under Bun). The root frontend/tsconfig.json excludes src/v1, so cloud CI's bun run typecheck never sees the overlaid manifests. In OSS, typecheck:v1 covers src/v1/plugins, but that folder is empty here. assertDeclared checks ids, distributions and slot keys only. A cloud manifest with slots: { "shell.product-switcher": undefined }, a non-function load, or a route without path therefore passes both repos. It fails only at render or routing time. An explicit undefined slot also claims the slot in collect, so a real filler for it fails as a conflict. Either have the overlay run tsc -p src/v1/tsconfig.json after copying and say so in the README, or add typeof load === "function" and typeof path === "string" checks to assertDeclared.
  • Verification: VERIFIED — read at this head: frontend/tsconfig.json excludes src/v1, and assertDeclared throws only on ids, distributions and slot keys

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 736790646. The registry now rejects a slot value that is not a function (an explicit undefined included), a route load that is not a function and a route without a string path, naming the plugin and the slot or route; tests cover each case in registry.test.ts and in the build-check fixtures.

Comment on lines +87 to +98
for (const route of m.routes ?? []) {
const taken = owned.find(([path]) => overlaps(path, route.path));
if (taken) {
const [path, owner] = taken;
const what =
path === route.path ? route.path : `${route.path} (overlaps ${path})`;
throw new PluginConflictError(
`route ${what} is defined by both ${owner} and ${m.id}`,
);
}
owned.push([route.path, m.id]);
routes.push({ ...route, owner: m.id });

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.

2. [Low] [Lens 3] — Plugin route paths skip patternProblem, so the overlap check can miss clashes

  • Location: frontend/src/v1/lib/plugins/registry.ts:L87-L98
  • patternsOverlap is exact only for patterns that pass patternProblem: absolute paths, static segments, :params and a final *. validate.ts runs that check on every page route. Plugin routes go straight into overlaps(path, route.path) with no check. A plugin route /:orgName/v1-demo/:id? is compared segment by segment. Its :id? meets the end of /:orgName/v1-demo, patternsOverlap returns false, and the build passes. React Router still matches /acme/v1-demo for both routes, so the plugin silently shadows the OSS page or is shadowed by it. A path without a leading / or with a mid-path * also gets in. The README now promises the check catches "two routes the router would match for the same URL". Reject a plugin route whose path fails the registry's pattern rule. Pass the validator in next to overlaps (lib/ cannot import app/), and add a fixture for :id?.
  • Verification: VERIFIED — read at this head

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 736790646. buildRegistry now takes { ossRoutes, overlaps, patternProblem }, both callers pass the route registry's patternProblem, and a plugin route it refuses fails the build; fixtures cover /:orgName/v1-demo/:id? and a path without a leading /.

Comment on lines +65 to +72
try {
mod = await importManifest(file);
} catch (err) {
throw new Error(
`${rel} could not be loaded (${err.message}). A manifest may only ` +
'`import type`; load components with `() => import("./X")`.',
);
}

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.

3. [Low] [Lens 10] — Manifest load errors lose their cause and always blame import type

  • Location: frontend/scripts/v1-plugins.mjs:L65-L72
  • The catch in load() throws a new Error that keeps only err.message. The stack, esbuild's location frames and the cause are dropped, and the CLI handler (L148-L151) prints only err.message again. Every failure also gets the hint "A manifest may only import type": a syntax error, a TypeError thrown while the manifest evaluates, or esbuild failing to import under Node. A developer with a typo on line 40 is steered toward import rules. Pass { cause: err }, and add the hint only for module-resolution failures (ERR_MODULE_NOT_FOUND, "Cannot find module", esbuild's "Could not resolve"). In the CLI catch, print err.stack or err.cause as well.
  • Verification: VERIFIED — read at this head

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 736790646. The wrapped error keeps the original as cause, the import type hint appears only for module-resolution failures, and the CLI prints the cause with its stack; a syntax-error fixture checks that it gets no hint.

Comment thread frontend/src/v1/lib/plugins/index.tsx Outdated
Comment on lines +2 to +4
* v1's extension-point contract. Shell and pages render `<Slot>` from here;
* they never import a plugin. The registry itself is built in
* `@v1/plugins` (the only folder that may see plugin manifests). The v1 shell

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.

4. [Low] [Lens 16] — The contract comments say pages never import a plugin; the boundary check allows it

  • Location: frontend/src/v1/lib/plugins/index.tsx:L2-L4
  • This header and the README Plugins paragraph (README.md:L181) say pages "never import a plugin" and call @v1/plugins "the only folder that may see plugin manifests". CONTRACT.pages in scripts/check-v1.mjs lists "plugins", and the README import table lets pages/ import plugins/. app, entry and test may import everything. So neither sentence is enforced. A page that reaches into plugins/<id>/ passes lint:v1, and a reader who trusts the comment assumes it would be caught. The README folder table (README.md:176, outside this diff) also still says plugins/ holds "only a placeholder", though it now ships the loader index.ts. Either drop plugins from CONTRACT.pages and the table so the prose becomes true, or remove the two claims and update the table row.
  • Verification: VERIFIED — read at this head

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 736790646. Both claims are removed (the lib/plugins/index.tsx header and the README Plugins paragraph), and the README row for plugins/ now says it holds the loader index.ts.

Comment thread frontend/src/v1/plugins/index.ts Outdated
Comment on lines +2 to +5
* Builds the plugin registry from the manifests the cloud overlay copies in
* as `plugins/<id>/manifest.ts`. An absent plugin has no manifest, so its
* slots show their fallbacks; every import error left is a real failure and
* is reported. Nothing renders `PluginSlot` or `pluginRoutes()` yet: the v1

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.

5. [Low] [Lens 16] — plugins/index.ts header still says every import error is reported

  • Location: frontend/src/v1/plugins/index.ts:L2-L5
  • The header says "every import error left is a real failure and is reported". Manifests load through an eager import.meta.glob, so a broken manifest import fails the build or module evaluation and never reaches reportPluginError. Only filler load and render errors caught by SlotBoundary are reported. Narrow it to "a filler that fails to load or render is reported", or drop the clause. The makeSlot docstring already describes this correctly.
  • Verification: VERIFIED — read at this head

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 736790646. The header now says a filler that fails to load or render is reported.

Comment thread frontend/src/v1/test/jest-dom.d.ts Outdated
@@ -0,0 +1,2 @@
// jest-dom matchers for v1 tests; src/setupTests.js registers them at run time.

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.

6. [Low] [Lens 16] — jest-dom.d.ts names v0's setup file

  • Location: frontend/src/v1/test/jest-dom.d.ts:L1
  • The comment says src/setupTests.js registers the matchers at run time. That is v0's setup file (vitest.config.mjs). v1 tests run src/v1/test/setup.ts (the setupFiles in vitest.v1.config.mts), which imports @testing-library/jest-dom/vitest. Point the comment at src/v1/test/setup.ts.
  • Verification: VERIFIED — read at this head

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 736790646. The comment now points at src/v1/test/setup.ts.

Comment thread frontend/src/v1/plugins/index.ts Outdated
type RouteEntry,
} from "@v1/lib/plugins";

const DISTRIBUTIONS: readonly Distribution[] = ["oss", "cloud", "onprem"];

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.

7. [Low] [Lens 2] — The distribution list is copied by hand, though the route registry already exports it

  • Location: frontend/src/v1/plugins/index.ts:L22
  • DISTRIBUTIONS here is typed readonly Distribution[], so it still type-checks if a distribution is added to PLUGIN_DISTRIBUTIONS and missed here. The same list is written out again in vite.config.js (with a "mirrors" comment) and in the matrix keys in scripts/v1-plugins.mjs:L112-L116. If a new distribution is missed in one copy, VITE_DISTRIBUTION=<new> builds but then throws at module load, or the matrix leaves out a key the cloud overlay indexes (matrix.distributions[distribution]). app/registry/types.ts already exports DISTRIBUTIONS as a const tuple. Import it here, and build the matrix keys from ["oss", ...PLUGIN_DISTRIBUTIONS].
  • Verification: VERIFIED — read at this head

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 736790646. plugins/index.ts and the matrix keys in scripts/v1-plugins.mjs now come from DISTRIBUTIONS in app/registry/types.ts. vite.config.js keeps its own list because the config loads synchronously, and a new fixture checks that it accepts every distribution the matrix lists.

Comment on lines +35 to +45
return import("esbuild")
.then(({ build }) =>
build({
entryPoints: [file],
bundle: true,
write: false,
format: "esm",
platform: "node",
}),
)
.then((r) => import(dataUrl(r.outputFiles[0].text)));

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.

8. [Low] [Lens 14] — esbuild is used at build time but not declared

  • Location: frontend/scripts/v1-plugins.mjs:L35-L45
  • Under Node, vite build runs this check, which imports esbuild. Since 5cc28d7 the check also uses esbuild's build (bundling) API, not just transform. esbuild is not in frontend/package.json. It resolves only because it is hoisted as Vite's dependency. An isolated or non-hoisted install would fail every build in buildStart. Add esbuild to devDependencies, pinned to Vite's range.
  • Verification: VERIFIED — read at this head

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 736790646. esbuild is a devDependency at Vite's range (^0.27.0), with a one-line bun.lock change.

});

test("no plugins directory gives an empty matrix", async () => {
assert.deepEqual(await check(path.join(os.tmpdir(), "v1-plugins-none")), {

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.

9. [Low] [Lens 13] — Fixture uses a fixed shared tmp path and the bare CLI run reads the real plugins folder

  • Location: frontend/scripts/v1-plugins.fixtures.mjs:L62
  • The "no plugins directory" case passes os.tmpdir()/v1-plugins-none. That path stays absent only by convention: if a leftover or a concurrent run creates it, the test fails. None of the mkdtemp directories the other cases create is removed. The bare CLI case (L243-L248) runs without --dir, so it checks the repo's own src/v1/plugins. In a cloud-overlaid checkout, any broken overlaid manifest fails this OSS fixture. Use a fresh mkdtemp path, removed afterwards, for the "none" case, and clean up the temp dirs. Point the bare run at a fixture through an env override or cwd.
  • Verification: VERIFIED — read at this head

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 736790646. Every fixture directory is a fresh mkdtemp removed after the run, including the "no plugins directory" case. The bare CLI run reads a fixture through a new V1_PLUGINS_DIR override instead of the repository's src/v1/plugins.

…, keep load-error causes

- registry: reject a slot value or route load that is not a function and a
  route without a string path; buildRegistry takes an options object
  (ossRoutes, overlaps, patternProblem) and rejects plugin routes the
  pattern rule refuses (default: must start with /)
- plugins/index.ts and scripts/v1-plugins.mjs pass the route registry's
  patternProblem; both take DISTRIBUTIONS from app/registry/types.ts
- v1-plugins.mjs keeps the load error as cause, adds the import-type hint
  only for module-resolution failures, and the CLI prints the cause
- declare esbuild as a devDependency (Vite's range, ^0.27.0)
- fixtures: temp dirs removed afterwards, fresh path for the "none" case,
  bare CLI run reads V1_PLUGINS_DIR; new cases for shape, :id?, relative
  paths, syntax errors and Vite accepting every distribution
- comments: drop unenforced "pages never import a plugin" claims, update
  the plugins/ README row, narrow the loader header, fix the jest-dom note

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@github-actions

Copy link
Copy Markdown
Contributor

Frontend Lint Report (Biome)

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

@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.1
✅ e2e-api-deployment e2e 3 0 0 0 18.1
✅ e2e-coowners e2e 1 0 0 0 1.1
✅ e2e-etl e2e 1 0 0 0 10.1
✅ e2e-login e2e 2 0 0 0 1.0
✅ e2e-prompt-studio e2e 1 0 0 0 10.6
✅ e2e-smoke e2e 2 0 0 0 0.8
✅ e2e-workflow e2e 1 0 0 0 18.0
✅ frontend unit 673 0 0 0 13.1
✅ frontend-v1-unit unit 698 0 0 1 20.5
✅ integration-backend integration 1823 0 0 26 79.5
✅ integration-connectors integration 1 0 0 7 7.3
✅ integration-workers integration 181 0 0 1 44.8
❌ ui e2e 0 1 0 0 0.0
✅ unit-agent-kv-schema unit 143 0 0 0 1.1
✅ unit-backend unit 2191 0 0 1 41.4
✅ unit-connectors unit 119 0 0 0 7.5
✅ unit-core unit 289 0 0 0 2.0
✅ unit-filesystem unit 1 0 0 0 2.7
✅ unit-platform-service unit 22 0 0 0 1.8
✅ unit-rig unit 139 0 0 0 3.5
✅ unit-runner unit 12 0 0 0 1.9
✅ unit-sdk1 unit 1358 0 0 0 29.5
✅ unit-tool-sidecar unit 8 0 0 0 1.1
✅ unit-workers unit 1675 0 0 2 105.8
✅ unit-x2text-service unit 116 0 0 0 2.5
TOTAL 9460 1 0 39 425.8

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

@ritwik-g ritwik-g 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.

All threads resolved, and each fix is covered by a test. Approving. Thanks, Athul.

@athul-rs
athul-rs requested a review from ritwik-g November 12, 2026 13:55
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.

5 participants