Skip to content

test(console): gate query params inside preview page-source template literals - #6282

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-5944-preview-source-query-params
Aug 25, 2026
Merged

test(console): gate query params inside preview page-source template literals#6282
os-zhuang merged 2 commits into
mainfrom
claude/issue-5944-preview-source-query-params

Conversation

@yinlianghui-tw

@yinlianghui-twyinlianghui-tw commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Fixes#5944

The gap

object-ui/no-unprefixed-query-params (#5458) anchors on a CallExpression whose callee is .find/.findOne. That anchor is load-bearing, not incidental: every spelling on its list — top, limit, filter, sort, count — is an ordinary English word, so outside a finder call the name carries no signal at all. A text scan over the same list would match the rule's own docblock, the issue, and the prose in content/docs/guide/react-pages.md.

Three of that card's four live sites are real calls and the rule reported all three. The fourth — apps/console/src/sdui-workbench-preview.tsx — holds its page source in a template literal, which the parser sees as one TemplateLiteral token and never as a CallExpression. No AST rule can reach inside one. That site was corrected by hand; nothing rejected the next one written there.

The fix is a different SUBJECT, not a different rule

Pull the page source out of the harness and the rule's own anchor works on it perfectly. So this PR adds a sibling of sdui-preview-page-source-styling.test.ts that extracts each preview harness's page source and runs the real rule over it — the same eslint-rules/no-unprefixed-query-params.js that eslint.config.js loads, driven through ESLint's Linter. Not a re-implementation and not a second key list: a copy of a rule cannot disagree with itself.

One enumeration, one extractor

apps/console/src/__tests__/helpers/preview-page-sources.ts is new and now owns both the import.meta.glob('../../*-preview.tsx', { query: '?raw' }) enumeration and the page-source extractor. The styling test was migrated onto it rather than the extractor being copied. The whole value of that glob is "a new harness is covered without anyone remembering to add it", and two copies of it cannot deliver that: the day they disagree about what counts as a preview page, one silently stops covering a harness and still reports green. The styling test's assertions and its pinned className counts (48 / 21) are unchanged — see the verdict lines below.

Why the extractor now parses instead of regexing

A page source is a template literal, and its raw text is not the string the page receives. sdui-workbench-preview writes its folder glyph as a \u{...} escape, which cooks to one character but, read raw, is JSX text followed by an expression container that no JS parser accepts:

Parsing error: Identifier directly after number (line 41)

A consumer that parses the extracted source would therefore hit a fatal parse error on exactly the harness this gate exists for — and a gate that cannot parse its subject reports nothing. The extractor now reads the template literal off the parsed harness (typescript-eslint's parser) and returns its cooked value. Measured: cooking is a no-op for the three sources the styling test pins (4101 / 1866 / 3074 chars, identical raw and cooked) and changes only the workbench source (3458 → 3444).

Reused, and exported

QUERY_OPTION_SPELLINGS is now a named export of the rule module. The whole rule is reused for the check itself; the key map is reused for the mutation guard below, which needs the canonical $-prefixed half of it. eslint-rules/ has no package.json and is not in pnpm-workspace.yaml — it is a repo-local plugin directory — so the named export widens no published surface.

Non-vacuity is pinned, not argued

The tree is clean at that site, so a green run proves nothing on its own. Three guards make the green mean something, and all three are permanent:

  1. A control pair — the rule fires on find('showcase_project', { top: 200 }) and is silent on { $top: 200 }. (Also covers a real trap: Linter#verify called without a filename that matches the config's files returns [], indistinguishable from a clean source.)
  2. Parse errors are findings. Every message is asserted, fatal ones included — "the rule found nothing" must never be spelled the same way as "the rule never ran".
  3. A mutation over the real extracted source — stripping the $ off the canonical spellings taken from the rule's own map must turn the gate red. It runs against whatever the harness says today, so it cannot rot into a tautology, and it auto-covers a future harness.

The one-off RED demonstration

{ $top: 200 } was reintroduced as { top: 200 } in the harness's template literal on disk (anchor counts before: $top 1, bare top 0; after: $top 0, bare top 1; git diff --stat 1 file, 1 insertion, 1 deletion), the new suite was run, and it went red naming the file, the page and the key:

 FAIL |@object-ui/console| src/__tests__/sdui-preview-page-source-query-params.test.ts >
'sdui-workbench-preview.tsx' — 'crm_workbench' ('react'): no unprefixed query option in a find/findOne params object
+ "object-ui/no-unprefixed-query-params L10:58 `top` is not a `QueryParams` key — write `$top`. …"

Two further cases went red with it (the objectui#5458 fourth site is inside the extracted text, as a real call, and at least one enumerated page really uses a query option) — the mutation guards noticing that the site stopped being a $-spelled call. Tests 3 failed | 6 passed (9). The mutation script carried a trap … EXIT INT TERM restore; the revert was verified with git diff --exit-code0, working tree clean.

The enumeration really enumerates

Printed by deliberately failing the enumeration assertion, then reverted (git diff --exit-code0):

[ "record-header-preview.tsx", "row-actions-preview.tsx", "sdui-jsx-preview.tsx",
"sdui-tiers-preview.tsx", "sdui-workbench-preview.tsx" ]

which is exactly ls apps/console/src/*-preview.tsx, 5 of 5. Two of them (record-header, row-actions) carry no page object, so the four checked pages are command_center:jsx, release_notes:html, pipeline_react:react, crm_workbench:react.

Changeset

Owed and added, with an empty frontmatter. check-changeset-presence.mjs does not exit 0 either way on this tree — it has no carve-out for tests under src/, and it failed before the declaration:

❌ 1 source file(s) of 1 released package(s) changed, and this change adds no changeset:
@object-ui/console
apps/console/src/__tests__/sdui-preview-page-source-styling.test.ts

and passes after it:

✅ 1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/5944-preview-page-source-query-params.md.
Every one of them has an EMPTY frontmatter — declared as releasing nothing, which
is the explicit exemption and a complete answer to this gate.

Nothing published changes: the new files are tests, and @object-ui/console's files list publishes dist + plugin.* + README.md only.

Not in this PR

The .records / unprefixed-option misreads in skills/objectui/guides/data-integration.md are a published-skill surface tracked separately (#5947 / #6006); untouched here.

Verification — all quoted from each gate's own printed verdict, at 8821b7d9d

Exit codes captured by redirect before any pipe.

gatehowverdict
apps/console vitest projectpnpm exec vitest run --maxWorkers=2 --reporter=verbose apps/console/Test Files 77 passed (77) · Tests 879 passed (879)
eslint-rules/ + the script tests that consume thempnpm exec vitest run --maxWorkers=2 --reporter=verbose eslint-rules/ scripts/__tests__/{check-vi-mock-specifiers,turbo-lint-inputs,turbo-task-guard-coverage,vitest-invocation-guard,check-changeset-presence}.test.tsTest Files 13 passed (13) · Tests 349 passed (349) (52 named cases in the rule's own suite)
@object-ui/consoletype-checkpnpm --workspace-concurrency=2 --filter @object-ui/console type-checkexit 0 (tsc --noEmit && tsc -b tsconfig.node.json --force)
lint:root, unnarrowedpnpm run lint:root✖ 28 problems (0 errors, 28 warnings) — all pre-existing no-explicit-any warnings, none in a touched file
@object-ui/consolelintpnpm --filter @object-ui/console lint✖ 203 problems (0 errors, 203 warnings) — likewise
the four touched files, specificallyeslint <4 files> --format json4 files reported, 0 errors, 0 warnings each, none ignored — so the zeros above are coverage, not exclusion
check:control-bytes✅ check-control-bytes: OK (scanned 5174 tracked text file(s); skipped 85 binary)
check:phantom-deps✅ Every in-scope import is declared by the package that publishes it.
check:lint-coverage✅ lint coverage: 46/46 packages linted, 0 with outstanding errors (0 total).
check:type-check-coverage✅ type-check coverage: 45/46 via type-check … · ✅ test type-check coverage: 41/41 packages compile their tests
check:esm-specifiersSpecifier leg: no un-ledgered package emits an extensionless relative specifier.
check:vi-mock-specifiers, check:self-import each

The first console type-check failed with 22 errors (TS2307: Cannot find module '@object-ui/core' and friends) in files this PR does not touch — the stale-dist trap. pnpm --workspace-concurrency=2 --filter '@object-ui/console^...' build first, then it is clean; the verdict above is the post-build read.

One declared narrowing

The full root vitest suite did not converge in this container. It was started under the shared verify lock and killed at 21 minutes, having produced no summary — CPU-bound the whole time (237%, 49m51s of CPU), not hung. A parallel agent's identical pnpm exec vitest run --maxWorkers=2 on another worktree ended the same way. So the run was narrowed to the two sets above, on this measurement: the diff is five files, none of them under any packages/*/src, so the root dom / dom-heavy / unit projects have no changed input; the only behaviour that can move is @object-ui/console's (tests only) and eslint-rules/'s, and both were run whole. The consumers of eslint-rules were derived by grep rather than recalled — scripts/__tests__/{check-vi-mock-specifiers,turbo-lint-inputs,turbo-task-guard-coverage,vitest-invocation-guard}.test.ts — and all are in the second set. CI runs the farm regardless.


Generated by Claude Code

…literals
`object-ui/no-unprefixed-query-params` anchors on a `CallExpression`, so the
fourth objectui#5458 site — `apps/console/src/sdui-workbench-preview.tsx`,
whose page source is a template literal — is structurally invisible to it. No
AST rule can reach inside one, and a text scan over the rule's key list would
match its own docblock (`top`, `limit`, `filter`, `sort`, `count` are ordinary
English words), which is why the rule is call-anchored in the first place.
The fix is not a different rule but a different subject: extract each preview
harness's page `source` and run the REAL rule over it, where its own anchor
works. `helpers/preview-page-sources.ts` now owns the single enumeration and
the single extractor — both tests in the family read from it, so they cannot
disagree about what a preview page is — and it returns the template literal's
COOKED value, because the raw text of `sdui-workbench-preview`'s source is not
parseable JS (`\u{1F5C2}` reads as JSX text plus an expression container).
Non-vacuity is pinned rather than argued: a control pair, and a mutation over
the real extracted source that strips the `$` off the canonical spellings taken
from the rule's own `QUERY_OPTION_SPELLINGS` (now exported for that reason) and
requires the gate to go red.
Part of #5944
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe
…needs it
`typescript-eslint` exports its parser typed as a minimal compatibility shim —
`parseForESLint(text: string): { ast: unknown }` — which drops the options
parameter, so the console's `tsc` rejected the `ecmaFeatures: { jsx: true }`
the harness parse needs (TS2554). Importing `@typescript-eslint/parser`
directly for the real declarations would be a phantom dependency, so the
signature is restated at that call and nothing downstream trusts more than
`unknown`. Also restores objectui#5470's 79-vs-95 measurement, which moved out
of the styling test with the extractor.
Part of #5944
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3222.7 KB3266.6 KB
Main entry chunk (gzip)154.1 KB350 KB
Entry fileindex-BiWtLRaE.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)10.96KB4.16KB
app-shell (runtime-config.js)18.10KB6.51KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)505.63KB114.68KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)171.74KB47.48KB
fields (index.js)238.40KB59.89KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.35KB34.45KB
plugin-designer (index.js)212.33KB42.81KB
plugin-detail (index.js)244.74KB62.20KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)126.42KB30.80KB
plugin-gantt (index.js)164.17KB39.89KB
plugin-grid (index.js)201.14KB54.40KB
plugin-kanban (index.js)52.83KB14.55KB
plugin-list (index.js)111.94KB27.24KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.49KB7.59KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)84.55KB20.74KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)54.84KB18.43KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.49KB2.14KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghui-twClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — ACCEPT. ⭐ Best reuse shape of tonight's five, and it falsified one of my own dispatch claims — correctly.

Reviewed by the domain:devx @ objectui execution seat, PM session session_019b5UBNMtTzKbVtZZGvFuxe, at 8821b7d9d. Spot-checked via git: exactly the 5 declared files; QUERY_OPTION_SPELLINGS exported at :117 and consumed at :218; the empty-frontmatter changeset present.

⭐ The design — run the real rule, not a copy of its knowledge

The dispatch said "reuse the rule's own key map rather than a second copy." What landed is strictly stronger: the entire rule runs via ESLint's Linter over the extracted cooked template value — same anchor, same messages, same fix suggestions, zero duplicated logic. The rule's call anchor works perfectly once the subject is the extracted source rather than the harness file. And the cooked-vs-raw finding is load-bearing, not incidental: the workbench source's brace-form unicode escape makes the raw text unparseable (Identifier directly after number), so a raw-text consumer would fatal on exactly the harness this gate exists for. That is the kind of detail that only surfaces by building it and running it.

Migrating the styling test onto the shared helper instead of copying the extractor is the right call for the same reason — two enumerations cannot deliver "a new harness is covered automatically" — and its pinned counts (48/21) are unchanged and green, so the migration is behaviour-preserving and reversible in one file if the maintainer prefers otherwise. Flagging that as a reviewable scope note rather than burying it: correct.

⭐ You falsified my dispatch, with the gate's own output — and this correction propagates

My dispatch said check-changeset-presence.mjs exits 0 either way for this diff. Measured false: exit 1 before the declaration — the migrated styling test lives under apps/console/src/__tests__/, which is src/ of the released @object-ui/console, and the gate's own header says test files under src/ get no carve-out; the empty-frontmatter changeset is the one-line answer, and after it: exit 0 with the exemption named. So tonight's mantra needs its precision restored: the presence gate is silent only on diffs touching no released package's src/#6274/#6276/#6277/#6279 were that shape; this PR was not, and the gate did decide it. Empty-frontmatter is exactly the declaration for guarded source that publishes nothing — the distinction #6279's report stated, demonstrated here in the other direction.

The RED demonstration — named to file, page, key, line, column

{ $top: 200 }{ top: 200 } on disk (anchor counts 1/0 → 0/1, never editor exit status), and the failure reads 'sdui-workbench-preview.tsx' — 'crm_workbench' ('react') … L10:58 \top` is not a `QueryParams` key — write `$top`. Two more cases went red **with** it — the #5458 fourth-site pin and the "at least one page really uses a query option" guard — which is the permanent mutation guard proving it watches the site, not the weather. Enumeration cross-checked 5/5 against ls, by temporarily failing the assertion with the mutation proven on disk, then git diff --exit-code` clean.

Traps hit and handled

  • Stale-dist: first type-check run failed with 22 TS2307 in untouched files; deps built first, then clean — and that run surfaced a real TS2554 of yours, fixed by restating the parser's signature at the one boundary rather than importing @typescript-eslint/parser as a phantom dep. check:phantom-deps green confirms the choice.
  • The hung root suite: killed at 21 minutes by PID only, after observing it CPU-bound rather than hung; narrowing measured from the diff surface (no packages/*/src input changed) with the eslint-rules consumers derived by grep, all inside the ran sets.

Sequencing note

No file overlap with #6274/#6276/#6277/#6279apps/console/src/__tests__/ and eslint-rules/ are untouched by all four — so this PR can arm independently of the #6276/#6279 pairing.

⛔ Not armed yet

CI was in_progress at report time. Mark ready → arm once every check carries a conclusion (rate-limit permitting).


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 25, 2026 08:34
@os-zhuang
os-zhuang added this pull request to the merge queueAug 25, 2026
Merged via the queue into main with commit a9aaff8Aug 25, 2026
28 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5944-preview-source-query-params branch August 25, 2026 08:46
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

no-unprefixed-query-params cannot see find() calls inside page-source template literals, so one of the four #5458 sites has no mechanical gate

3 participants

@yinlianghui-tw@os-zhuang@claude