Author: Adham · Date: 2026-06-26 · Status: Draft for approval Companion: ENGINE_ARCHITECTURE.md (what-does-what) Tracking issues: #396 (freeze fixtures), #401 (consolidate runtime), #615 (cross-browser selectors), #616 (observer-only detection)
The extension ships two full detection stacks running at once (shadow mode
default): legacy V1 (detect + render) and V2 (detect + compute, render off). They
never communicate — V2's decisions drive nothing. Cost:
- Performance: 8+ MutationObservers + 4 setInterval heartbeats per Classroom page; every post card scanned by 3 independent scanners on every mutation.
- Detection: comment/edited/anchor logic duplicated 2–3×, can disagree.
- Activity: the smarter engine (V2) is inert — it computes and throws the result away.
- Comms: no contract between layers; coordination via scattered
data-cqd-*DOM attributes. - Availability:
chrome.*used without abrowserpolyfill; no cross-browser tests; V1 has zero dedicated tests. - Scalability: adding a capability means touching tangled monoliths.
One role-based, fault-tolerant, event-driven architecture: four modules (Detect / Compute / Render / Harden) connected by a typed EventBus, with versions (V1/V2/V3) demoted to swappable strategies. Same behavior on Chrome / Firefox / Edge across Windows / macOS / Linux. Instant rollback via a settings toggle.
Targets the six named outcomes: detection, performance, activity, comms, availability, scalability.
| # | Decision | Choice | Rationale |
|---|---|---|---|
| 1 | Structure | Role modules: Detect / Compute / Render / Harden | versions become strategies; clean long-term shape |
| 2 | Seam | Typed pub/sub EventBus, synchronous | zero module↔module coupling, testable, swap-friendly |
| 3 | V1 detector | Dual-run → strip after parity proven | safe; instant rollback during transition |
| 4 | V3 / API | Interface now, OAuth later; DOM stays live | ships architecture without blocking on Google review |
| 5 | Harden | Peer module on the bus | matches the 4-role model; centralizes repair+budget |
| 6 | Renderer keeper | LegacyRenderStrategy (wrap V1 render fns) | proven output; V2 render code kept as alt strategy |
- DetectEngine — owns the single MutationObserver + RouteWatcher. Strategies:
DomDetectStrategy(fromv2/model/dom-scanner+selectors/),ApiDetectStrategy(fromengines/v3/api/, OAuth-gated). Publishesroute:changed,post:scanned,file:discovered. - ComputeEngine — pure computation from
v2/decision/*. Subscribespost:scanned; publishesdecision:flags,decision:placement. No DOM writes. - RenderEngine — the only DOM writer. Strategies:
LegacyRenderStrategy(wraps V1 fns),V2RenderStrategy(fromv2/render/*). Subscribesdecision:*+correction:needed; publishesrender:applied. - HardenEngine — from
v2/repair/*+v2/telemetry/*. Subscribes to all events; idle-validates DOM vs decisions; publishescorrection:neededandbudget:throttle.
type TopicMap = {
'route:changed': { view: ViewKind; url: string };
'post:scanned': { posts: PostNode[] };
'file:discovered': { postId: string; files: FileNode[] };
'decision:flags': { decisions: FlagDecision[] };
'decision:placement': { decisions: PlacementDecision[] };
'render:applied': { postId: string; kind: 'button'|'flag'|'all' };
'correction:needed': { item: CorrectionItem };
'budget:throttle': { level: ThrottleLevel };
};
interface EventBus {
publish<T extends keyof TopicMap>(topic: T, payload: TopicMap[T]): void;
subscribe<T extends keyof TopicMap>(topic: T, fn: (p: TopicMap[T]) => void): () => void;
}Synchronous dispatch (no serialization, no latency). Subscriptions return an unsubscribe fn; Orchestrator clears all on navigation via AbortController.
Keeps its job (v2/orchestrator/orchestrator.ts): route watch → abort previous
page → init modules for the ViewKind → wire bus → teardown on nav. One
AbortController per page. The single MutationObserver moves into DetectEngine.
Popup Engine Mode → chrome.storage.local.cqdV2Mode: Legacy / New /
API (API hidden until OAuth lands). Live switch already supported by
mode-controller + engine-registry mode-change callback.
| Risk | Today | Requirement |
|---|---|---|
| API namespace | chrome.* inconsistent, no polyfill |
adopt one browserApi shim (or webextension-polyfill); all modules import it |
requestIdleCallback |
has setTimeout fallback in correction-queue |
extract one scheduleIdle() util; use everywhere |
WeakRef (ES2021) |
used in model/entities |
OK (FF 109+ already required); document min versions |
IntersectionObserver rootMargin:'100%' |
viewport-observer |
verify on Edge; pixel fallback if needed |
history monkey-patch |
route-classifier |
keep; add Navigation API path where available |
| Download divergence | Chrome onDeterminingFilename vs FF onCreated |
keep both; add tests for both paths |
| Icon API | action vs browserAction |
already handled in icon-manager; keep |
| Filenames / OS | conflictAction:'uniquify' |
OS-agnostic; no change |
| No cross-browser tests | jsdom only | add browser-matrix smoke plan (Phase 5) |
- Freeze golden fixtures + regression matrix (#396).
- Add V1 characterization tests (V1 has zero today) — lock current render output.
- Reconcile stale
TEST.md(claims 134 vs 3233 tests). - Gate: golden baseline green and frozen.
- Create
src/contracts/(Detector/Computer/Renderer/Hardener +TopicMap). - Create
src/bus/event-bus.ts+ unit tests. - Add
browserApishim +scheduleIdle()util. - Gate: bus + shim unit-tested, build green on all 3 targets.
- DetectEngine wraps
dom-scanner+ RouteWatcher + the one observer. - ComputeEngine wraps
decision/*. - RenderEngine =
LegacyRenderStrategywrapping V1 render fns. - HardenEngine wraps
repair/*+telemetry/*. - Wire through bus in
shadow; V1 detector still dual-runs. - Gate: ShadowComparator match ≥ 99.5% coverage / 98% precision
(existing
readiness-gatethresholds) on golden fixtures.
- In
v2mode, ComputeEngine decisions → bus → RenderEngine(Legacy) actually render. This is the connection that does not exist today. - Ship the settings toggle (Legacy / New).
- Gate: New mode passes full golden + manual smoke; rollback verified.
- Delete
smart-detector*duplication; legacy scripts keep only render fns. - Remove redundant V1 observers/heartbeats (target: 1 observer, 0 heartbeats).
- Gate: golden green; observer/heartbeat count verified down.
- Browser-matrix smoke tests (Playwright per target or scripted manual matrix).
- Verify download both-paths, idle fallback, IntersectionObserver on Edge.
- Gate: green Chrome/Firefox/Edge builds + smoke.
- Google Cloud OAuth client + consent screen +
classroom.courses.readonly. - Add
identitypermission +oauth2block towxt.config.ts. ApiDetectStrategygoes live behind theAPItoggle.- Gate: Google verification passed; DOM fallback still default.
| Metric | Baseline | Target |
|---|---|---|
| Concurrent observers (Classroom page) | 8+ | 1 |
| setInterval heartbeats | 4 | 0 |
handleMutations p95 |
unmeasured cross-stack | < 6 ms |
| Detection match vs golden | n/a (no frozen baseline) | ≥ 99.5% coverage / 98% precision |
| Duplicate detection passes per card | 3 | 1 |
| Module↔module imports | many | 0 (bus only) |
| Browsers with passing smoke | 0 automated | Chrome + Firefox + Edge |
| V1 dedicated tests | 0 | characterization suite |
| Risk | Mitigation |
|---|---|
| ComputeEngine misses a V1-handled case | dual-run + shadow gate before strip (Phase 2–3) |
| Bus dispatch becomes a perf bottleneck | synchronous + topic-scoped; budget throttle via Harden |
| Stripping V1 breaks a niche view | golden fixtures cover 8 ViewKinds; toggle = instant rollback |
| Google OAuth review delay | V3 is isolated Phase 6; DOM path unaffected |
| Refactor churn vs 36 weekly Jules PRs | land phases as small PRs; freeze fixtures first |
- Background download engine internals (stable; only add cross-browser tests).
- Analytics pipeline, Oracle backend, Svelte site.
- New user-facing features (this is structural).
Companion to PRD_ENGINE_REFACTOR.md and ENGINE_ARCHITECTURE.md. Defines how the existing 100-file suite is preserved and where new coverage lands, phase by phase.
TEST.md is stale and self-contradictory: its header claims "100 files /
3233 tests" while its overview table claims a "Total of 134" across only
core.test.ts (60), dom.test.ts (45), ui.test.ts (29). Neither reflects the
tree. Ground truth today: 100 *.test.ts files under extension/tests/
(plus fixtures.ts, setup.ts, fixtures/, cloudflare-workers-globals.d.ts).
Phase 0 reconciles TEST.md to point at the golden suite and the role-module
layout below; the per-file fate table in this PRD is the authoritative map.
Config facts the strategy relies on:
vitest.config.ts: jsdom env,include: tests/**/*.test.ts, alias@ -> entrypoints/content,setup.tspreloaded.- Three coverage profiles: default (content/**), critical and runtime (analytics + background, 100% lines/branches/funcs/statements). These gates are out-of-scope code and must keep passing untouched.
- Golden scripts already exist:
test:golden = regression + fuzz + stress + visual, plustest:fixtures:manifest. These become the Phase 0 freeze.
Every existing file is assigned one fate:
- keep — unaffected (analytics, background download engine, student-work subsystem, cross-cutting utils, security/fuzz suites). ~55 files. These stay byte-for-byte and remain green every phase.
- adapt — same target module, edits only when code moves behind the bus /
contracts / a strategy wrapper, or when a mode default/label changes (e.g.
core.test.ts,content-button-factory.test.ts,v2-mode-controller.test.ts,v2-engine-registry.test.ts,engine-combiner.test.ts, theclassroom-*-regressiongolden gates). - migrate — move to the new role-module test location with the code:
v2/decision/*->tests/compute/*,v2/model|selectors|context/*->tests/detect/*,v2/render/*->tests/render/v2-render-strategy,v2/repair|telemetry/*->tests/harden/*,engines/v3/api/*->tests/detect/api-detect-strategy. - split — one file spans multiple roles; carve detection vs render:
content-flags,content-both-badge,ui.test.ts,content-observers,content-post-card-utils,content-url-utils,scan_optimization,flag-detection-comprehensive,v2-engines,v3-engine-student-work-scope. - delete — tests V1 detection logic removed at strip:
dom.test.ts(after its language cases are migrated into ComputeEngine flag-scoring). The V1 render fns are NOT deleted — they survive asLegacyRenderStrategy.
Migration tooling tests (v2-launch-controller, v2-readiness-gate,
v2-shadow-diff-report) are keep through the transition, then retire only
when V1 dual-run ends (Phase 4) and shadow comparison is no longer wired.
| Feature | New test file(s) | Phase |
|---|---|---|
| Golden-fixture freeze (#396) | tests/golden/classroom-golden-freeze.test.ts (+ existing classroom-fixture-manifest.test.ts as the hash lock) |
0 |
| V1 characterization (zero today) | tests/v1-characterization/legacy-render-characterization.test.ts, legacy-detect-characterization.test.ts |
0 |
| EventBus | tests/bus/event-bus.test.ts |
1 |
| Contracts (Detector/Computer/Renderer/Hardener + TopicMap) | tests/contracts/contracts.test.ts |
1 |
| browserApi shim + scheduleIdle | tests/platform/browser-api-shim.test.ts, tests/platform/schedule-idle.test.ts |
1 |
| DetectEngine + Dom/Api strategies | tests/detect/detect-engine.test.ts, dom-detect-strategy.test.ts, api-detect-strategy.test.ts |
2 |
| ComputeEngine | tests/compute/compute-engine.test.ts |
2 |
| RenderEngine + Legacy/V2 strategies | tests/render/render-engine.test.ts, legacy-render-strategy.test.ts, v2-render-strategy.test.ts |
2 |
| HardenEngine | tests/harden/harden-engine.test.ts |
2 |
| Bus activation bridge | tests/orchestrator/bus-wiring.test.ts |
3 |
| Settings toggle (Engine Mode) | tests/settings/engine-mode-toggle.test.ts |
3 |
| Parity gate | tests/parity/engine-parity-gate.test.ts |
3 |
| Observer/heartbeat budget | tests/perf/observer-heartbeat-budget.test.ts |
4 |
| Cross-browser matrix | tests/cross-browser/browser-matrix.smoke.test.ts, download-paths.test.ts |
5 |
| V3 OAuth live | tests/detect/api-detect-strategy.live.test.ts |
6 |
| Phase | Gate (must be green to ship) |
|---|---|
| 0 | Golden manifest hashes match + golden-freeze snapshots immutable; V1 render+detect characterization captured; test:golden green; existing critical/runtime 100% profiles untouched. |
| 1 | EventBus + contracts + browserApi shim + scheduleIdle unit-tested; tsc --noEmit clean; build:all (chrome/firefox/edge) green. No behavior change — full existing suite still passes unmodified. |
| 2 | Role modules wrapped, wired in shadow, V1 still primary. Parity gate >= 99.5% coverage / 98% precision vs golden via readiness-gate + shadow-diff-report. Migrated tests pass in their new locations; characterization suites still green. |
| 3 | Bus-wiring renders in New mode; engine-parity-gate green for BOTH Legacy and New over full golden; settings toggle persists + live-switches + rollback verified. |
| 4 | V1 detector deleted; dom.test.ts removed (cases migrated); observer-heartbeat-budget proves observers->1, heartbeats->0, 1 pass/card, p95 < 6ms; golden still green via Legacy render path. |
| 5 | browser-matrix.smoke + download-paths green on Chrome/Firefox/Edge; idle fallback, IntersectionObserver-on-Edge, both download paths verified. |
| 6 | API toggle live behind OAuth; api-detect-strategy.live green; DOM path remains default and still passes all detection gates. |
Each phase keeps the full prior suite green (no red between phases); migrations move tests with their code in the same PR so coverage never dips.
Today: jsdom only, zero automated cross-browser coverage (PRD risk row). Plan addresses each PRD section-5 risk with an explicit check:
| Risk | Test |
|---|---|
chrome.* vs browser.*, action vs browserAction |
browser-api-shim.test.ts (Phase 1) resolves the right namespace; smoke matrix confirms on each target. |
| Download filename divergence | download-paths.test.ts — Chrome/Edge onDeterminingFilename + authuser rotation vs Firefox onCreated (no rename, always bypass tab, no 403 retry). Extends background-download-handler/background-index. |
requestIdleCallback absence |
schedule-idle.test.ts fallback path; matrix confirms on each engine. |
WeakRef (ES2021) |
documented min versions (FF 109+); covered by migrated v2-element-lifecycle/v2-entities running on each target. |
IntersectionObserver rootMargin:'100%' on Edge |
matrix case + pixel-fallback assertion (from migrated v2-viewport-observer). |
history monkey-patch / Navigation API |
migrated route-classifier cases run per target. |
Mechanism (open question 2): default to a scripted manual checklist
driven by wxt build -b {chrome,firefox,edge} + the existing
utils-firefox-debug helper for the first matrix; upgrade to Playwright
per-target (--project=extension-chromium already used for student-work e2e)
if the manual matrix proves flaky. The smoke suite loads the built extension on
each target, opens a golden Classroom fixture page, and asserts buttons/badges
render and a download both-path completes.
This section defines what happens to every CQD doc during the engine refactor: what is the source of truth, what is stale, what new docs the seam needs, and when each lands. It is reconciled against the actual repo state on 2026-06-26 (src/bus and src/contracts do not exist yet — Phase 0/1 are genuinely pending; the extension has ~100 test files / ~1316 it() cases, not the 134 or 3233 figures floating in the docs).
- One source of truth per topic.
docs/TESTING.mdowns testing;extension/TEST.mdshrinks to extension-specific specifics + a pointer.docs/ARCHITECTURE.mdowns the analytics backend;extension/docs/ENGINE_ARCHITECTURE.mdowns the detection engine. No doc should restate another's authority. - Never hardcode a test count. Every stale number ("134", "3233", "43", "47", "101") came from copy-pasting a point-in-time scan. Counts must be derived by command at write time and stated once.
- The seam gets a contract doc. The EventBus TopicMap is currently duplicated inline in two docs. The moment
src/bus/event-bus.tslands it gets one canonicalEVENTBUS_CONTRACT.mdthat both other docs link to. - Dated snapshots are history, not truth.
AUDIT_REPORT,MAJOR_SCAN_*,CODE_QUALITY_*,TODO_LIST_2026-03-13,POST_MERGE_FOLLOWUP_BOARD,extension-core-strategyare point-in-time. They get a "superseded by" header or move todocs/archive/; they are never cited as the live state. - Phase numbers are namespaced. "Phase 12/13" belongs to the old analytics
refactor-plan.md; "Phase 0–6" belongs to this engine PRD. Docs that use either must say which.
| Doc | Problem | Fix |
|---|---|---|
extension/TEST.md |
Self-contradictory (134 vs 3233 tests); frames the suite as a 3-file "Universal V4" system; omits the golden suites. | Delete the 134/3-file table and "Universal V4" framing; reduce to extension-specific content (Student Work verification block, golden/test:golden scripts) + a pointer to canonical docs/TESTING.md. Derive any count by command. |
docs/TESTING.md |
Inconsistent counts (43 / 47 / 101); still labels core/dom/ui.test.ts as the detection system. |
Make it the single testing source of truth; state the count once (script-derived); reframe the 3 legacy files as a slice; add the golden-suite scripts. |
docs/ARCHITECTURE.md |
Name implies engine docs; ~95% is the analytics backend. | Add a top banner pointing engine readers to ENGINE_ARCHITECTURE.md + PRD_ENGINE_REFACTOR.md. Rename to ANALYTICS_ARCHITECTURE.md is a stretch goal behind the banner. |
extension/README.md, docs/DEVELOPMENT.md |
No mention of the engines; Node version differs from .nvmrc. |
Add a "Detection Engine" subsection linking the two new engine docs; reconcile Node version to .nvmrc. |
Stale Update (2026-02-28) banners across README/TEST/ARCHITECTURE/BOTS/DEVELOPMENT/SECURITY_DEV |
Point at an archived MAJOR_SCAN_2026-02-28.md. |
Drop or update those banner lines while editing each file. |
docs/README.md |
Does not exist — 40+ docs with no index; the new engine docs are undiscoverable. | Create a domain-grouped docs index (Engine / Backend / Testing / Distribution / SEO-Website / Planning / Archive). |
docs/SPRINT_PLAN.md |
Lists #396/#401/#615/#616 without tying them to the PRD. | Annotate each with the PRD phase it maps to so the board and PRD stay reconciled. |
docs/PHASE12_TEST_MATRIX.md |
Phase-number collision with the engine PRD. | Add a header: "Phase 12 = analytics refactor-plan.md, not engine PRD phases 0–6." |
ENGINE_ARCHITECTURE.md and PRD_ENGINE_REFACTOR.md (both new this session, accurate) are correct as written — they only need to be linked from README, docs/ARCHITECTURE.md, docs/DEVELOPMENT.md, and the new docs index, and to reference EVENTBUS_CONTRACT.md once it exists. EXTENSION_GOLDEN_BEHAVIOR_MATRIX.md is the parity oracle the ShadowComparator gate must satisfy — link it from the PRD. CLASSROOM_FIXTURE_CAPTURE_GUIDE.md is the #396 freeze procedure — link it from PRD Phase 0. EXTENSION_TESTING_RUNBOOK.md is the manual companion for Phase 0 (V1 characterization) and Phase 5 (cross-browser). student-work-* docs are accurate and gain only a forward-link to PRD Phase 6.
| New doc | When | Why |
|---|---|---|
extension/docs/EVENTBUS_CONTRACT.md |
Phase 1 (with src/bus/) |
Canonical TopicMap + publisher→subscriber matrix + dispatch/teardown semantics + the "bus-only imports" rule. Replaces the inline duplication. |
extension/docs/CROSS_BROWSER_MATRIX.md |
Phase 1 stub → Phase 5 complete | Living version of PRD §5: shim/idle/observer/download-path coverage per Chrome/Firefox/Edge × OS. Gates Phase 5 smoke. |
extension/docs/MIGRATION_RUNBOOK.md |
Phase 2 draft → Phase 3–4 use | The "is it safe to strip V1?" operator doc: reading ShadowComparator, readiness-gate thresholds, the Engine Mode toggle, instant rollback, per-phase gate checklist. |
extension/docs/ENGINE_DEBUG_GUIDE.md |
Phase 3 (only if the in-line quick-ref proves too thin) | Deep troubleshooting: decision traces, shadow diffing, budget:throttle, tracing a missing button through Detect→Compute→Render. |
Move to docs/archive/: TODO_LIST_2026-03-13.md (superseded by SPRINT_PLAN), extension-core-strategy-2026-03-08.md (folded into PRD). Reduce RUNBOOK_DEPLOYMENT.md to a redirect to the canonical DEPLOYMENT_RUNBOOK.md (the SPRINT-flagged duplicate). Add "superseded by" headers to dated snapshots (AUDIT_REPORT, MAJOR_SCAN_2026-03-17, CODE_QUALITY_2026-06-24). plan.md (Website Reviews) stays live until Reviews ships, then archives. Disambiguate ARCHITECTURE_RUNTIME_CONTRACT.md (website telemetry contract) from the engine contracts/ seam with a one-line scope note.
- Phase 0 gate also requires:
TEST.md+TESTING.mdreconciled,ARCHITECTURE.mdcross-link banner added,docs/README.mdindex created, engine docs linked from README. Documentation is part of the safety net, not an afterthought. - Phase 1 gate also requires:
EVENTBUS_CONTRACT.mdpublished,CROSS_BROWSER_MATRIX.mdstubbed. - Phase 3 gate also requires:
MIGRATION_RUNBOOK.mdusable (toggle + rollback documented and verified). - Phase 5 gate also requires:
CROSS_BROWSER_MATRIX.mdfully filled per target.
browserApishim hand-rolled vswebextension-polyfilldependency?- Browser-matrix tests: Playwright (heavier, real) vs scripted manual checklist?
- Settings UI: separate Engine Mode control, or fold into existing flag toggles?
- Keep
V2RenderStrategylong-term, or delete once Legacy is confirmed keeper?