diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 661d44371..135fa6d58 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -109,7 +109,8 @@ pnpm run coverage # Generate coverage reports ### Browser Extension Specifics - **Manifest V3** with service worker background - **User Scripts API** for script injection (Chrome/Edge) -- **Offscreen API** for DOM access in background contexts +- **Offscreen API** for DOM access in background contexts — **Firefox MV3 has no Offscreen API**; + `EventPageOffscreenManager` runs the same DOM-dependent logic inside the background event page instead - **Declarative Net Request** for script installation interception ## Critical Integration Points @@ -136,7 +137,8 @@ pnpm run coverage # Generate coverage reports - Use `pnpm run dev:noMap` for incognito window development - Background script changes require extension reload - Message passing debugging available in service worker console -- Sandbox script execution isolated from page context - use `unsafeWindow` for page access +- Sandbox (background/scheduled scripts) has no page at all — not "isolated from" a page, it never has one. + `unsafeWindow` belongs to the **Inject** context (page scripts), not Sandbox ## File Structure Patterns - Tests co-located with source files (`.test.ts` suffix) diff --git a/docs/DOC-MAINTENANCE.md b/docs/DOC-MAINTENANCE.md index 64679645e..75608bec7 100644 --- a/docs/DOC-MAINTENANCE.md +++ b/docs/DOC-MAINTENANCE.md @@ -111,6 +111,29 @@ for doc in AGENTS.md docs/README.md docs/DOC-MAINTENANCE.md docs/develop.md docs done ``` +Anchor integrity — the check above only confirms the **target file** exists; a `file.md#anchor` link's fragment +can still rot silently if the target heading is renamed. This second pass re-derives each anchor from the +target's actual headings using a best-effort GitHub-slug approximation (lowercase; drop everything but letters, +digits, spaces, hyphens, underscores; each space becomes its own hyphen — consecutive spaces do **not** collapse, +which is why `## Commit & Pull Request Guidelines` slugs to `commit--pull-request-guidelines` with a double +hyphen). **ASCII-only:** it cannot slugify CJK/non-ASCII headings correctly, so anchors into those headings +(e.g. `translation.md#各语言术语规范--per-locale-terminology`) need manual verification instead. + +```bash +slugify() { printf '%s\n' "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[`]//g; s/[^a-z0-9 _-]//g; s/[[:space:]]/-/g'; } +for doc in AGENTS.md docs/README.md docs/DOC-MAINTENANCE.md docs/develop.md docs/references/develop-testing.md docs/design.md docs/references/design-tokens.md docs/references/design-components.md docs/references/design-patterns.md docs/architecture.md docs/references/architecture-services.md docs/references/architecture-data.md docs/references/architecture-gm-api.md docs/references/architecture-execution.md docs/references/architecture-build.md docs/verification.md docs/references/verification-debugging.md docs/references/verification-report-template.md docs/cloud-sync.md docs/translation.md; do + sed '/^```/,/^```/d' "$doc" | sed -E 's/`[^`]*`//g' | grep -oE '\]\([^)]+#[^)]+\)' | sed -E 's/^\]\(|\)$//g' | while read -r link; do + tf="${link%%#*}"; anchor="${link#*#}" + target="$doc"; [ -n "$tf" ] && target="$(dirname "$doc")/$tf" + [ -e "$target" ] || { echo "BROKEN-FILE $doc → $link"; continue; } + found=0 + while IFS= read -r h; do [ "$(slugify "$h")" = "$anchor" ] && found=1 && break; done \ + < <(grep -E "^#{1,6} " "$target" | sed -E 's/^#{1,6} +//') + [ "$found" -eq 1 ] && echo "ok $doc → $link" || echo "BROKEN-ANCHOR $doc → $link (target: $target)" + done +done +``` + ## When you find a discrepancy Fix the **doc** to match the code — the code on this branch is the source of truth. The exception: if the code diff --git a/docs/architecture.md b/docs/architecture.md index cbe6f5f7a..830502c73 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -219,7 +219,7 @@ Map a change onto the existing extension points instead of inventing new structu - **A new GM API.** Decorate the method with `@GMContext.API` on the content side, add a privileged/offscreen handler if needed, register the `@grant` (see [GM API system](./references/architecture-gm-api.md#adding-a-gm-api-sketch)). -Follow the engineering principles in [`AGENTS.md`](../AGENTS.md): fix root causes (no `as any` / swallowed +Follow the engineering principles in [`AGENTS.md`](../AGENTS.md#engineering-principles): fix root causes (no `as any` / swallowed errors), prefer direct replacement over adapter sandwiches, and keep scope tight — three similar lines beat a premature abstraction. diff --git a/docs/design.md b/docs/design.md index 54edc4f75..98a51eba3 100644 --- a/docs/design.md +++ b/docs/design.md @@ -10,7 +10,7 @@ | Owned here | Owned elsewhere | | --- | --- | -| Color-token values, semantics, usage → [`tokens.md`](./references/design-tokens.md) | The hard rules that mandate them (no hard-coded colors, hover via pseudo-classes, `cn()` / CVA / `lucide`) → [`DEVELOP.md` UI section](./develop.md) | +| Color-token values, semantics, usage → [`tokens.md`](./references/design-tokens.md) | The hard rules that mandate them (no hard-coded colors, hover via pseudo-classes, `cn()` / CVA / `lucide`) → [`DEVELOP.md` UI section](./develop.md#ui) | | Theming mechanism, `dark:` usage | Commands, structure, coding style, testing, i18n, commit/PR → [`DEVELOP.md`](./develop.md) | | Component palette, variants, selection guidance → [`components.md`](./references/design-components.md) | Process model, message passing, service layers, internals → [`ARCHITECTURE.md`](./architecture.md) | | Layout shell, responsive patterns, **layering (z-index)**, motion, state patterns, **accessibility** → [`patterns.md`](./references/design-patterns.md); **elevation (shadows)** → [`tokens.md`](./references/design-tokens.md#elevation-shadows); page recipe (this doc) | — | diff --git a/docs/pull-request.md b/docs/pull-request.md index b3e4ce25c..620df153a 100644 --- a/docs/pull-request.md +++ b/docs/pull-request.md @@ -1,6 +1,6 @@ # Pull Request Description Guide -This guide defines the detailed PR description format for agents and contributors. The human-facing template at [`../.github/pull_request_template.md`](../.github/pull_request_template.md) intentionally remains lightweight; use it as the starting point and expand its `Description / 描述` section when the change needs more context. +This guide defines the detailed PR description format for agents and contributors. The human-facing template at [`../.github/pull_request_template.md`](../.github/pull_request_template.md) intentionally remains lightweight; use it as the starting point and expand its `Description / 描述` section when the change needs more context. This guide covers the description body only — for the PR **title** (gitmoji + Chinese preferred), see [`develop.md`](./develop.md#commit--pull-request-guidelines). ## Recommended structure diff --git a/docs/references/architecture-build.md b/docs/references/architecture-build.md index 64f29ea00..c0b48ab1d 100644 --- a/docs/references/architecture-build.md +++ b/docs/references/architecture-build.md @@ -1,7 +1,5 @@ # Build pipeline & manifest -## Build Pipeline & Manifest - ### Rspack [`rspack.config.ts`](../../rspack.config.ts) emits one bundle per context/page. Entry points: diff --git a/docs/references/architecture-data.md b/docs/references/architecture-data.md index 171041664..05112f59d 100644 --- a/docs/references/architecture-data.md +++ b/docs/references/architecture-data.md @@ -1,7 +1,5 @@ # Data layer (Repo + DAOs) -## The Data Layer (`Repo` + DAOs) - Persistence is a thin generic over `chrome.storage.local` with an optional in-memory cache, in [`src/app/repo/repo.ts`](../../src/app/repo/repo.ts). diff --git a/docs/references/architecture-execution.md b/docs/references/architecture-execution.md index 717ea0f12..563e60047 100644 --- a/docs/references/architecture-execution.md +++ b/docs/references/architecture-execution.md @@ -1,7 +1,5 @@ # Script execution -## Script Execution - There are three execution paths; all share one **compilation** step. ### Compilation — the `with(){}` sandbox wrapper diff --git a/docs/references/architecture-gm-api.md b/docs/references/architecture-gm-api.md index 19bae745b..42f728f91 100644 --- a/docs/references/architecture-gm-api.md +++ b/docs/references/architecture-gm-api.md @@ -1,7 +1,5 @@ # GM API system -## The GM API System - The `GM_*` / `GM.*` functions a userscript calls are not one function — each is a small client that forwards across contexts to a privileged handler, then streams the result back. The implementation is split: @@ -57,9 +55,11 @@ work. ### Adding a GM API (sketch) 1. Add the method to the content `GMApi` with `@GMContext.API({ alias: "GM.foo" })`; for sync APIs return - directly, for async ones forward via `sendMessage`/`connect`. + directly, for async ones forward via `sendMessage` (single request/reply) — or `connect` only when the reply + is streamed in chunks (progress events, large payloads; see the `GM_xmlhttpRequest` walkthrough above). 2. If it needs privilege (network, cookies, tabs), add the handler on the SW `GMApi` with `@PermissionVerify.API(...)`. -3. If it needs DOM, route through the offscreen GM API instead. -4. Register the `@grant` so the linter and the context builder recognize it (see - [`packages/eslint`](../../packages/eslint)). +3. If it needs DOM, route through the offscreen GM API instead + ([`src/app/service/offscreen/gm_api.ts`](../../src/app/service/offscreen/gm_api.ts)). +4. Register the `@grant` so the linter and the context builder recognize it + ([`packages/eslint/linter-config.ts`](../../packages/eslint/linter-config.ts)). diff --git a/docs/references/architecture-services.md b/docs/references/architecture-services.md index 2c2797bd3..7aeac49a4 100644 --- a/docs/references/architecture-services.md +++ b/docs/references/architecture-services.md @@ -1,7 +1,5 @@ # Service layer -## The Service Layer - Services live under `src/app/service//` — **split by the context they run in** — and hold the domain logic. They are deliberately "dumb plumbing on the outside, smart logic on the inside": construction is pure DI, wiring happens once in a manager, and message handlers are registered in `init()`. diff --git a/docs/references/design-components.md b/docs/references/design-components.md index 6f4ec4f6e..0ff8fc62c 100644 --- a/docs/references/design-components.md +++ b/docs/references/design-components.md @@ -1,8 +1,6 @@ # Component palette -## Component palette & usage - -The shadcn primitives live in [`src/pages/components/ui/`](../../src/pages/components/ui/) — `new-york` style, CSS variables enabled, no class prefix (`components.json`). Icons are always `lucide-react`; class merging is always `cn()` ([`src/pkg/utils/cn.ts`](../../src/pkg/utils/cn.ts)); variants are always CVA — these are the [`DEVELOP.md` UI section](../develop.md) hard rules, not repeated here. This section is "what exists and how to choose." +The shadcn primitives live in [`src/pages/components/ui/`](../../src/pages/components/ui/) — `new-york` style, CSS variables enabled, no class prefix (`components.json`). Icons are always `lucide-react`; class merging is always `cn()` ([`src/pkg/utils/cn.ts`](../../src/pkg/utils/cn.ts)); variants are always CVA — these are the [`DEVELOP.md` UI section](../develop.md#ui) hard rules, not repeated here. This section is "what exists and how to choose." ### Primitives & shared composites diff --git a/docs/references/design-tokens.md b/docs/references/design-tokens.md index 09c614a53..5557dea81 100644 --- a/docs/references/design-tokens.md +++ b/docs/references/design-tokens.md @@ -7,7 +7,7 @@ **Usage:** - Background `bg-`, text `text-`, border `border-`, focus ring `ring-ring`. - Opacity modifiers compose directly: `bg-primary-background/90` (solid primary hover), `ring-destructive/20`, `bg-input/30`. -- **Never hard-code a color value** — see Constraint 1 and [`DEVELOP.md` UI section](../develop.md). For dark-only tweaks use the `dark:` variant. +- **Never hard-code a color value** — see Constraint 1 and [`DEVELOP.md` UI section](../develop.md#ui). For dark-only tweaks use the `dark:` variant. ### Base surfaces & text diff --git a/docs/references/develop-testing.md b/docs/references/develop-testing.md index 2f381c40d..cc8a40c28 100644 --- a/docs/references/develop-testing.md +++ b/docs/references/develop-testing.md @@ -1,18 +1,18 @@ # Testing > The **TDD/BDD-first principle** (write failing tests before implementation; fix code not tests) lives in -> [`AGENTS.md`](../../AGENTS.md) → *Engineering Principles*. This section is the mechanics. +> [`AGENTS.md`](../../AGENTS.md#engineering-principles) → *Engineering Principles*. This section is the mechanics. Vitest + happy-dom. Per-test budgets live in `vitest.config.ts` per project: non-UI projects (`fast`, `isolated`) use 340ms; the `ui` project (`src/pages/**/*.test.{ts,tsx}` — React renders, including `renderHook` tests in `.ts` files) uses 850ms because a render + interaction case genuinely costs 100–200ms solo under coverage and worker parallelism multiplies that (fake-timer countdown cases have been observed at ~630ms under full local load). Don't pass `--test-timeout` on the CLI — it would override every project's -budget at once. Chrome APIs mocked via -`@Packages/chrome-extension-mock` (`tests/vitest.setup.ts`). `MockMessage` available for message-system tests. +budget at once. Chrome APIs are mocked via +`@Packages/chrome-extension-mock` (`tests/vitest.setup.ts`). `MockMessage` is available for message-system tests. `happy-dom` is patched via `patches/` (see `pnpm-workspace.yaml` `patchedDependencies`) to build its invalid-selector `DOMException` lazily — the upstream eager construction captures a deep stack on every -`matches()`/`querySelector()` call and cost ~15% of TSX suite time. +`matches()`/`querySelector()` call and costs ~15% of TSX suite time. - Write failing tests **before** implementation; co-locate `*.test.ts`/`*.test.tsx` next to source (or place in `tests`). - BDD-style Chinese `describe`/`it` titles. Use `describe.concurrent()` / `it.concurrent()` where independent. diff --git a/docs/references/verification-debugging.md b/docs/references/verification-debugging.md index fa3fc6d4f..fc0fb1b9d 100644 --- a/docs/references/verification-debugging.md +++ b/docs/references/verification-debugging.md @@ -3,7 +3,7 @@ ## Five-context debug map A feature can break in any of ScriptCat's five isolated contexts. Match the symptom to where its logs live (deep -model in [`ARCHITECTURE.md`](../architecture.md)): +model in [`ARCHITECTURE.md`](../architecture.md#the-five-contexts-process-model)): | Symptom | Where to look | | --- | --- | @@ -12,6 +12,11 @@ model in [`ARCHITECTURE.md`](../architecture.md)): | Background / scheduled script, DOM-needing GM APIs | **Offscreen** — `dist/ext/src/offscreen.html` context | | Cron scheduling, `with`-sandboxed execution | **Sandbox** — `dist/ext/src/sandbox.html` context | +> **Firefox has no separate Offscreen context.** `chrome.offscreen.createDocument` doesn't exist there, so +> `offscreen.html` is never loaded — `EventPageOffscreenManager` runs the same logic inside the background event +> page instead (see [`ARCHITECTURE.md`](../architecture.md#chrome-vs-firefox-the-offscreen-split)). On Firefox, +> inspect the background/event page, not `offscreen.html`. + In a scratch script, capture the page console (`page.on("console", …)`), take screenshots (`await page.screenshot({ path: "test-results/verify//screenshots/…png" })`), and write any manual notes to `test-results/verify//`. Playwright's automatic failure artifacts also go under diff --git a/docs/references/verification-report-template.md b/docs/references/verification-report-template.md index 0ead5e653..9e5fd2f87 100644 --- a/docs/references/verification-report-template.md +++ b/docs/references/verification-report-template.md @@ -4,8 +4,11 @@ Before running the browser, create a short verification record in the scenario d `test-results/verify//report.md`. Keep the reusable template headings in English, but write the actual record content in the user's language. Update it as the run proceeds instead of filling it in only at the end. +This is the `Evidence Index` section alone, filled in, to show its shape — it is not the full report; the full +report shape (which reuses this same section) is below. + ```md -## Evidence Index +## Evidence Index (filled-in example) ### Screenshots @@ -81,7 +84,8 @@ Use this shape: ## Evidence Index -Embed screenshots inline, link videos / logs / resources, and annotate every item — see the shape above. +Embed screenshots inline, link videos / logs / resources, and annotate every item — see `Evidence Index +(filled-in example)` above. ``` Fill `Result` at the end — the honest verdict, per *Step 4 — Report honestly* in [`verification.md`](../verification.md). diff --git a/docs/translation.md b/docs/translation.md index 600dc91bd..a7bbcc703 100644 --- a/docs/translation.md +++ b/docs/translation.md @@ -50,6 +50,7 @@ - **关键字冲突**:同一页面中关键字相同但翻译不同时,使用 `page.key` 的方式区分。 - 为满足部分扩展市场要求,`chrome.i18n` 语言文件位于 `src/assets/_locales`。 - i18n 方案的实现细节见 [`src/locales/README.md`](../src/locales/README.md)。 +- 自动检查:[`src/locales/i18n-usage.test.ts`](../src/locales/i18n-usage.test.ts)(`pnpm test`)静态扫描代码中的 `t()`/`i18n.t()` 调用,但只按 **zh-CN** 资源解析 key 是否存在;修改其它 locale 时该测试不会给出任何信号,仍需人工核对目标 locale 是否缺 key。 ## 提取翻译提示词 / Extract-translation prompt diff --git a/docs/verification.md b/docs/verification.md index 5a1af17b3..d47ea758c 100644 --- a/docs/verification.md +++ b/docs/verification.md @@ -7,8 +7,14 @@ > scripts and local-only evidence — kept out of Git and never deleted as part of a run; cleanup is the user's call. > > **What this is NOT.** It is *not* the test-suite reference. The mechanics of Vitest unit tests and the -> permanent Playwright E2E suite live in [`DEVELOP.md`](./develop.md) → *Testing*; the TDD-first principle and -> engineering rules live in [`../AGENTS.md`](../AGENTS.md). Read those for writing committed tests. +> permanent Playwright E2E suite live in [`DEVELOP.md`](./develop.md#testing) → *Testing*; the TDD-first principle +> and engineering rules live in [`../AGENTS.md`](../AGENTS.md#engineering-principles). Read those for writing +> committed tests. +> +> **When to skip this guide.** Docs-only, comment-only, and type-only changes need no browser run. When a change +> or bug is fully reproducible in pure service/unit logic — no cross-context wiring, no real Chrome API, no DOM — +> reproduce and verify it with a throwaway Vitest case instead of a scratch script; reach for this guide only when +> the behavior only manifests through the built extension. ## The one rule: verification ≠ growing the E2E suite @@ -20,12 +26,13 @@ you only want to *check that a feature works*, do not pay that cost and do not l - ✅ Write a **throwaway scratch script** under `e2e/scratch/` (git-ignored), run it, and keep any evidence local. Promoting a scenario into the permanent suite is a *separate, deliberate* decision — only when it deserves -permanent regression coverage. That path is owned by [`DEVELOP.md`](./develop.md), not this guide. +permanent regression coverage. That path is owned by [`DEVELOP.md`](./develop.md#testing), not this guide. **Reproducing a bug you intend to fix is *not* "casual verification."** A scratch reproduction is the *确定 bug -存在* step from [`../AGENTS.md`](../AGENTS.md) (确定 bug 存在 → 写测试 → 修复): it confirms the bug is real but does -**not** replace the test. Next, promote it into a *failing* committed test, then fix, then confirm it goes green — -that permanent test is owned by [`DEVELOP.md`](./develop.md) / [`../AGENTS.md`](../AGENTS.md). +存在* step from [`../AGENTS.md`](../AGENTS.md#engineering-principles) (确定 bug 存在 → 写测试 → 修复): it confirms the +bug is real but does **not** replace the test. Next, promote it into a *failing* committed test, then fix, then +confirm it goes green — that permanent test is owned by [`DEVELOP.md`](./develop.md#testing) / +[`../AGENTS.md`](../AGENTS.md#engineering-principles). ## Prerequisite gate (cheap signals first) @@ -89,8 +96,9 @@ sanitize them before saving evidence. Embed screenshots inline with `![alt](screenshots/…png)` plus a caption line, so `report.md` renders as a visual record you can skim without opening files. Link videos, logs, and resources instead — markdown can't inline them. -Never list bare links: every item carries a short note explaining what it proves and how to read it. Before running -the browser, create the record following the Evidence Index shape in the +Never list bare links: every item carries a short note explaining what it proves and how to read it. + +**Before running the browser**, create the record following the Evidence Index shape in the [verification record template](./references/verification-report-template.md). ### Minimal template (drive a UI page) @@ -114,6 +122,13 @@ test("verify: options page opens and renders the script list area", async ({ con }); ``` +To verify a UI change in **both themes** ([design.md](./design.md#theming) requires it), set the theme before +navigating and capture one screenshot per theme: + +```ts +await context.addInitScript(() => localStorage.setItem("lightMode", "dark")); // or "light" +``` + If you need video evidence, enable it explicitly for the run. The shared fixture writes videos only when `E2E_RECORD_VIDEO_DIR` is set: @@ -126,7 +141,7 @@ Playwright finalizes `.webm` files when pages/contexts close at the end of the t produce multiple videos because the harness may open setup pages as well as the page under verification; keep all of them beside the screenshots for the same scenario. -> Scratch copying the inline fixture won't read `E2E_RECORD_VIDEO_DIR` — see [gotchas](./references/verification-debugging.md#common-gotchas). +> A scratch script that copies the inline fixture won't read `E2E_RECORD_VIDEO_DIR` — see [gotchas](./references/verification-debugging.md#common-gotchas). ### Run only your scratch script