15 KiB
OneTalk Rendered Card Readers Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use
subagent-driven-development(recommended) orexecuting-plansto implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: Safely observe and persist normalized order, inquiry, and product card content from OneTalk React Fiber while removing the non-existent storeImageUrl concept.
Architecture: Extract a contract-owned canonical product-reference module shared by base-message and rendered-card validation. Split the MAIN-world card reader from its MutationObserver lifecycle, use one strict dispatcher and three bounded typed projections, then forward the unchanged normalized observation through the existing bridge and durable coordinator.
Tech Stack: TypeScript, Node test runner, Chrome MV3 content scripts, React Fiber runtime data, pnpm workspace.
Spec: .trellis/tasks/09-15-onetalk-rendered-card-readers/{prd.md,design.md,implement.md}
Global Constraints
- MAIN world may output only whitelisted normalized plain data; raw props, action params, callbacks, trace data, DOM/Fiber references, credentials, query strings, and fragments never cross the bridge.
- Identity remains
channelAccountId + conversationId + messageId; no DOM text, ordering, or CSS inference is permitted. - Unknown card templates fail closed. No generic props crawl or active page interaction is allowed.
- Existing bridge, ledger, ACK/conflict state machine, base-message sync, database schema, and Mind events retain their current behavior.
storeImageUrlis removed completely. Products retain onlyproduct.imageUrl.- Before each shared-symbol edit, run GitNexus upstream impact; report HIGH/CRITICAL risk before proceeding.
- The accepted prerequisite repair must preserve the IndexedDB index-refactor invariant: rendered-card pending recovery is account-scoped and index-backed, never a restored full-store scan.
Task 0: Repair the rendered-card ledger consumer omitted by the IndexedDB index refactor
Files:
- Modify:
apps/chrome-extension/src/onetalk/service-worker/storage.ts - Test:
apps/chrome-extension/test/onetalk-sync-storage.test.js - Test:
apps/chrome-extension/test/onetalk-rendered-card-coordinator.test.jsonly if its ledger fake must reflect the required account argument
Root cause: 6be3b839 introduced rendered-card listPending() using readAll; 61df41d replaced readAll with indexed readers but missed this consumer. This is a prerequisite because it blocks extension typecheck.
- Write a failing storage test that calls rendered-card
listPending("account-1")across records for two accounts and terminal/pending states. It must expect only account-1pending_ackrecords and assert the ledger object store made nogetAll()call. - Extend the existing old-version migration fixture with a v9 rendered-card record; run the focused test and record the failure caused by unresolved
readAll. - Increment
ONE_TALK_SYNC_DATABASE_VERSIONto v10. Inonupgradeneeded, create only[channelAccountId, status]and[channelAccountId, conversationId]indexes ononetalk_rendered_card_ledger, without record rewrite, clear, or backfill. - Make
listPending(channelAccountId: string)usereadByIndex(..., [channelAccountId, "pending_ack"]). It must not accept an omitted account or reintroducereadAll. - Include the rendered-card ledger in
clearConversationHistory's existing index-key deletion loop using its account/conversation index. This restores the pre-existing cleanup test and does not change the transaction's scope or status-machine behavior. - Run focused storage/coordinator tests and
pnpm exec tsc --noEmit -p apps/chrome-extension/tsconfig.jsonbefore accepting the prerequisite. Preserve durable-first, first-content-wins, ACK and reconnect behavior.
Task 1: Share canonical product URL validation across content contracts
Files:
- Create:
packages/onetalk-contract/src/product-url.ts - Modify:
packages/onetalk-contract/src/content.ts - Modify:
packages/onetalk-contract/src/index.ts - Test:
packages/onetalk-contract/test/contract.test.ts
Consumes: The current normalizeOneTalkProductUrl host/path/ID rules in content.ts.
Produces:
export type OneTalkProductReference = { sourceUrl: string; productId: string };
export const normalizeOneTalkProductUrl = (value: unknown): OneTalkProductReference | null;
export const isOneTalkProductReference = (value: unknown): value is OneTalkProductReference;
- Step 1: Add failing contract tests for a canonical reference and invalid variants.
assert.deepEqual(normalizeOneTalkProductUrl(validProductUrl), {
sourceUrl: validProductUrl,
productId: "1601456609478",
});
assert.equal(isOneTalkProductReference({ sourceUrl: validProductUrl, productId: "wrong" }), false);
assert.equal(normalizeOneTalkProductUrl(urlWithFragment), null);
- Step 2: Run the contract test before refactoring.
Run: pnpm --filter @trade-message-center/onetalk-contract test
Expected: the new reference-validator assertions fail because no standalone product-reference validator exists.
- Step 3: Move only the current canonical URL parse logic into
product-url.ts.
Keep the current chinese.alibaba.com/product-detail/... path rule and query stripping semantics unchanged. Define isOneTalkProductReference by validating the exact two keys and checking that normalizeOneTalkProductUrl(sourceUrl) returns the same sourceUrl and productId.
- Step 4: Import the normalizer from
content.tsand export the new module throughindex.ts.
The base kind: "product" validation must keep its public behavior. Do not import content.ts from rendered-cards.ts, because content.ts already consumes the rendered-card union.
- Step 5: Re-run the contract suite.
Run: pnpm --filter @trade-message-center/onetalk-contract test
Expected: all base-product URL tests and new canonical-reference tests pass.
Task 2: Narrow rendered-product JSON to the approved product image and reference
Files:
- Modify:
packages/onetalk-contract/src/rendered-cards.ts - Modify:
packages/onetalk-contract/src/index.ts - Test:
packages/onetalk-contract/test/contract.test.ts - Test:
apps/chrome-extension/test/onetalk-page-bridge.test.js - Test:
apps/chrome-extension/test/onetalk-rendered-card-coordinator.test.js
Consumes: OneTalkProductReference and the existing card image/text/quantity validators.
Produces:
type OneTalkRenderedProductContent = {
version: 1;
kind: "rendered_product";
product: OneTalkRenderedCardImage & OneTalkProductReference;
priceDisplay: string;
minimumOrder: OneTalkRenderedCardQuantity;
serviceBadges: string[];
};
- Step 1: Add failing strict-shape tests.
assert.equal(isOneTalkRenderedCardContent(validRenderedProduct), true);
assert.equal(isOneTalkRenderedCardContent({ ...validRenderedProduct, storeImageUrl: null }), false);
assert.equal(
isOneTalkRenderedCardContent({
...validRenderedProduct,
product: { ...validRenderedProduct.product, sourceUrl: "https://img.alicdn.com/item.jpg" },
}),
false,
);
- Step 2: Run focused contract tests to establish failure.
Run: pnpm --filter @trade-message-center/onetalk-contract test
Expected: the valid canonical product reference is rejected by the current image-host validator, and the old shape still accepts storeImageUrl.
- Step 3: Remove
storeImageUrlfrom the type and exact-key check.
Replace isOneTalkRenderedCardImageUrl(product.sourceUrl) with isOneTalkProductReference({ sourceUrl: product.sourceUrl, productId: product.productId }), after the full product object has separately passed its exact-key, image, and title validation. This retains the exact two-key product-reference contract without allowing the rendered product's additional fields to bypass it.
- Step 4: Update every rendered-product fixture or assertion.
Search the entire workspace for storeImageUrl and remove it from valid payloads. Add one negative case at each strict decoder boundary so the removed key cannot silently re-enter through bridge or coordinator fixtures.
- Step 5: Run contract and downstream targeted tests.
Run:
pnpm --filter @trade-message-center/onetalk-contract test
pnpm --filter @trade-message-center/chrome-extension test -- onetalk-page-bridge onetalk-rendered-card-coordinator
Expected: valid product payloads survive cloning/fingerprinting; any storeImageUrl, invalid URL, or mismatched product ID is rejected.
Task 3: Make the Fiber reader independently testable
Files:
- Create:
apps/chrome-extension/src/onetalk/main-page/card-observer/react-card-reader.ts - Modify:
apps/chrome-extension/src/onetalk/main-page/card-observer/entry.ts - Create:
apps/chrome-extension/test/onetalk-rendered-card-reader.test.js - Modify:
apps/chrome-extension/test/onetalk-rendered-card-observer.test.js
Consumes: OneTalkRenderedCardContent, createOneTalkRenderedCardContentFingerprint, normalizeOneTalkProductUrl, readConversationSelection, and OneTalkPageRenderedCardBaseEvidence.
Produces:
export type OneTalkRenderedCardRead = {
observation: OneTalkRenderedCardObservation;
baseEvidence: OneTalkPageRenderedCardBaseEvidence;
};
export const readOneTalkRenderedCard = (
pageWindow: OneTalkPageWindow,
wrapper: Element,
): OneTalkRenderedCardRead | null;
- Step 1: Write fixtures that model bounded React Fiber links.
The order fixture must use productName; inquiry must use cardType: 6, inquiryCardDTO, displayProducts, inquiryContent, and inquiryID; product must use msgType: 101, descendant cardType: 54, productAction.actionParams.url, and visible benefit/promotion strings. Each fixture must expose a selected data-cid matching conversationCode.
- Step 2: Add failing reader assertions for all three valid shapes.
assert.equal(
readOneTalkRenderedCard(page, orderWrapper)?.observation.content.kind,
"rendered_order",
);
assert.equal(
readOneTalkRenderedCard(page, inquiryWrapper)?.observation.content.kind,
"rendered_inquiry",
);
assert.equal(
readOneTalkRenderedCard(page, productWrapper)?.observation.content.kind,
"rendered_product",
);
Assert the full approved output shape, including inquiry action label order and product badge order. Assert that product output has no storeImageUrl, hsfImg, action params, trace data, or raw DTO fields.
- Step 3: Add failing rejection cases.
Cover cardType: 12, unknown 10010 cards, msgType: 101 without renderer cardType: 54, missing template fields, unsafe image/reference URL, incomplete identity, and a selected conversation that changes between before/after reads.
- Step 4: Implement bounded common Fiber and identity helpers.
Move FiberRecord, fiberFor, itemDataFor, text/scalar/image guards, the fixed traversal bounds, and before/after conversation checks into react-card-reader.ts. Return both observation and base evidence so entry.ts does not re-read Fiber state after normalization.
- Step 5: Implement an explicit classifier and three direct template predicates.
const classifier = classifyRenderedCard(item, fiber);
switch (classifier.kind) {
case "order":
return readOrderTemplate(classifier.data);
case "inquiry":
return readInquiryTemplate(classifier.data);
case "product":
return readProductTemplate(classifier.data);
default:
return null;
}
Do not add a generic record walk. Product renderer detection remains bounded to the same Fiber subtree. Deduplicate badges in fixed observed order and only emit nonempty validated text.
- Step 6: Keep
entry.tslifecycle-only and run reader tests.
installOneTalkRenderedCardObserver continues to own WeakSet deduplication, setTimeout(0), root rebinding, pagehide, and mutation discovery. It calls the new reader once and forwards paired observation/base evidence.
Run:
pnpm --filter @trade-message-center/chrome-extension test -- onetalk-rendered-card-reader onetalk-rendered-card-observer
Expected: all valid fixtures normalize exactly once; every lookalike produces no observation.
Task 4: Verify end-to-end boundaries and finalize the change
Files:
- Modify only test files shown by the
storeImageUrland reader-symbol searches. - Modify task artifacts only if implementation changes a documented invariant.
Consumes: Completed contract and reader tests.
Produces: A reviewed, type-safe, buildable reader change with documented runtime evidence limits.
- Step 1: Run targeted bridge and coordinator regressions.
Run:
pnpm --filter @trade-message-center/chrome-extension test -- onetalk-page-bridge onetalk-rendered-card-coordinator onetalk-sync-storage
Expected: normalized observations remain durable-first and are sent only after matching base candidate confirmation.
- Step 2: Run workspace static and build validation.
Run:
pnpm format:check
pnpm typecheck
pnpm build
git diff --check
Expected: no formatting/type/build failure; no server migration or generated artifact change.
- Step 3: Perform GitNexus review and code review.
Run detect_changes() against the active worktree, inspect all changed symbols and affected flows, and check that no change escapes the contract/MAIN reader/test scope. Review the diff for raw-prop leakage, URL fallback, duplicated validation, hidden success paths, and storeImageUrl remnants.
- Step 4: Request permission before user-visible runtime refresh.
Only if runtime confirmation needs new code loaded, ask the user to authorize extension reload and reopening the OneTalk tab. After authorization, use Chrome read-only probes to confirm the exact three classifiers/template predicates and the normalized bridge/ledger count. Do not click, navigate, synchronize, or read sensitive card values.
- Step 5: Record validation and commit one cohesive change.
Use the repository commit style after all checks pass. Include contract, reader, test, and task documentation changes in one commit; do not include generated extension artifacts.
Plan self-review
- Spec coverage: Tasks 1-2 implement contract URL safety and field removal; Task 3 implements all three strict readers; Task 4 protects downstream semantics and runtime validation.
- Placeholder scan: no TODO/TBD steps remain.
- Type consistency: Task 1 defines
OneTalkProductReference; Task 2 consumes it in the rendered product contract; Task 3 consumes the public normalizer and returns one paired observation/evidence value; Task 4 exercises existing bridge/coordinator interfaces without changing them.