star-chart-search-enhancer/docs/superpowers/plans/2026-04-15-star-chart-market-visible-page-columns-implementation.md
opencode 8f44e157f1 feat: add market page div-grid support with after-search-rate columns
- Support both HTML table and div-based grid layouts on creator/market
- Harden DOM insertion by using actual parent nodes (prevents NotFoundError
  when page nesting differs from test fixtures)
- Skip malformed/empty table rows instead of throwing on missing action cell
- Add rowKey to BatchLoaderRow to align LoadRowsOptions typing
- Add tests for div-grid sync and controller lifecycle at document_start
2026-04-15 18:04:10 +08:00

451 lines
18 KiB
Markdown

# Star Chart Market Visible Page Columns Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add two after-search-rate columns to the Xingtu creator market list page, auto-load them for the current visible result page, and support cache-backed retryable row rendering without regressing the existing detail-page experiment.
**Architecture:** Keep the existing detail-page hook flow intact, split the content entry into route-aware controllers, and implement the market-page feature entirely inside `src/content/market/`. Market-page data loading uses a dedicated same-origin API client plus a list-sequence-aware controller so DOM sync, cache reuse, retry, and stale-result suppression stay deterministic and testable.
**Tech Stack:** TypeScript, Vitest, jsdom, tsup, Chrome Extension Manifest V3
---
## Implementation Notes
- Current workspace already contains working detail-page code, build scripts, and tests. This plan assumes incremental change, not greenfield scaffolding.
- Current workspace is still not a git repository, so each “commit” step is replaced with a verification note.
- This plan only covers stage 1: current visible result page enhancement.
- Follow TDD strictly: failing test first, then minimal code.
- Do not route the market-page feature through `src/page/hook.ts` unless runtime evidence proves content-script requests are blocked.
- Market async flows must not cache raw `HTMLElement` references across request boundaries. Cache/request layers may store only row metadata such as `authorId`, `listSeq`, and list signature.
- The market API client should expose deterministic error reasons: `timeout` for `AbortError`, `request-failed` for network/non-2xx/parse failures, and `bad-response` for structurally valid responses missing either target field.
- Every async writeback must re-check current DOM markers (`data-sces-author-id`, `data-sces-list-seq`) before rendering, instead of trusting a stale row reference captured earlier.
## Planned File Map
- Modify: `src/manifest.json`
- Add market-page match coverage while preserving detail-page coverage.
- Modify: `src/content/index.ts`
- Reduce to route bootstrap and controller selection.
- Create: `src/content/detail/index.ts`
- Hold the existing detail-page controller logic currently living in `src/content/index.ts`.
- Create: `src/content/market/index.ts`
- Orchestrate market-page DOM sync, list sessions, loading, cache reuse, and retry.
- Create: `src/content/market/api-client.ts`
- Build request URLs, issue same-origin fetches with timeout, and map the known API response into normalized rates.
- Create: `src/content/market/batch-loader.ts`
- Coordinate concurrency-limited loading, stale-result suppression, and row-level retry.
- Create: `src/content/market/cache-store.ts`
- Store success cache and in-flight request dedupe entries by `authorId`.
- Create: `src/content/market/dom-sync.ts`
- Find the target table, insert headers/cells, and bind row metadata markers.
- Create: `src/content/market/id-extractor.ts`
- Extract `authorId` from row links and fallback attributes.
- Create: `src/content/market/list-signature.ts`
- Produce deterministic list/session signatures from current rows and URL state.
- Create: `src/content/market/row-render.ts`
- Render loading, success, and error states into the two injected cells.
- Create: `src/content/market/row-state.ts`
- Define row-status types and transition helpers.
- Create: `src/shared/normalize-rate-value.ts`
- Normalize known rate shapes such as `<0.02%` and `0.02 - 0.1%`.
- Modify: `src/shared/extract-after-search-rates.ts`
- Reuse the extracted shared normalizer so detail-page behavior stays consistent.
- Modify: `tests/build-layout.test.ts`
- Extend manifest/build assertions for the market route.
- Modify: `tests/content-bridge.test.ts`
- Point detail-page tests at the extracted detail controller module.
- Create: `tests/content-entry.test.ts`
- Cover route bootstrap behavior for detail, market, and unsupported URLs.
- Create: `tests/market-api-client.test.ts`
- Cover request URL generation, timeout handling, and response mapping.
- Create: `tests/market-id-extractor.test.ts`
- Cover author-id extraction from row fixtures.
- Create: `tests/market-dom-sync.test.ts`
- Cover header/cell insertion and duplicate protection.
- Create: `tests/market-row-render.test.ts`
- Cover row rendering transitions and retry affordance.
- Create: `tests/market-batch-loader.test.ts`
- Cover cache reuse, in-flight dedupe, concurrency, and retry behavior.
- Create: `tests/market-controller.test.ts`
- Cover list changes, stale-result suppression, and cache-backed rehydration.
- Modify: `tests/readme.test.ts`
- Require README coverage for the market-page feature.
- Modify: `README.md`
- Document market-page support and manual verification.
### Task 1: Split the Content Entry Into a Route Bootstrap and a Detail Controller
**Files:**
- Modify: `src/content/index.ts`
- Create: `src/content/detail/index.ts`
- Modify: `tests/content-bridge.test.ts`
- Create: `tests/content-entry.test.ts`
- [ ] **Step 1: Write the failing route-bootstrap tests**
Add tests asserting:
- detail URLs bootstrap the detail controller
- non-detail, non-market URLs no-op cleanly
- the detail controller still injects the page hook exactly once
- [ ] **Step 2: Run the tests to verify they fail**
Run: `npm test -- tests/content-entry.test.ts tests/content-bridge.test.ts`
Expected: FAIL because route bootstrap and extracted detail controller do not exist yet
- [ ] **Step 3: Implement the minimal route bootstrap split**
Implement:
- `createDetailContentController` in `src/content/detail/index.ts`
- route matching in `src/content/index.ts`
- no-op behavior on unsupported URLs
- updated imports in the existing detail tests
- [ ] **Step 4: Run the tests to verify they pass**
Run: `npm test -- tests/content-entry.test.ts tests/content-bridge.test.ts`
Expected: PASS
- [ ] **Step 5: Record the verification note**
Record which tests passed and that detail-page behavior remained intact
### Task 2: Extend Manifest Coverage to the Market Page
**Files:**
- Modify: `src/manifest.json`
- Modify: `tests/build-layout.test.ts`
- [ ] **Step 1: Write the failing manifest assertions**
Extend the build-layout test to assert:
- the content script matches both detail and market URLs
- the emitted JS asset list is unchanged
- `web_accessible_resources` for the detail hook still stay present
- [ ] **Step 2: Run the test to verify it fails**
Run: `npm test -- tests/build-layout.test.ts`
Expected: FAIL because the manifest only targets the detail page
- [ ] **Step 3: Implement the minimal manifest update**
Implement:
- add `https://*.xingtu.cn/ad/creator/market*` to `content_scripts.matches`
- keep existing detail-page matches and page-hook asset coverage
- [ ] **Step 4: Run the test to verify it passes**
Run: `npm test -- tests/build-layout.test.ts`
Expected: PASS
- [ ] **Step 5: Verify the build still works**
Run: `npm run build`
Expected: PASS and `dist/manifest.json` includes the market match
- [ ] **Step 6: Record the verification note**
Record the passing build-layout test and build output
### Task 3: Add the Shared Rate Normalizer and the Market API Client
**Files:**
- Create: `src/shared/normalize-rate-value.ts`
- Modify: `src/shared/extract-after-search-rates.ts`
- Create: `src/content/market/api-client.ts`
- Create: `tests/market-api-client.test.ts`
- Modify: `tests/extract-after-search-rates.test.ts`
- [ ] **Step 1: Write the failing API-client and normalization tests**
Cover:
- normalizing `<0.02%` without changing it
- normalizing `0.02 - 0.1%` into `0.02% - 0.1%`
- mapping the known API fields into the two display fields
- returning `bad-response` when either field is missing
- returning `timeout` for aborted requests and `request-failed` for non-OK/network failures
- issuing fetch with `credentials: "include"` and a timeout signal
- [ ] **Step 2: Run the tests to verify they fail**
Run: `npm test -- tests/market-api-client.test.ts tests/extract-after-search-rates.test.ts`
Expected: FAIL because the shared normalizer and market API client do not exist yet
- [ ] **Step 3: Implement the minimal shared normalizer and API client**
Implement:
- `normalizeRateValue`
- market request URL builder for `get_author_ase_info`
- response mapper for `avg_search_after_view_rate` and `personal_avg_search_after_view_rate`
- explicit error classification for timeout vs request-failed vs bad-response
- reuse the shared normalizer from the detail extractor
- [ ] **Step 4: Run the tests to verify they pass**
Run: `npm test -- tests/market-api-client.test.ts tests/extract-after-search-rates.test.ts`
Expected: PASS
- [ ] **Step 5: Record the verification note**
Record that both the new market mapper tests and the existing detail extractor tests passed
### Task 4: Add Author-ID Extraction and List-Signature Utilities
**Files:**
- Create: `src/content/market/id-extractor.ts`
- Create: `src/content/market/list-signature.ts`
- Create: `tests/market-id-extractor.test.ts`
- [ ] **Step 1: Write the failing extractor tests**
Cover:
- extracting `authorId` from a row detail link
- extracting from fallback row attributes when available
- returning an explicit missing-id result when no stable source exists
- producing a deterministic list signature from author IDs plus relevant URL state
- [ ] **Step 2: Run the test to verify it fails**
Run: `npm test -- tests/market-id-extractor.test.ts`
Expected: FAIL because the market row extraction utilities do not exist yet
- [ ] **Step 3: Implement the minimal extractor utilities**
Implement:
- detail-link parsing via `getStarIdFromUrl`
- fallback attribute parsing
- explicit `missing-author-id` results
- deterministic list-signature generation
- [ ] **Step 4: Run the test to verify it passes**
Run: `npm test -- tests/market-id-extractor.test.ts tests/get-star-id.test.ts`
Expected: PASS
- [ ] **Step 5: Record the verification note**
Record the passing extractor and shared ID tests
### Task 5: Insert the Two Columns and Render Row States
**Files:**
- Create: `src/content/market/dom-sync.ts`
- Create: `src/content/market/row-render.ts`
- Create: `src/content/market/row-state.ts`
- Create: `tests/market-dom-sync.test.ts`
- Create: `tests/market-row-render.test.ts`
- [ ] **Step 1: Write the failing DOM and rendering tests**
Cover:
- inserting two headers before the `操作` column
- inserting two cells before the action cell for each row
- tagging injected cells with stable `data-*` markers
- avoiding duplicate insertion on repeated sync
- rendering `加载中...`, success values, and `加载失败`
- exposing retryable error rows without creating per-cell state divergence
- avoiding any design that requires batch/cache layers to hold row elements after the sync pass ends
- [ ] **Step 2: Run the tests to verify they fail**
Run: `npm test -- tests/market-dom-sync.test.ts tests/market-row-render.test.ts`
Expected: FAIL because the market DOM modules do not exist yet
- [ ] **Step 3: Implement the minimal DOM sync and row rendering**
Implement:
- table/header lookup
- injected header/cell creation
- row-state types
- row rendering with shared row-level status
- ephemeral DOM references only inside the sync/render step
- [ ] **Step 4: Run the tests to verify they pass**
Run: `npm test -- tests/market-dom-sync.test.ts tests/market-row-render.test.ts`
Expected: PASS
- [ ] **Step 5: Record the verification note**
Record the passing DOM and row-render test output
### Task 6: Add Cache, In-Flight Dedupe, Concurrency Control, and Row Retry
**Files:**
- Create: `src/content/market/batch-loader.ts`
- Create: `src/content/market/cache-store.ts`
- Create: `tests/market-batch-loader.test.ts`
- Modify: `src/content/market/api-client.ts`
- Modify: `src/content/market/row-state.ts`
- [ ] **Step 1: Write the failing batch-loader tests**
Cover:
- current page rows auto-enter loading state
- same `authorId` reuses success cache
- same `authorId` in-flight requests are deduplicated
- concurrency cap is honored
- failed rows transition to `加载失败`
- clicking one failed cell retries the whole row
- loader outputs can be dropped safely when the consumer reports a newer `listSeq`
- [ ] **Step 2: Run the test to verify it fails**
Run: `npm test -- tests/market-batch-loader.test.ts`
Expected: FAIL because cache/dedupe/loader behavior does not exist yet
- [ ] **Step 3: Implement the minimal cache and loader pipeline**
Implement:
- in-memory cache entries keyed by `authorId`
- in-flight request reuse
- concurrency-limited scheduling
- row retry behavior
- timeout and request-failed transitions
- result delivery based on row metadata and callbacks, not long-lived DOM node ownership
- [ ] **Step 4: Run the test to verify it passes**
Run: `npm test -- tests/market-batch-loader.test.ts tests/market-api-client.test.ts`
Expected: PASS
- [ ] **Step 5: Record the verification note**
Record the passing loader and API-client tests
### Task 7: Build the Market Controller and Activate It on the Market Route
**Files:**
- Create: `src/content/market/index.ts`
- Modify: `src/content/index.ts`
- Create: `tests/market-controller.test.ts`
- Modify: `tests/content-entry.test.ts`
- [ ] **Step 1: Write the failing controller tests**
Cover:
- initial market page auto-load for all current rows
- pagination/filter/search/sort DOM changes trigger a fresh sync
- stale async results do not overwrite a newer list
- cached rows rehydrate immediately when they reappear
- the content entry now selects the market controller on market URLs
- writeback only succeeds when fresh DOM markers still match the returning `authorId` and `listSeq`
- [ ] **Step 2: Run the test to verify it fails**
Run: `npm test -- tests/market-controller.test.ts tests/content-entry.test.ts`
Expected: FAIL because the market controller and route activation do not exist yet
- [ ] **Step 3: Implement the minimal market controller**
Implement:
- table observation
- `listSeq` or equivalent sync-session tracking
- fresh sync on list changes
- stale result suppression before DOM writeback
- re-scan current DOM on each sync instead of holding old row node references
- market route activation in `src/content/index.ts`
- [ ] **Step 4: Run the test to verify it passes**
Run: `npm test -- tests/market-controller.test.ts tests/content-entry.test.ts`
Expected: PASS
- [ ] **Step 5: Run the focused market suite**
Run: `npm test -- tests/market-api-client.test.ts tests/market-id-extractor.test.ts tests/market-dom-sync.test.ts tests/market-row-render.test.ts tests/market-batch-loader.test.ts tests/market-controller.test.ts`
Expected: PASS
- [ ] **Step 6: Record the verification note**
Record that the market suite and route-entry tests passed
### Task 8: Update README and Run Final Verification
**Files:**
- Modify: `README.md`
- Modify: `tests/readme.test.ts`
- [ ] **Step 1: Write the failing README expectation**
Extend README tests to assert documentation now includes:
- market page enhancement scope
- the two inserted column names
- loading/failure/retry behavior
- how to manually verify on `creator/market`
- the fact that detail-page behavior still exists
- [ ] **Step 2: Run the test to verify it fails**
Run: `npm test -- tests/readme.test.ts`
Expected: FAIL because README does not document market-page behavior yet
- [ ] **Step 3: Update README minimally**
Add:
- market page support notes
- manual verification steps for both detail and market pages
- current stage-1 limitations
- [ ] **Step 4: Run the test to verify it passes**
Run: `npm test -- tests/readme.test.ts`
Expected: PASS
- [ ] **Step 5: Run the full verification suite**
Run: `npm test`
Expected: all tests PASS
- [ ] **Step 6: Run a fresh production build**
Run: `npm run build`
Expected: build exits 0 and `dist/` contains the updated extension assets
- [ ] **Step 7: Record the verification note**
Record the passing full test run and build output
---
## Runtime DOM Note
After testing against the real logged-in `creator/market` page, the market result area was confirmed to be a column-major sticky grid instead of a row-major HTML table:
- outer list root: `.base-author-list`
- header root: `.section-wrapper.sticky-header.hide-scrollbar`
- body root: `.section-wrapper.hide-scrollbar`
- left author area: sticky `content-section` with one `content-column`
- middle metrics: `.middle-columns`
- right sticky area: `content-section` containing multiple `.content-column`s, with the last column being `操作`
This means DOM sync cannot treat each result as a `<tr>` or assume the injected cells live under the same row container. The implementation now reconstructs each logical row by cell index across columns and stamps row metadata onto:
- the author-side row anchor cell
- the injected `单视频看后搜率` cell
- the injected `个人视频看后搜率` cell
## Verification Note
Verified in this workspace after the column-major DOM fix:
- `npm test -- tests/market-dom-sync.test.ts tests/market-controller.test.ts`
- `npm test`
- `npm run build`
All commands exited successfully.
## Runtime Stability Note
After manual browser verification on the real `creator/market` page, an additional runtime issue was identified: refreshing the market page could leave the result area blank or stuck loading.
The market controller was then hardened with three runtime constraints:
- do not start observing the full market DOM while `document.readyState === "loading"`
- do not override `history.pushState` or `history.replaceState` on the market page
- coalesce repeated `MutationObserver` callbacks into one scheduled sync cycle instead of launching unbounded concurrent sync passes during page boot
These constraints are now covered by `tests/market-controller.test.ts` together with the existing market-page behavior checks.