9.1 KiB
OneTalk DOM Secondary Card Collection 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: Replace the Fiber-backed secondary card producer with strict DOM readers for inquiry, product, and order cards, while removing four obsolete server audit columns.
Architecture: A single DOM identity resolver validates card and wrapper metadata before three typed readers produce the existing rendered-card contract. The bridge and local ledger cease carrying unused Fiber-only base evidence; the server wire and JSON content shapes remain unchanged. The migration drops only approved columns and preserves first-content-wins with conflict_count.
Tech Stack: TypeScript, MV3 MAIN/ISOLATED page bridge, IndexedDB, shared OneTalk contract, Fastify/Drizzle/PostgreSQL, Node test runner.
Spec: .trellis/tasks/09-16-onetalk-dom-secondary-card-collection/{prd,design,implement}.md
Global Constraints
- DOM is a structural data source; no React/Fiber inspection, root traversal, timer rescan, raw metadata forwarding, or fallback path.
messageIdmust be present and match in card metadata and closest wrapper metadata whenever both exist; metadata conversation IDs must also agree when both exist.- Quote (
cardType=8) and unknown cards fail closed. - Keep existing three rendered content JSON shapes, server frame, compound message identity, fingerprint, observedAtMs and
conflict_count. - Migration never issues DELETE, TRUNCATE, table rebuild or historical JSON rewrite.
Task 1: DOM reader and legacy reader removal
Files:
- Create:
apps/chrome-extension/src/onetalk/main-page/card-observer/dom-card-reader.ts - Delete:
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 - Test:
apps/chrome-extension/test/onetalk-rendered-card-reader.test.js
Interfaces:
-
Produces:
readOneTalkRenderedCardFromDom(pageWindow, card): OneTalkRenderedCardObservation | null. -
Consumes:
readConversationSelection,normalizeOneTalkProductUrl, rendered-card validators and fingerprint creator. -
Step 1: Replace Fiber fixtures with DOM fixtures and red tests.
assert.equal(readOneTalkRenderedCardFromDom(page, cardWith({ cardMessageId: "m-1", wrapperMessageId: "m-2" })), null);
assert.equal(readOneTalkRenderedCardFromDom(page, quoteCard({ cardType: 8 })), null);
assert.equal(readOneTalkRenderedCardFromDom(page, productCard({ rawUrl: "https://bad.example" })), null);
- Step 2: Run the focused reader test and confirm the missing DOM reader fails.
node --experimental-strip-types --test apps/chrome-extension/test/onetalk-rendered-card-reader.test.js
- Step 3: Implement the shared resolver and three projections.
const messageId = cardMessageId ?? wrapperMessageId;
if (!messageId || (cardMessageId && wrapperMessageId && cardMessageId !== wrapperMessageId)) return null;
if (!sameOptionalConversation(cardInfo, wrapperInfo)) return null;
Construct only exact rendered_inquiry, rendered_product, or rendered_order shapes, validate with isOneTalkRenderedCardContent, and return an observation whose fingerprint is recomputed from that content.
- Step 4: Rewrite entry to scan cards and react only to DOM mutations.
for (const card of cardsForMutation(record)) publish(readOneTalkRenderedCardFromDom(pageWindow, card));
observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ["data-expinfo", "src", "style"] });
Remove setInterval, wrapper rescans, React imports, and the deleted reader file.
- Step 5: Run focused reader/observer tests.
node --experimental-strip-types --test apps/chrome-extension/test/onetalk-rendered-card-reader.test.js apps/chrome-extension/test/onetalk-rendered-card-observer.test.js
Task 2: Remove Fiber-only base evidence from local boundaries
Files:
- Modify:
apps/chrome-extension/src/onetalk/page-bridge/{main.ts,model.ts} - Modify:
apps/chrome-extension/src/onetalk/service-worker/{rendered-card-coordinator.ts,storage.ts} - Test:
apps/chrome-extension/test/{onetalk-page-bridge,onetalk-rendered-card-coordinator,onetalk-sync-storage}.test.js
Interfaces:
-
onetalk.page.rendered-card-observedcontains{ channelAccountId, observations }only. -
OneTalkRenderedCardCoordinator.observe(observations)persists and flushes observations without base evidence. -
Step 1: Add bridge and ledger tests that accept an observation-only message and reject legacy base-evidence fields.
assert.deepEqual(decodeOneTalkPageMessage({ source, version, type, channelAccountId, observations }), expected);
assert.equal("baseDirection" in pendingRecord, false);
- Step 2: Run focused tests and confirm they fail against the old exact shape.
node --experimental-strip-types --test apps/chrome-extension/test/onetalk-page-bridge.test.js apps/chrome-extension/test/onetalk-rendered-card-coordinator.test.js apps/chrome-extension/test/onetalk-sync-storage.test.js
- Step 3: Remove baseEvidence from the page message decoder/sink and from coordinator/ledger APIs.
return coordinator.observe(message.observations);
await ledger.observe({ channelAccountId: scope.channelAccountId, observation });
Keep markSent, listPending, markAcknowledged, fingerprints and exact ACK matching unchanged.
- Step 4: Re-run focused tests.
node --experimental-strip-types --test apps/chrome-extension/test/onetalk-page-bridge.test.js apps/chrome-extension/test/onetalk-rendered-card-coordinator.test.js apps/chrome-extension/test/onetalk-sync-storage.test.js
Task 3: Remove approved server audit columns without data cleanup
Files:
- Modify:
apps/server/src/database/schema/onetalk.ts - Modify:
apps/server/src/onetalk/rendered-card-{model,repository,service}.ts - Create: generated
apps/server/drizzle/0015_*.sqland matchingdrizzle/metafiles - Test:
apps/server/test/onetalk-rendered-card-{migration,repository,flow}.test.ts
Interfaces:
-
Stored row retains key,
renderedCardContent,renderedCardContentFingerprint,renderedCardObservedAtMs, andconflictCount. -
Same content stays duplicate; different content increments
conflictCountand returns conflict without overwriting JSON. -
Step 1: Change migration/repository test expectations to the retained row shape and conflict-only audit.
assert.equal(saved.conflictCount, 1);
assert.equal("lastObservedAt" in saved, false);
assert.doesNotMatch(migrationSql, /\b(?:DELETE|TRUNCATE)\b/i);
- Step 2: Run the server rendered-card tests and confirm failure.
node --experimental-strip-types --test apps/server/test/onetalk-rendered-card-migration.test.ts apps/server/test/onetalk-rendered-card-repository.test.ts apps/server/test/onetalk-rendered-card-flow.test.ts
- Step 3: Remove four Drizzle fields and generated SQL columns, then remove their repository/service writes.
ALTER TABLE "onetalk_rendered_card_content" DROP COLUMN "first_confirmed_at";
ALTER TABLE "onetalk_rendered_card_content" DROP COLUMN "last_observed_at";
ALTER TABLE "onetalk_rendered_card_content" DROP COLUMN "last_conflicting_fingerprint";
ALTER TABLE "onetalk_rendered_card_content" DROP COLUMN "last_conflict_observed_at_ms";
Retain conflict_count = conflict_count + 1 in the conflict branch and leave all supplement JSON untouched.
- Step 4: Run migration/repository/flow tests and Drizzle validation.
node --experimental-strip-types --test apps/server/test/onetalk-rendered-card-migration.test.ts apps/server/test/onetalk-rendered-card-repository.test.ts apps/server/test/onetalk-rendered-card-flow.test.ts
pnpm --filter @trade-message-center/server db:check
Task 4: Contract-preserving integration verification
Files:
-
Test: existing contract, read-projection, extension and server suites
-
Step 1: Add read projection regressions for all three base kinds and reject mismatched supplement kinds.
assert.equal(projected.content.kind, "inquiry");
assert.throws(() => toOneTalkCenterMessage(product, renderedInquiry));
- Step 2: Run all scoped checks.
pnpm --filter @trade-message-center/onetalk-contract test
pnpm --filter @trade-message-center/chrome-extension test
pnpm --filter @trade-message-center/chrome-extension typecheck
pnpm --filter @trade-message-center/chrome-extension build
pnpm --filter @trade-message-center/server typecheck
git diff --check
- Step 3: Run final source-boundary searches.
rg -n 'React|Fiber|baseEvidence|baseDirection|baseSentAtMs|setInterval' apps/chrome-extension/src/onetalk/main-page/card-observer apps/chrome-extension/src/onetalk/page-bridge apps/chrome-extension/src/onetalk/service-worker
Expected: no legacy secondary-card collector references; unrelated modules may remain outside the reviewed paths.