9ac580869d3e555ac6f4bbdd028151cdfe13461c
285 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 6a77967b6c |
feat(01-13): wave-3B — A5+A6+A7 GREEN + Bug B canonical regression rewind
Wave 3B lands the A5 (SAVE_ARCHIVE → zip on disk) and A7 (genuine
RECORDING_ERROR → ERR + recovery notification) assertions, completing
8/14 of the orchestrator's GREEN floor (A0+A1+A2+A3+A4+A5+A6+A7).
Bails at A8 (Wave 3C scope).
Changes per file:
tests/uat/extension-page-harness.ts
- assertA5: 11s settle (>= SEGMENT_DURATION_MS so first rotation
lands a segment) + send SAVE_ARCHIVE + assert resp.success=true.
Page-side only checks SW handler ack; host-side driver verifies
disk-side outcome (zip presence + size floor).
- assertA7: setupFreshRecording helper (A6 tears down; A7 needs
REC state) → snapshot notif count → send RECORDING_ERROR with
a non-Bug-B error code ('codec-unsupported') → 200ms settle →
assert badge='ERR' + popup endsWith popup.html + notif delta=1
+ set-membership for 'mokosh-recovery-*' prefix.
- setupFreshRecording: shared helper for A7 + future assertions
that need a fresh REC state after a teardown.
tests/uat/lib/harness-page-driver.ts
- driveA5: page.evaluate(assertA5) THEN host-side fs polling for
*.zip in handles.downloadsDir. The CDP Browser.setDownloadBehavior
override renames the file to download.zip (data: URL filename
gap), so we accept any *.zip suffix. Merges page-side check +
host-side checks into a single AssertionRecord. Signature now
takes downloadsDir as a second arg.
- driveA7: standard page.evaluate wrapper (no host-side work).
tests/uat/harness.test.ts
- Wraps driveA5 in a closure that captures handles.downloadsDir.
- Reordered: launchHarnessBrowser MUST run before driver list so
the closure can read handles without a TDZ trap.
tests/uat/lib/launch.ts
- Victim page switched from about:blank to a file:// URL backed by
a tmp HTML file in downloadsDir. About:blank breaks A5 because
chrome.tabs.captureVisibleTab needs <all_urls> permission which
matches http/https/file/ftp but NOT about: or data: URLs. The
stub HTML satisfies <all_urls> + provides a real .url for the
production saveArchive's chrome.tabs.query.
src/test-hooks/offscreen-hooks.ts (test-only — tree-shaken from prod)
- installFakeDisplayMedia: mintStream() helper called per
fakeGetDisplayMedia invocation; each call mints a FRESH
MediaStream from the persistent canvas. Real getDisplayMedia
returns a new stream per call — fake now matches. Required for
A7's setupFreshRecording where the previous recording's stream
tracks were stopped by A6's onUserStoppedSharing teardown.
- Added 33ms setInterval-driven drawFrame() alongside the
existing requestAnimationFrame loop. RAF can throttle in
headless Chrome on offscreen documents (page-visibility
heuristics produce 0 fps), which yields zero-byte
MediaRecorder segments that crash ts-ebml's VINT decode in
webm-remux.extractFramesFromSegment with "Unrepresentable
length: Infinity". The setInterval is redundant when RAF fires
at full rate; it's a safety net for the headless-MV3 corner.
Bug B regression-catch demo (success_criteria #3 — MANDATORY per plan):
Step 1 — apply local regression patch (NOT committed):
src/background/index.ts:792 setIdleMode() → setErrorMode()
Step 2 — npm run build:test && npm run test:uat RED snippet:
A6 — BUG B canonical: user-stopped-sharing routes via setIdleMode: FAIL
[PASS] SETUP: badge becomes REC after start
[FAIL] A6.1: badge text is '' (NOT 'ERR') after user-stop
expected: ""
actual: "ERR"
[FAIL] A6.2: popup is '' (NOT manifest default) after user-stop
expected: ""
actual: "chrome-extension://<id>/src/popup/index.html"
[PASS] A6.3: NO recovery notification fired (count delta === 0)
[PASS] A6.4: isRecording=false (via badge proxy)
UAT harness: 6/14 assertions passed (bailed: A6 failed; see above)
Step 3 — revert local patch (git checkout -- src/background/index.ts).
Step 4 — npm run build:test && npm run test:uat GREEN snippet:
A6 — BUG B canonical: user-stopped-sharing routes via setIdleMode: PASS
[PASS] SETUP: badge becomes REC after start
[PASS] A6.1: badge text is '' (NOT 'ERR') after user-stop
[PASS] A6.2: popup is '' (NOT manifest default) after user-stop
[PASS] A6.3: NO recovery notification fired (count delta === 0)
[PASS] A6.4: isRecording=false (via badge proxy)
UAT harness: 8/14 assertions passed (bailed: A8 failed — NOT YET
IMPLEMENTED — Wave 3C wires driveA8)
The harness CORRECTLY catches the Bug B regression — the canonical
debug 01-09-recovery-flow scenario (operator-initiated stop routed
through setErrorMode locks the operator out of restart because popup
stays pinned to SAVE-only mode). Bug B is now CI-callable end-to-end.
vitest 93/93 GREEN throughout (unit-test layer unaffected). Tier-1
grep gate GREEN (9 forbidden hook strings: 0 occurrences in dist/).
npm run build exit 0; npx tsc --noEmit exit 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 1b67b1c1d3 |
feat(01-13): wave-3A — A1+A2+A3+A4 GREEN + harness.test.ts orchestrator (5/14 assertions GREEN)
Wave 3A landed. `npm run test:uat` now exercises 5/14 assertions
end-to-end (A0 + A1 + A2 + A3 + A4); bails at A5 NOT YET IMPLEMENTED
(Wave 3B scope). A6 still PASSES 5/5 through the standalone
`npx tsx tests/uat/a6.test.ts` entry — the orchestrator-level A6 won't
reach in Wave 3A because the sequential loop bails at A5; once Wave 3B
wires driveA5 the loop will fall through to A6 (which uses the proven
Wave-2 driveA6 driver — no rework needed there).
Files changed:
- `tests/uat/extension-page-harness.ts` — extends `window.__mokoshHarness`
from `{ assertA6 }` to `{ assertA1, assertA2, assertA3, assertA4,
assertA6 }`. Per-assertion contracts:
• A1 — chrome.action.getBadgeText({}) === '' + getPopup({}) === ''
+ isRecording=false (badge !== 'REC' proxy per state-machine atomic
pairing). 3 CheckRecords.
• A2 — ensureOffscreen + START_RECORDING direct-to-offscreen
(workaround for the `tabs` manifest permission gap per
01-11-SUMMARY + plan resolved-questions row 2) + manual
setBadgeText('REC') + setPopup(POPUP_HTML_PATH) + waitFor
badge==='REC'. The bypassed chrome.action.onClicked →
startVideoCapture path is unit-tested in
tests/background/badge-state-machine.test.ts; A2 verifies the
contract that matters (recording reaches the REC state-machine
row). 2 CheckRecords.
• A3 — offscreen bridge query 'get-display-surface' (new in this
plan via the prior commit's offscreen-hooks extension) → asserts
=== 'monitor'. 1 CheckRecord.
• A4 — getPopup remains 'src/popup/index.html' + hasDocument()===true
(no duplicate offscreen). Essentially a no-op verification —
regression protection against future refactors that might unpin
the popup during recording or spawn extra offscreens on stray
events. 2 CheckRecords.
• IMPORTANT: chrome.action.getPopup() returns the FULL absolute
chrome-extension://<id>/... URL (not the manifest-relative path).
A2.2 + A4.1 assert via .endsWith('src/popup/index.html') to stay
extension-id independent. Empirical finding from first orchestrator
run; documented inline.
- `tests/uat/lib/harness-page-driver.ts` — wires `driveA1/A2/A3/A4`
(replaces the 4 NOT YET IMPLEMENTED Wave-3A stubs from
|
|||
| 2f1b1f36a7 |
feat(01-13): wave-3A — add get-display-surface bridge op (A3 prereq) + extend Tier-1 grep gate
Scope: prerequisite step for Wave 3A's A3 assertion (displaySurface=monitor
verification). The page→offscreen bridge gains a new op so the harness can
query the active stream's `getSettings().displaySurface` without needing
direct offscreen.evaluate access (impossible by-construction; the only
cross-isolate path is chrome.runtime.sendMessage).
Bridge op contract (`src/test-hooks/offscreen-hooks.ts`):
- Protocol: { type: '__mokoshOffscreenQuery', op: 'get-display-surface' }
- Response: { displaySurface: string|null }
• null when no current stream (recording not active)
• 'monitor' when installFakeDisplayMedia's monkey-patched
getSettings() reports it (production code in
src/offscreen/recorder.ts enforces this same value — tears down
stream + throws 'wrong-display-surface' otherwise).
- Failure: { ok: false, error: <message> } only on getSettings throw.
Tier-1 grep gate extension (`tests/background/no-test-hooks-in-prod-bundle.test.ts`):
- FORBIDDEN_HOOK_STRINGS: 8 → 9 entries.
- Added: 'get-display-surface' (the literal bridge-op string;
matches the production-bundle absence invariant — the offscreen-hooks
module is tree-shaken in production builds by the Vite mode gate in
src/offscreen/recorder.ts top-of-module).
Verification:
- npx tsc: clean
- npm run build: clean (dist/ 4 chunks; no offscreen-hooks artifact)
- npm run build:test: clean (dist-test/ adds offscreen-hooks-DfWtG71P.js, 2.38kB)
- SKIP_BUILD=1 vitest run no-test-hooks-in-prod-bundle.test.ts → 10/10 GREEN
(1 build-sanity + 9 forbidden-string checks; production bundle hook-free)
- SKIP_BUILD=1 vitest run (full) → 93/93 GREEN
(Wave 0+1+2 baseline 92 + 1 from the 9th grep-gate string)
- npx tsx tests/uat/a6.test.ts → A6 5/5 GREEN
(lib-driven path preserved; bridge op addition does not interfere)
Wave 3A continuation: assertA1/A2/A3/A4 land in the next commit which
wires the harness-page surface + driver wrappers + harness.test.ts
orchestrator. This commit is the bridge prerequisite — keeping the
bridge-op extension atomic + the grep-gate extension atomic so the
'production bundle hook-free' invariant is provable BEFORE the page-side
surface lands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| eb64521321 |
feat(01-13): wave-2 — launchHarnessBrowser + assertions + harness-page-driver scaffolding
Build out the Approach-B harness driver utilities atop the Wave 1
production paths. Three new files form the shared scaffold that
Wave 3's 13 assertion drivers (A1-A5, A7-A13) and the eventual
orchestrator (`tests/uat/harness.test.ts`) will all consume. The
standalone A6 driver (`tests/uat/a6.test.ts`) is rewritten to use
the new lib — behavior-preserving: A6 still PASSES 5/5 in ~7s.
New files:
- tests/uat/lib/launch.ts (~320 LoC)
`launchHarnessBrowser({ headless?, downloadsDir? }) → HarnessHandles`
Extracts the Chrome-launch + victim-page + harness-page + console-
attach pattern from a6.test.ts into a single reusable helper.
NEW vs prototype: CDP `Browser.setDownloadBehavior` wires
Chrome's download path to a per-run `mkdtempSync` tmp dir so A5
(SAVE_ARCHIVE) can poll a known location without colliding with
the operator's real downloads. Architectural commitments
enforced (per 01-11-SUMMARY): no `--auto-select-desktop-capture-
source` flag; victim about:blank brought to front for the
production `chrome.tabs.query({active:true})` workaround; SW
console attach best-effort with bounded poll; offscreen console
attach opportunistic via `targetcreated` listener (offscreen
target appears later, when the harness page calls
chrome.offscreen.createDocument).
- tests/uat/lib/assertions.ts (~210 LoC)
Host-side assertion primitives:
* `AssertionRecord`, `CheckRecord`, `ConsoleBuffers` types —
mirror the page-side shape returned by `assertA*` methods.
* `runAssertion(name, fn, buffers)` — try/catch wrapper that
dumps the SW + offscreen console tails (last 100 lines each)
to stderr on failure, then returns `{passed: false, error}`
if `fn` throws.
* `printAssertionResult(result)` — single source of truth for
the formatted result print. Extracted from the inline
`printResult` previously in the prototype's a6.test.ts so
Wave 3's orchestrator can reuse it across all 14 assertions.
* `assertEqual / assertGte / assertMatch / assertTrue` —
structured failure messages atop node:assert/strict.
* `waitFor(probe, predicate, timeoutMs, description)` — host-
side polling primitive; mirrors the page-side waitFor
semantics verbatim (they can't share a module: page-side is
bundled into the harness HTML, host-side runs in Node).
NO chrome.* helpers here — all chrome.* work happens inside the
extension-internal harness page. This module is host-side ONLY
by construction (no chrome global in Node anyway).
- tests/uat/lib/harness-page-driver.ts (~170 LoC)
One driver wrapper per assertion (A1..A13). Each wraps a single
`page.evaluate(() => window.__mokoshHarness.assertXX())`.
Centralizing this means adding/renaming an assertion = two-file
edit (extension-page-harness.ts impl + this file) instead of
touching every test-file caller.
Wave 2 wires `driveA6` (proven from
|
|||
| eb2258a880 |
feat(01-13): wave-1 — promote c647f61 prototype to production paths; A6 GREEN
Move the three load-bearing prototype files from `tests/uat/prototype/`
to their production paths under `tests/uat/`, leaving the architectural
narrative (research findings, BLOCKER citations, falsification table
references) intact. No behavioral changes — A6 still PASSES 5/5 in ~7s
end-to-end from the new paths.
File moves (git mv preserves history):
- tests/uat/prototype/extension-page-harness.html
→ tests/uat/extension-page-harness.html
- tests/uat/prototype/extension-page-harness.ts
→ tests/uat/extension-page-harness.ts
- tests/uat/prototype/a6.test.ts
→ tests/uat/a6.test.ts
The `tests/uat/prototype/` directory is now empty (git does not track
empty directories; will not appear in subsequent `git status`).
Path-reference updates inside the moved files:
- tests/uat/extension-page-harness.html: `<p>` line referencing the
chrome-extension:// URL updated to drop `/prototype/`.
- tests/uat/extension-page-harness.ts: file-header docstring rewritten
to cite Plan 01-13 / Approach B / inheritance from
|
|||
| a63066a289 |
chore(01-13): wave-0 — clean broken Approach-A artifacts per 01-11-SUMMARY
Restore a clean baseline before promoting the |
|||
| 70f4f4136a |
docs(01-13): create UAT harness plan — Approach B (extension-internal page)
5 waves, 9 tasks. Inherits Plan 01-11 spike-pivot rationale per 01-11-SUMMARY (commit |
|||
| ba5474c54f |
docs(01-11): close as spike-pivot — SUMMARY landed, AMENDMENT-A deleted, pivots to 01-13
Closes Plan 01-11 honestly per GSD spike-pivot pattern. Original
Approach A (Puppeteer sw.evaluate per RESEARCH §1+6) empirically
falsified across Wave 3 execution + feasibility research. Approach B
(extension-internal-page harness + offscreen synthetic stream) proven
via
|
|||
| 565f8fa44c |
docs(01-11): amendment A — pivot to extension-internal harness page
Architectural pivot triggered by feasibility research prototype (commit |
|||
| c647f61553 |
wip(01-11): prototype — A6 via test-page+bridge+synthetic-stream PASSES
Plan 01-11 orchestrator commissioned a research+prototype investigation
into whether full MV3 UAT automation is feasible with the architecture:
extension-internal test page + chrome.runtime.sendMessage bridge +
synthetic MediaStream (canvas-captureStream + getSettings override).
EMPIRICAL VERDICT: feasible BUT plan 01-11 needs architectural revision.
Architectural findings (with proof):
1. DYNAMIC IMPORT BLOCKED IN MV3 SW. Top-of-module
`await import('../test-hooks/sw-hooks')` in src/background/index.ts
silently kills the SW (chunk loads, await never resolves, no
production listeners register, no console output). This is by design
per Chromium docs (es_modules.md) + w3c/webextensions#212. The Plan
01-11 RESEARCH §6 architecture was wrong for the SW side.
Workaround in this prototype: REMOVE the SW-side gated dynamic
import. SW-side test hooks need a different design (see verdict).
2. OFFSCREEN-SIDE DYNAMIC IMPORT WORKS. Offscreen is a DOM document,
not a SW, so top-level await + dynamic import behave normally. The
offscreen-hooks.ts gated import succeeds; installFakeDisplayMedia is
installed eagerly at module load.
3. EXTENSION-INTERNAL PAGE HAS FULL chrome.* SURFACE. Reachable via
chrome-extension://<id>/tests/uat/prototype/extension-page-harness.html
(added as rollup input in vite.test.config.ts). The page can call
chrome.action.getBadgeText, chrome.action.getPopup, chrome.offscreen
.createDocument, chrome.notifications.getAll, chrome.runtime
.sendMessage — everything needed for A6.
4. NO 'tabs' PERMISSION → tab.url IS UNDEFINED. Production
startVideoCapture's `chrome.tabs.query({active:true})` check
(`if (!tab.id || !tab.url) throw`) fails because the manifest lacks
the 'tabs' permission. Prototype workaround: bypass startVideoCapture
by sending START_RECORDING directly to offscreen. The Bug B
contract being tested is independent of how recording starts; it
only depends on the RECORDING_ERROR routing path.
5. SYNTHETIC MEDIASTREAM WORKS. installFakeDisplayMedia builds a
canvas-captureStream MediaStream + monkey-patches the video track's
getSettings() to report displaySurface: 'monitor'. Production code's
post-grant validation passes. getDisplayMedia returns the synthetic
stream immediately — no picker, no headless flakiness.
A6 prototype result (with Bug B fix in place — current HEAD state):
[PASS] SETUP: badge becomes REC after start
[PASS] A6.1: badge text is '' (NOT 'ERR') after user-stop
[PASS] A6.2: popup is '' (NOT manifest default) after user-stop
[PASS] A6.3: NO recovery notification fired (count delta === 0)
[PASS] A6.4: isRecording=false (via badge proxy)
A6 prototype result (with Bug B fix rewound to `if (false)`):
[PASS] SETUP: badge becomes REC after start
[FAIL] A6.1: badge text is '' (got "ERR")
[FAIL] A6.2: popup is '' (got chrome-extension://.../popup/index.html)
[FAIL] A6.3: notif delta = 0 (got 1)
[PASS] A6.4: isRecording=false ← false-positive (badge='ERR' not 'REC')
The Bug B regression rewind cycle proves the harness CAN catch regression:
4/5 checks turn RED on rewind, 5/5 turn GREEN with the fix restored.
Files in this commit:
- tests/uat/prototype/extension-page-harness.{html,ts} — the harness
page (chrome-extension URL, exposes window.__mokoshHarness.assertA6)
- tests/uat/prototype/a6.test.ts — Puppeteer driver (~270 lines)
- tests/uat/prototype/probe_*.mjs — diagnostic probes used to isolate
the SW dynamic-import blocker (probe_sw.mjs is the key one)
- src/test-hooks/offscreen-hooks.ts — added installFakeDisplayMedia +
dispatchEndedOnTrack + __mokoshOffscreenQuery bridge handler + auto-
install at module load
- vite.test.config.ts — added prototype harness page as rollup input;
added modulePreload.polyfill=false (red herring; harmless)
- src/background/index.ts — removed the broken SW-side gated dynamic
import (this is the BLOCKER unblocker — production 01-11 plan needs
to redesign SW-side test hooks before re-spawning)
Bundle hygiene: prototype runs against dist-test/; production dist/
remains hook-free (Tier-1 grep gate still GREEN, verified via
no-test-hooks-in-prod-bundle.test.ts in the unit test suite).
Vitest baseline: 89/89 GREEN preserved.
Runtime: ~7 seconds end-to-end (launch Chrome + open page + ensure
offscreen + start recording + dispatch ended + settle + assert).
See: research return for VERDICT + recommended next step.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| f44ca3afba |
wip(01-11): wave-3 partial — A1+A4 attempted, popup-bridge SW state query unreliable
Task 4 of Plan 01-11 attempted A1-A4 wiring. Empirical run reveals an
architectural blocker that needs orchestrator-level decision.
Current state after this commit (SKIP_PROD_REBUILD=1 npx tsx tests/uat/harness.test.ts):
- A0 [PASS]: production bundle hook-leak grep gate (17ms)
- A1 [FAIL]: SW bootstrap → setIdleMode — popup state never transitions
to '' despite keepalive ping + 3s waitFor. chrome.action.getPopup({})
from the popup page consistently returns the manifest default
(chrome-extension://<id>/src/popup/index.html), not the '' that
setIdleMode's chrome.action.setPopup({popup:''}) should produce.
- A2 [FAIL]: toolbar onClicked — badge never transitions to "REC" after
page.triggerExtensionAction(extension); 8s timeout. Either the
toolbar action isn't reaching the SW listener, OR getDisplayMedia's
picker isn't resolving in headless mode (despite the auto-select flag).
- A3 [FAIL]: offscreen target never appears (correlates with A2 — no
recording started, no offscreen document spawned).
- A4 [PASS]: trivially passes (offscreen count is 0 → 0, both before
+ after the click). Not a true assertion of behavior; would also pass
if the whole extension were broken.
- A5-A13: stubbed RED per plan.
Architectural blocker (Rule 4 — needs orchestrator decision):
- Puppeteer 25.0.2 + Chrome 148 + headless cannot reliably keep the MV3
SW alive long enough OR expose its real chrome.* state to a popup
page query. The popup-bridge architecture (Task 3 commit
|
|||
| dbd977c815 |
feat(01-11): wave-2 — Puppeteer harness scaffolding + A0 GREEN, popup-bridge architecture
Task 3 of Plan 01-11 (Puppeteer UAT harness).
Harness file tree (tests/uat/):
- harness.test.ts: tsx-runnable top-to-bottom harness entry point.
Runs A0 inline (filesystem grep gate, abort-on-fail T-1-11-01),
then launches Chrome + opens popup bridge + queries manifest, then
iterates A1-A13 stubs. Each stub throws "NOT YET IMPLEMENTED —
Plan 01-11 Task N wires this assertion". Exit code = 0 on full
pass, 1 otherwise. Final line: "UAT harness: N/14 assertions passed".
- lib/launch.ts: launchHarnessBrowser() — wraps puppeteer.launch with
enableExtensions:[dist-test/], headless default (HEADLESS=0
override), --no-sandbox + --auto-select-desktop-capture-source flags.
Polls browser.extensions() until the extension registers (empirically
~100ms but the first call right after launch returns Map(0)).
Opens both a blank page (for triggerExtensionAction) AND the popup
page (the bridge surface). Returns { browser, extension, extensionId,
sw, downloadsDir, page, popup }.
- lib/extension.ts: waitForOffscreenTarget + attachToOffscreen +
countOffscreenTargets. Offscreen attach uses target.type() ===
'background_page' + .asPage() (NOT .page() — RESEARCH §4 Pitfall 1).
- lib/sw.ts: chrome.* state queries via the POPUP page handle (NOT
the WebWorker handle — see architecture note below). getBadgeText,
getPopup, getManifest, getIconSize, getIsRecording (side-channeled
through badge text), fireOnStartup (via __mokoshTestQuery bridge),
sendSyntheticRecordingError, getNotificationSnapshot (via bridge),
keepalivePing (no-op message to wake SW for ~30s).
- lib/offscreen.ts: getDisplaySurface, simulateUserStop (the
dispatchEvent('ended') path per RESEARCH §7 BLOCKER — DO NOT REFACTOR
to track.stop()), getSegmentCount.
- lib/assertions.ts: runAssertion(idx, name, buffers, fn) wrapper —
records pass/fail/duration; on failure dumps last 30 lines of SW
+ offscreen console buffers to stderr before rethrowing. assertEqual
/ assertMatch / assertTrue / assertGte / waitFor polling helper.
- lib/zip.ts: jszip-based assertArchiveShape + extractEntryToFile for
assertions 12 + 13.
- README.md: runtime + local-debug + CI semantics + locale gotcha
+ dev-dep size note + assertion catalog table.
- tsconfig.json: per-tree type-check config (mirrors root tsconfig.json
compiler options but includes the harness tree explicitly).
Architecture refinement (DEVIATION from RESEARCH §1 — Rule 1+3 inline fix):
- RESEARCH §1 sketched `sw.evaluate(() => chrome.action.getBadgeText({}))`
as the chrome.* query path. Empirical probes during Task 3 execution
against Puppeteer 25.0.2 + Chrome 148 + --headless=true revealed two
blockers:
1. Puppeteer's WebWorker.evaluate runs in an ISOLATED WORLD that
carries SW globals (clients, registration, ...) but NOT the
extension's full chrome.* API surface. Object.keys(chrome) inside
sw.evaluate returns ["loadTimes","csi"] — the public webpage
chrome, not the extension chrome.
2. Chrome 148's headless mode aggressively suspends MV3 service
workers; subsequent swTarget.worker() calls return
"Protocol error: No target with given id found".
- WORKAROUND: open the popup page (chrome-extension://<id>/src/popup/
index.html) as a separate Puppeteer Page. The popup has full
chrome.* access (it's an extension context with same privileges as
the SW) AND stable Puppeteer lifetime. For SW-globalThis state
(__mokoshTest in the SW isolate, NOT in the popup), bridge via
chrome.runtime.sendMessage. The popup sends
{ type: '__mokoshTestQuery', op: 'snapshot' | 'fire-on-startup' |
'handler-types' }; the SW hook's onMessage handler responds.
- Bridge implementation added to src/test-hooks/sw-hooks.ts — registers
AFTER the production listeners so it never intercepts production
messages (__mokoshTest* type is unambiguously test-only). Tier-1
grep gate (no-test-hooks-in-prod-bundle.test.ts) continues to enforce
ZERO __mokoshTest occurrences in dist/ — the bridge handler is
tree-shaken alongside the rest of the hook module via the
__MOKOSH_UAT__ gate.
Other configuration changes:
- vitest.config.ts: exclude tests/uat/** from vitest discovery. The
Puppeteer harness is invoked via `npm run test:uat` (not vitest);
running it under vitest would try to launch real Chrome inside a
vitest worker. The .test.ts suffix is retained for editor +
naming-convention consistency with the rest of the tree.
Verification:
- npx tsc --noEmit (src/): exit 0
- npx tsc --noEmit -p tests/uat: exit 0
- npm run build: exit 0
- grep -rln '__mokoshTest|simulateUserStop|getSegmentCount|setCurrentStream|setSegmentCountGetter|__mokoshTestQuery|__mokoshKeepalive' dist/: ZERO matches
- npm run build:test: exit 0; dist-test/ populated with the new bridge code
- SKIP_BUILD=1 npx vitest run: 89/89 GREEN
- SKIP_PROD_REBUILD=1 npx tsx tests/uat/harness.test.ts:
→ A0 [PASS]: production bundle has no test-hook leaks (19ms)
→ Browser launches; popup opens; manifest read succeeds
→ A1-A13 [FAIL]: NOT YET IMPLEMENTED — Plan 01-11 Task N wires this
→ "UAT harness: 1/14 assertions passed, 13 failed (first failure: A1)"
→ Exit code: 1 (expected — 13 RED stubs intentional)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| cb1a729962 |
feat(01-11): wave-1 — gated test hooks for SW + offscreen, dist/ stays hook-free
Task 2 of Plan 01-11 (Puppeteer UAT harness).
Test hook surface:
- src/test-hooks/types.ts: canonical MokoshTestSurface — handlers
(onClicked, onStartup, notificationOnClicked), notificationCount,
lastNotificationOptions<true>, notificationIds, getCurrentStream,
getSegmentCount. globalThis.__mokoshTest ambient declaration.
- src/test-hooks/sw-hooks.ts: SW-side hook. Monkey-patches addListener
on chrome.action.onClicked / chrome.runtime.onStartup / chrome
.notifications.onClicked to capture handler refs while chaining to
the original. Wraps chrome.notifications.create across all four
overload shapes (id+options+cb, options+cb, id+options→Promise,
options→Promise) to increment notificationCount, save
lastNotificationOptions, push resolved id into notificationIds.
- src/test-hooks/offscreen-hooks.ts: offscreen-side hook. Exports
setCurrentStream + setSegmentCountGetter; the recorder calls both
inside startRecording after the mediaStream + segments assignments.
getCurrentStream getter closes over the cell so the harness reads
the live MediaStream for displaySurface inspection + 'ended'
dispatch (Bug B BLOCKER per RESEARCH §7).
- tests/uat/lib/test-hook-contract.d.ts: manual harness-side mirror of
MokoshTestSurface (decoupled from src/ to keep tests/ import-clean
per RESEARCH §11 resolution 5; drift risk documented inline).
Production-side wires (gated by __MOKOSH_UAT__ token):
- src/background/index.ts top-of-module: `if (__MOKOSH_UAT__) { await
import('../test-hooks/sw-hooks'); }`. MUST run before any chrome.*
addListener call below — top-of-module placement satisfies this.
- src/offscreen/recorder.ts top-of-module: symmetric gated dynamic
import + module-scoped testHooks reference.
- src/offscreen/recorder.ts inside startRecording (after mediaStream
assignment): `if (__MOKOSH_UAT__) { testHooks?.setCurrentStream(stream);
testHooks?.setSegmentCountGetter(() => segments.length); }`
- src/offscreen/recorder.ts inside onUserStoppedSharing (after
mediaStream = null): `if (__MOKOSH_UAT__) { testHooks?.setCurrentStream(null); }`
— T-1-11-05 (Repudiation: stale stream ref) mitigation.
Build-time token wiring:
- vite.config.ts: declares `define: { __MOKOSH_UAT__: 'false' }` (prod
default) + bumps `build.target: 'es2022'` so the top-level await in
the gated dynamic imports compiles (MDN: Chrome 89 / Edge 89 /
Firefox 89 / Safari 15 support TLA; MV3 floor Chrome 88 is
effectively Chrome 89+ in field — comfortably inside the envelope).
- vite.test.config.ts: overrides `define: { __MOKOSH_UAT__: 'true' }`
so the test bundle has the hooks active.
- vitest.config.ts: declares `define: { __MOKOSH_UAT__: 'false' }` for
vitest's own source-loading runs. CRITICAL — without this, vitest
would throw `ReferenceError: __MOKOSH_UAT__ is not defined` when
loading src/background/index.ts; OR if we'd used `import.meta.env.MODE
=== 'test'` (RESEARCH §6's initial guidance), vitest's default
MODE='test' would have ACTIVATED the hooks under unit tests +
clobbered every existing vi.fn() chrome.notifications.create mock.
The dedicated `__MOKOSH_UAT__` token sidesteps both failure modes
cleanly — a refinement on RESEARCH §6 documented in the comment
preambles of all three configs.
- globals.d.ts: declares `__MOKOSH_UAT__: boolean` ambient so
`npx tsc --noEmit` passes without per-file annotations.
- tsconfig.json: include adds `globals.d.ts`.
Notification options generic refinement:
- chrome.notifications.NotificationOptions is declared with a
`<true | false>` generic distinguishing "create" (all required —
true) from "update" (all optional — false). Plan 01-11's production
code always uses the create shape; types.ts + sw-hooks.ts pin to
`NotificationOptions<true>` so the harness reads iconUrl etc. as
definitely-present.
Verification:
- npx tsc --noEmit: exit 0
- npm run build: exit 0
- grep -rln '__mokoshTest\|simulateUserStop\|getSegmentCount\|setCurrentStream\|setSegmentCountGetter' dist/:
ZERO matches (Tier-1 gate stays GREEN)
- npm run build:test: exit 0; dist-test/ emits separate sw-hooks-*.js
+ offscreen-hooks-*.js chunks (the gated dynamic imports survive
tree-shaking when __MOKOSH_UAT__ === true)
- grep -rln '__mokoshTest' dist-test/: 2 matches
(assets/sw-hooks-*.js + assets/offscreen-hooks-*.js)
- SKIP_BUILD=1 npx vitest run: 89/89 GREEN
(83 baseline + 6 Tier-1 hook-leak surfaces)
- sw-bundle-import.test.ts: GREEN (the gated dynamic import does not
break production module init — the `if (false)` branch is never
reachable so the await + import are dead code in dist/)
In-flight bugs auto-fixed (Rule 1 + Rule 3):
- Rule 3: original RESEARCH §6 plan called for `import.meta.env.MODE
=== 'test'` as the gate; switched to `__MOKOSH_UAT__` define-token
after observing vitest contamination (vitest defaults MODE='test'
→ hooks activated under unit tests → 8 existing tests broke with
"Cannot read properties of undefined (reading 'calls')" because the
hook wrapper replaced vi.fn() mocks). Documented in the comment
preambles of all three configs as a refinement on RESEARCH §6.
- Rule 3: esbuild rejected TLA against the default ES2020 target;
bumped to es2022 (Chrome 89+ supports TLA per MDN — inside MV3
envelope). Recorded in vite.config.ts preamble.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 0cd50fde94 |
docs(debug): import Bug B recovery-flow debug record from prior session
Untracked file present at session spawn (per orchestrator pre-flight
intelligence). The Bug B debug investigation that produced commit
|
|||
| 96fa8e8e11 |
chore(01-11): wave-0 — install puppeteer + tsx, add vite.test.config + Tier-1 hook-leak grep gate
Task 1 of Plan 01-11 (Puppeteer UAT harness). - npm install --save-dev puppeteer@^25.0.2 tsx@^4 @types/node resolved: puppeteer@25.x, tsx@4.22.1, @types/node@25.8.0 pulls ~150MB Chromium binary at install time (T-1-11-03 — accepted, package-lock pins resolved hashes via @puppeteer/browsers). - package.json scripts: add build:test + test:uat (per RESEARCH §10 two-bundle orchestration); existing dev/build/preview/test untouched. - vite.test.config.ts: extends ./vite.config.ts via mergeConfig with mode:'test' + build.outDir:'dist-test' + emptyOutDir:true. Verified npm run build:test produces dist-test/ in 7.93s; npm run build keeps producing dist/ in 7.67s (no clobber). - tsconfig.json `include: ["src"]` already covers src/test-hooks/**/* via wildcard — no edit needed. - tests/background/no-test-hooks-in-prod-bundle.test.ts: Tier-1 gate mirroring sw-bundle-import.test.ts's execFile pattern. Greps the BUILT dist/ tree for 5 forbidden hook surfaces (one `it` per surface for granular failure isolation): __mokoshTest, simulateUserStop, getSegmentCount, setCurrentStream, setSegmentCountGetter. All 5 surfaces absent today (RED-then-GREEN polarity inverted — the gate is GREEN now and MUST stay GREEN after Task 2 lands the hooks). SKIP_BUILD=1 escape hatch for developer iteration. - .gitignore: add dist-test/ (no point versioning generated test bundle). Verification: - npx tsc --noEmit: exit 0 - npm run build: exit 0; dist/ populated (375.37 kB SW chunk) - npm run build:test: exit 0; dist-test/ populated (identical chunk sizes — the gated dynamic imports do not land until Task 2; this commit only proves the two-bundle plumbing) - SKIP_BUILD=1 npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts: 6/6 GREEN (1 build-sanity + 5 forbidden-surface) - SKIP_BUILD=1 npx vitest run (full suite): 89/89 GREEN (83 baseline + 6 new Tier-1 surfaces = 89) Working-tree cleanup: a stale 5.4 MB tests/fixtures/last_30sec.webm (unrelated operator smoke regen present at session spawn) was stashed before running the baseline — it caused the webm-playback test to time out at 5s. After stashing back to HEAD's 1.9 MB fixture, baseline passes cleanly. Not committing the fixture restoration here (pre-existing working-tree state, not part of Task 1). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 2669ce38e7 |
docs(intel): designer follow-up #1 — Newsreader has no Cyrillic glyphs
D-05 picked Newsreader as display serif. Engineering research
(01-12-RESEARCH.md §1, commit
|
|||
| 3df2750c64 |
docs(01-12): research Plan 01-12 (Design Integration) — 13 areas + BLOCKER
13 research surfaces investigated per Plan 01-12 prompt. Key findings: - BLOCKER §1: Newsreader has no Cyrillic glyphs (verified via Decision Brief's own embedded @font-face subsets + Production Type README). Russian display text in Newsreader will silently fall through to serif fallback. Recommend §1 R1: add Cyrillic-capable OFL serif (PT Serif / EB Garamond / Lora) to --mks-font-display stack. - §3 + §4: Mark legibility at 16 px is VERIFIED OK. rsvg-convert produces 406 / 784 / 1952 B PNGs at 16/48/128 — all clear Chrome imageUtil floors. The 2x2 weave holds at 16 px. - §10: 8 i18n strings extracted verbatim from Brief §02 (decoded via Python JSON-decode of the bundler-template script). Recommended __MSG_*__ key names + source-file mapping table included. - §12: D-09 (smoke gating) is NEAR-NO-OP. smoke.sh is a standalone bash script; nothing in src/ references it; dist/ carries no smoke artifacts. Recommend documenting current state + adding optional no-smoke-in-dist regression test. - §13: tokens.css line 12 Google Fonts @import migration → 8 local @font-face rules (Plex variable consolidation collapses the estimated 12-18 down to 8). All tooling verified installed on dev machine: rsvg-convert 2.60.0, pyftsubset 4.63.0, woff2_compress, inkscape 1.4.4. No npm install needed for execute-plan; only one-off font source TTF downloads. 7 assumptions logged for planner to surface (A1 default_locale, A2 popup DOM rework, A4 Cyrillic serif fallback are highest-impact). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 5efc2a89e3 |
docs(intel): ingest designer-team handoff v1 + record 9 brand decisions
Designer-team delivered two bundles (2026-05-17): - Mokosh.zip: presentation artifacts (Decision Brief + Surface Kit standalone HTMLs) - Mokosh Design System.zip: implementation handoff (tokens.css + mark/lockup SVGs + memo) Designer respected every contract: codename Mokosh preserved, MV3 CSP acknowledged (flagged dev-only Google Fonts @import in tokens.css line 12 for engineering to replace), Cyrillic-first typography (IBM Plex Sans), WCAG AA validation noted, made recommendations not unilateral decisions. 9 decisions resolved per user ack: - D-01..06, D-08, D-09: accepted designer defaults - D-07 OVERRIDE: B · "Mokosh — Session Capture" (vs designer's A · just "Mokosh") — operator-facing manifest:name surfaces capture purpose Brand decisions persisted to brand-decisions-v1.md; brand-identity.md Open Questions section marked resolved with backlink. Designer artifacts staged at .planning/intel/design-incoming/ for Plan 01-12 (Design Integration) consumption. Mokosh.zip presentation HTMLs (3.4 MB) preserved as audit/onboarding reference; can be pruned later if repo size warrants. Plan 01-12 implementation handoff catalogued in brand-decisions-v1.md §"Implementation handoff" — covers tokens.css ingestion, WOFF2 self-hosting, SVG → PNG rasterization, manifest:name + description update, welcome layout, VITE_DEV smoke gating. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 66e6c4af74 |
docs(01-11): create Puppeteer UAT harness plan (14 assertions, 9 tasks)
Retires operator-as-assertion-library role from Plan 01-09 Task 5. Bug A
(notification icon API rejection) and Bug B (state-machine routing of
user-stopped-sharing) both escaped vitest unit coverage and cost ~4-6h of
operator UAT cycles in Phase 1. Plan 01-11 ships a Puppeteer-driven Node
harness with CDP attach to SW + offscreen contexts; the 14 assertions cover
the Plan 01-08/01-09 functional contract end-to-end.
Locked from research (RESEARCH §1-§11):
- Puppeteer 25.0.2 + tsx + node:assert/strict (no vitest browser mode)
- Two-bundle separation via vite.test.config.ts (mode 'test' + dist-test/)
- Hook gating: import.meta.env.MODE === 'test' + dynamic import (Vite
tree-shakes from production)
- Bug B trigger: track.dispatchEvent(new Event('ended')) — NOT track.stop()
(W3C spec + empirical probe7 — track.stop does NOT fire 'ended')
- Tier-1 grep gate (tests/background/no-test-hooks-in-prod-bundle.test.ts)
enforces zero __mokoshTest in production dist/
- Single browser, serial assertions, bail-on-first-failure (open question 4)
Wave structure (4 waves):
- Wave 0 (Task 1): puppeteer+tsx install, vite.test.config, build:test +
test:uat scripts, Tier-1 grep gate committed GREEN.
- Wave 1 (Task 2): gated SW + offscreen hooks at src/test-hooks/; production
bundle remains hook-free.
- Wave 2 (Task 3): harness scaffolding — tests/uat/lib/* + harness.test.ts
with assertion 0 wired GREEN + assertions 1-13 stubbed RED.
- Wave 3 (Tasks 4-7): wire 13 assertions in 4 logically-grouped bundles
(1-4 toolbar/displaySurface; 5-7 SAVE+BugB+ERROR; 8-10 BugA+icons+manifest;
11-13 buffer continuity + ffprobe + zip).
- Wave 4 (Tasks 8-9): amend Plan 01-09 Task 5 to redirect functional steps
to npm run test:uat; operator confirms brand/design.
Bug A + Bug B each have RED-on-regression canonical demos required in the
respective task's commit body — proves the harness CAN catch the regression,
not just passes under current conditions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 969afbac89 |
docs(01-11): research Puppeteer UAT harness — empirical probes verify 10/10 unknowns
Probes 1-11 against local Chrome 148.0.7778.167 + Puppeteer 25.0.2:
- triggerExtensionAction works; popup-vs-onClicked contract confirmed
- --headless=new supports MV3 + getDisplayMedia (Xvfb not required)
- offscreen page reachable via background_page target type + .asPage()
- BLOCKER: track.stop() does NOT fire 'ended' per W3C spec — Bug B harness
must use track.dispatchEvent(new Event('ended')) instead
13-assertion implementation table + 7 pitfalls + 2-bundle build design.
Wave 0 grep gate enforces tree-shake of __mokoshTest from production.
|
|||
| 9d0313acd2 |
docs(01-09): amend Task 5 step 11 + success criteria #3 post-Bug B
Original step 11 expected stop-sharing → ERROR badge + recovery
notification; under Bug B fix (
|
|||
| b9eeeeb386 |
feat(01-09): GREEN — Bug B route user-stopped-sharing → IDLE; other codes → ERROR (preserved)
Patches the RECORDING_ERROR onMessage handler in src/background/index.ts
(lines 725-744 pre-patch) with conditional routing on the incoming
`message.error` payload:
- 'user-stopped-sharing' → setIdleMode() (popup empties; badge OFF;
isRecording flipped to false). Recovery notification suppressed —
the operator stopped deliberately, surfacing one would be UX noise.
The offscreen recorder's onUserStoppedSharing has already cleared
the buffer (src/offscreen/recorder.ts:457 resetBuffer), so IDLE is
the correct landing state.
- all other codes → setErrorMode() + recovery notification, preserving
the existing operator-facing surface for genuine capture failures
(codec-unsupported, wrong-display-surface, capture-failed, etc.).
Closes the operator-lockout regression observed in Plan 01-09 Task 5
empirical UAT: after Chrome's "Stop sharing" banner click, the badge
stayed yellow and the popup pinned to SAVE-only, gating
chrome.action.onClicked behind the popup forever. Operator had no
restart path. With IDLE routing, the popup empties and the toolbar
click fires startVideoCapture as designed.
Tests: 83/83 GREEN (was 81; +2 from Tests E+F). tsc clean. Build exit 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 91b4475ea1 |
test(01-09): RED — Bug B route user-stopped-sharing → IDLE; other codes → ERROR
Adds Tests E + F to tests/background/badge-state-machine.test.ts pinning
the conditional-routing contract for RECORDING_ERROR onMessage:
E (RED today): RECORDING_ERROR{error:'user-stopped-sharing'} must route
through setIdleMode — badge OFF (text '', red #D32F2F), popup ''. The
current handler routes ALL codes through setErrorMode, locking the
operator out of restart (popup wins toolbar.onClicked forever).
F (GREEN today, preserved after fix): RECORDING_ERROR with any other
error code (representative: 'codec-unsupported') continues to route
through setErrorMode — badge ERR + yellow #F9A825 + popup html. This
is the defensive-fallback regression pin guarding against the patch
over-rotating to IDLE for all codes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 6dca46529b |
chore(01-09): resume from pause — sync STATE.md, remove HANDOFF.json
HANDOFF.json artifact consumed per /gsd-resume-work workflow (one-shot resumption pointer). STATE.md synced forward to reflect true mid-Plan-01-09 state — status flipped planning→executing, progress 90→92, Current Position now shows Phase 1 REOPENED with Plan 01-09 Bug B pending and Plan 01-10 Wave 3 pending. Session continuity records: - Pause checkpoint commit: |
|||
| f768498b87 |
docs(intel): unlock creative decisions across brand + design + assets specs
Adds brand-identity.md (Brief #1: name + blurb + creative slate). Rewrites design-system.md and assets-spec.md to flip aesthetic prescriptions into options while keeping Chrome MV3 / WCAG / icon-size floors as binding (marked (FLOOR) inline). Mokosh codename is the only locked decision; every other creative choice delegated to brand + design teams with explicit ownership annotations. Three-file intel pack now consistent: - brand-identity.md: brand-team primary (naming, voice, blurb) - design-system.md: design-team primary (palette, type, components) - assets-spec.md: design-team primary (deliverables + floors) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| a881bf0f6a |
fix(01-09): Bug A — icon placeholders unblock notification API per assets-spec.md Path A
icons/icon{16,48,128}.png at 574/1153/2615 bytes — all above Chrome
imageUtil silent-rejection floors per assets-spec.md A-01/A-02/A-03.
Auto-generated via ImageMagick (Path A pathway). Branded variants
swap in cleanly when design team delivers (Path B/C).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| ed82fd6051 | wip: phase-01 paused mid-Plan-01-09; Bug B fix + icon placeholder commit pending | |||
| 949aa03db5 |
docs(intel): design system + assets spec for designer-team handoff
Two new cross-phase intel docs surfacing the visual + interaction
language and the concrete asset deliverables list.
Triggered by Plan 01-09's notification-icon failure: the original
placeholder PNGs (icons/icon{16,48,128}.png at 79/123/306 bytes)
are too small for chrome.notifications.create's imageUtil validation.
Plan 01-09 closeout blocks on valid replacement icons.
design-system.md captures:
- Brand voice (quietly competent operator-tool; not playful)
- Identity (Mokosh codename + "AI Call Recorder" display name)
- Color tokens (semantic state + UI surfaces; WCAG AA validated)
- Typography (system fonts only; type scale)
- Iconography (solid filled, 24px grid, neutral mark + state via badge)
- Spacing (4px base scale)
- Motion (conservative; reduced-motion-aware)
- Component conventions (toolbar action, notification, popup, welcome)
- Accessibility & i18n (Russian-first, contrast floors)
- Open design decisions (mark concept, pulsing badge, tagline)
assets-spec.md captures:
- Priority 0 (blocks Plan 01-09): icons 16/48/128 with min file sizes
+ dimension/format requirements + validation commands
- Priority 1 (Plan 01-10): welcome page hero, optional 192px icon
- Priority 2 (future phases): per-state icon variants, popup polish,
runbook visuals
- Three implementation paths: auto-generated placeholders (interim),
design-first (commission), hire/commission
User will delegate execution to designer team; specs are handoff-ready
with binding technical floor + designer-judgment aesthetic direction.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| c711d7e74e |
docs(01-09): SUMMARY — Tasks 1-4 autonomous complete; Task 5 awaiting operator
Plan 01-09 SUMMARY:
• 17 new tests landed GREEN (4 displaySurface + 5 toolbar-action
including W-02 popup-idle-race + 4 badge + 4 notification).
• Baseline 64 + 17 new = 81 GREEN. Full suite 18 files / 81 tests.
• Tier-1 SW-bundle-import gate (Layer 1 + 2) remains GREEN.
• tsc clean; npm run build clean; dist/manifest.json carries
notifications permission.
• 4 deviation rules auto-fixed inline (navigator getter helper,
jsdom-free W-02 Test E refactor, cursor type-widening cast,
chrome.* listener try/catch for pre-existing test compatibility).
• Task 5 (operator empirical checkpoint) deferred per plan.
|
|||
| 06dee246c9 |
feat(01-09): GREEN — toolbar onClicked + badge state machine + onStartup notification + SAVE-only popup
Plan 01-09 Task 4 GREEN — flips all 13 Task 3 RED tests to GREEN:
src/background/index.ts:
• Badge palette + notification id prefix constants (SCREAMING_SNAKE).
• setBadgeState(state) helper: 3-state machine REC/OFF/ERROR with
deterministic setBadgeText + setBadgeBackgroundColor + setTitle.
Each chrome call wrapped in try/catch (defense in depth).
• setIdleMode / setRecordingMode / setErrorMode helpers — drive the
setPopup dance: '' in OFF (so onClicked fires), html path in REC/
ERROR (so popup opens for SAVE).
• startVideoCapture wires setRecordingMode on success, setErrorMode
in catch.
• chrome.action.onClicked.addListener — direct toolbar-to-picker flow
(no popup needed for start). isRecording guard prevents double-start.
• chrome.runtime.onStartup.addListener — fires once per browser
session; creates mokosh-startup- notification inviting recording.
• chrome.notifications.onClicked.addListener — T-1-09-01 spoofing
mitigation via 'mokosh-' prefix gate; clears notification + invokes
startVideoCapture (notification click is a valid activation gesture).
• RECORDING_ERROR onMessage branch — setErrorMode + creates a
mokosh-recovery- notification inviting the operator to restart.
• initialize() calls setIdleMode at SW boot — ensures fresh OFF state
on every (re-)spawn including Chrome's idle-eviction respawn.
• All new listener registrations wrapped in try/catch so unit-test
chrome stubs that don't define action/notifications/onStartup don't
crash SW load (preserves the 5 pre-existing request-id-protocol +
1 port-lifecycle-continuous tests as GREEN).
src/popup/index.ts:
• Removed checkPermissions + requestPermissions functions entirely
(no more REQUEST_PERMISSIONS round-trip on popup open).
• popupState defaults isRecording=true, hasPermissions=true under
SAVE-only charter — the popup ONLY opens when recording is active
(REC/ERROR setPopup html path), so SAVE button is always enabled.
• init() calls updateUI() directly (no async permission probe).
• Empty-state copy updated: 'Откройте запись через иконку расширения'
(Open recording via the extension icon — points operator back to
the toolbar for starting a new session).
• saveArchive() simplified: no permission re-check.
manifest.json:
• Added 'notifications' to permissions array (preserves all existing).
• default_popup retained — popup still opens in REC/ERROR modes.
smoke.sh (W-04 5-sub-step update):
• SHARE_TARGET='Entire screen' (was 'Mokosh Smoke Test').
• Added 14-line locale-fallback comment block citing Chromium
generated_resources.grd as authoritative source + 4 known locale
strings + KEEP_PROFILE=1 fallback path.
• <title> changed to 'Mokosh Smoke Test — monitor mode' to keep tab
title distinct from the screen-source string.
• <ol> instruction updated: picker auto-accepts entire screen, not
the tab. Body intro paragraph also updated.
• T+/wall timer overlay (commit
|
|||
| 2d7ff7d4e3 |
test(01-09): RED — toolbar-onClicked + badge state machine + onStartup notification + popup SAVE-only
Plan 01-09 Task 3 RED — 13 tests across 3 new files:
tests/background/toolbar-action.test.ts (5 tests):
A: chrome.action.onClicked.addListener registered at SW init
B: onClicked while not recording triggers startVideoCapture
C: onClicked while isRecording does NOT double-start
D: setPopup('') in OFF mode, popup html path in REC mode
E: popup init does NOT send REQUEST_PERMISSIONS + saveButton enabled
(W-02 fix — without jsdom, uses node-env document stub)
tests/background/badge-state-machine.test.ts (4 tests):
A: REC state = text 'REC' + #00C853 green + Recording title
B: OFF state = text '' + #D32F2F red + Not recording title
(fired at SW init via initialize → setIdleMode)
C: ERROR state = text 'ERR' + #F9A825 yellow + error title
D: RECORDING_ERROR onMessage triggers setBadgeText('ERR') within microtask
tests/background/onstartup-notification.test.ts (4 tests):
A: chrome.runtime.onStartup.addListener registered at SW load
B: onStartup fires exactly one mokosh-startup- notification
with basic type + 'Mokosh ready' title + Click-instructed message
C: notifications.onClicked with mokosh- id clears + triggers START_RECORDING
D: RECORDING_ERROR onMessage triggers mokosh-recovery- notification
Task 4 will flip all 13 to GREEN by adding the listeners + state machine
+ helpers in src/background/index.ts, popup SAVE-only, manifest update.
Deviation Rule 3: jsdom not in node_modules; refactored Test E to use a
node-env document stub instead of @vitest-environment jsdom pragma.
|
|||
| de162b4293 |
feat(01-09): GREEN — displaySurface:'monitor' constraint + post-grant validation
Plan 01-09 Task 2 GREEN — flips Task 1 tests 1, 2, 4 to GREEN:
1. CaptureErrorCode union extended with 'wrong-display-surface'.
2. classifyCaptureError branch matches 'wrong-display-surface' prefix.
3. getDisplayMedia call carries {video:{displaySurface:'monitor',
cursor:'always'},audio:false} (Plan 01-09 D-15-display-surface +
Phase 5 cursor:'always' opportunistic lift).
4. Post-grant validation block reads track.getSettings().displaySurface;
on non-monitor pick: tears down stream, nulls mediaStream, throws
wrong-display-surface Error which routes through the existing
classifyCaptureError + RECORDING_ERROR broadcast path.
Type note: lib.dom.d.ts MediaTrackConstraints omits 'cursor' — used
explicit type-widening cast (NOT 'as any') to add the field without
suppressing other type checking.
Tests: 4/4 GREEN; full suite 15 files / 68 tests / GREEN.
tsc --noEmit exit 0. npm run build exit 0.
|
|||
| 333e0dcb18 |
test(01-09): RED — displaySurface:'monitor' + cursor:'always' constraint contract
Plan 01-09 Task 1 RED — pins 4 tests for D-15-display-surface contract:
1. getDisplayMedia called with strict {video:{displaySurface:'monitor',
cursor:'always'},audio:false} (deep-equality, NOT objectContaining).
2. Non-monitor pick (browser/window) tears down stream + emits
RECORDING_ERROR wrong-display-surface.
3. Monitor pick does NOT trip wrong-display-surface (over-fire guard).
4. classifyCaptureError routes 'wrong-display-surface' message prefix
to 'wrong-display-surface' code.
Task 2 will flip Tests 1, 2, 4 to GREEN by adding constraints +
post-grant validation + extending CaptureErrorCode union.
Deviation Rule 3: navigator getter-only in Vitest's node env required
Object.defineProperty wrapper (installNavigatorStub helper) instead
of direct assignment.
|
|||
| e40949d1d2 |
test(01-08): regenerate last_30sec.webm fixture + split remux input/output fixtures
Plan 01-08 Task 5 closeout. The post-B+ smoke run produced a working single-EBML WebM (28.76s, 676 frames, 1.89 MB, monotonic 0→28.76s timestamps). Operator-confirmed empirically (timer overlay in smoke HTML showed the latest frames matched expectations). Two-fixture split resolves a test-design conflict surfaced when last_30sec.webm flipped from pre-remux input shape to post-remux output shape: - tests/fixtures/last_30sec.webm — POST-REMUX output (single EBML, 41 ffmpeg dry-run lines). Validates webm-playback.test.ts' playable-duration + structural assertions. - tests/fixtures/raw-3ebml-concat.webm — PRE-REMUX input (3-EBML concat, 299 ffmpeg dry-run lines = 3 segment boundaries). Preserved from the original 2026-05-15 Phase 1 closure fixture. Used by webm-remux.test.ts to test that remuxSegments correctly transforms 3-EBML input → single-EBML output. tests/background/webm-remux.test.ts FIXTURE_PATH updated to point at raw-3ebml-concat.webm; the hardcoded EBML byte offsets [0, 509038, 970967] and frame bounds [905, 912] remain valid against that preserved input. Result: 64/64 vitest GREEN (was 61/64). tsc clean. Build exit 0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 923aaca3a8 |
test(smoke): add T+/wall timer overlay to smoke page for D-13 stale-gap measurement
The smoke test page now displays a fixed top-right overlay showing elapsed-since-load (T+) and wall-clock (HH:MM:SS). Operator can: - Note timer values at save-click moment - Examine the saved WebM's last frame for the visible timer values - Compute (save-click value − last-frame value) = operator-visible "stale gap" the D-13 architecture leaks This converts the subjective "video isn't latest" observation into a precise measurement, enabling correct routing: - Gap ≤ 10s → matches D-13 in-flight-segment trade-off (architectural, not a regression; would inform a follow-up plan to reduce the gap) - Gap > 10s → real regression (ring buffer rotation broken or similar) Pure diagnostic addition to smoke.sh; no extension code changed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 073e7b3584 |
docs(debug-01-08): update Resolution — B+ polyfill closed Layer 2 gap
The 01-08 fix took TWO iterations, not one. Iteration 1 (commits |
|||
| 761dfc0388 |
test(debug-01-08): extend Tier-1 gate to Layer 2 (exercises remuxSegments)
The original Layer 1 gate (
|
|||
| dd7bf00d1d |
fix(01-08): B+ — vite-plugin-node-polyfills for Buffer (resolves runtime ts-ebml crash)
Layer 2 of the extended SW-bundle-import gate caught a runtime
ReferenceError: Buffer is not defined at EBMLDecoder.constructor
(this._buffer = Buffer.alloc(0)). Reached from remuxSegments via
extractFramesFromSegment for every input segment — would crash
the SW on every SAVE_ARCHIVE click in real Chrome.
ts-ebml has a 5-year-old open issue (legokichi/ts-ebml#37,
"Can't use Buffer in browser") acknowledging the incompatibility
with no maintainer fix. The canonical Vite workaround is
vite-plugin-node-polyfills with a narrow Buffer-only config (per
the plugin author's official docs).
Changes:
- vite-plugin-node-polyfills@0.27.0 added as devDependency
- vite.config.ts adds nodePolyfills plugin with narrow config:
include: ['buffer'], globals.Buffer: true, globals.global: false,
globals.process: false, protocolImports: false (Buffer only, no
stdlib pull-in)
- bundle delta: SW chunk 373.05 kB (-0.49 kB vs C-config alone);
+27.48 kB shared polyfill chunk (index-CgqXENQe.js, used by SW
and offscreen). Net cost ~26.3 kB for full Buffer support.
Bundle verification:
- bundled EBMLDecoder.js now reads `this._buffer = me.alloc(0)`
where `me` is the imported polyfill Buffer (was `Buffer.alloc(0)`
against undefined globalThis.Buffer). Same rewrite applied to
all 3 Buffer.alloc/Buffer.concat/Buffer.from sites in ts-ebml.
- bundle does NOT depend on globalThis.Buffer (the polyfill
rewrites references as imports, not as global assignments) —
Layer 1 of the gate still strips Buffer from globalThis and
passes, confirming this.
Layer 2 gate: RED → GREEN. resolve.alias.ebml fix from commit
|
|||
| cc6e81a825 |
docs(debug-01-08): archive — fix landed, gate completed
Resolves Vite/Rollup CJS-interop tree-shake bug that killed SW init. Two-part fix: - vite.config.ts resolve.alias for ebml -> CJS main entry ( |
|||
| 74400ae6ac |
test(debug-01-08): complete SW-bundle-import gate — mock chrome.* surface
The Tier-1 SW-bundle-loadability gate (
|
|||
| 52c76362ae |
fix(01-08): resolve ebml via CJS main entry to bypass Vite/Rollup tree-shake bug
Vite's @rollup/plugin-commonjs failed to bridge ts-ebml's
`require("ebml")` against ebml's mixed-main/module/browser package.
Rollup tree-shook ebml.esm.js entirely, leaving `var Pc={}` as a
dangling placeholder. ts-ebml/tools.js's destructure
`{tools:f}=Pc` threw TypeError at SW top-level module init,
blocking handler registration -> chrome://serviceworker-internals
Status=STARTING forever.
`resolve.alias: { ebml: 'ebml/lib/ebml.js' }` forces resolution to
the CJS main entry whose assignment-style exports survive
plugin-commonjs's namespace allocation. Empirically verified:
bundle init progresses ~340 KB further; readVint error gone.
Probes C1 (resolve.mainFields), C2 (treeshake.moduleSideEffects),
C3 (C1+C2 combined), C4 (commonjsOptions.strictRequires) were
all falsified before C-config landed.
Resolves: .planning/debug/01-08-sw-incompatibility.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| c75854cbef |
test(debug-01-08): RED Tier-1 SW-bundle-loadability gate + corrected hypothesis
Adds tests/background/sw-bundle-import.test.ts that loads the built SW
chunk under SW-simulated globals (Buffer/process/window/document stripped)
via a spawned Node child process. Pins the orchestrator-side gap that
caused Plan 01-08's SW init crash: the prior deps test only checked
SOURCE packages under default Node globals, never the bundled output, so
Vite/Rollup's CJS-interop bug (tree-shaking the `ebml` package while
leaving a dangling `{tools:f}=Pc` destructure against an empty Pc) went
undetected until operator empirical smoke.
RED against HEAD
|
|||
| aabbd0c05c |
docs(01-08): write SUMMARY — Tasks 1-4 autonomous complete; Task 5 awaiting operator
- D-14-remux WebM remux pipeline (ts-ebml parse + webm-muxer write)
replaces D-13 file-concat; single-EBML-headered output empirically
spans 29.954 s of 912 VP9 frames (matching the per-segment sum
301+300+311 with zero loss).
- All 5 RED unit tests in tests/background/webm-remux.test.ts flipped
GREEN; 2 SW-compat tests in webm-remux-deps.test.ts GREEN; 53 baseline
tests preserved. tsc exit 0. npm run build exit 0.
- mergeVideoSegments deleted from src/background/index.ts; only a
retirement comment naming Plan 01-08 D-14-remux remains.
EmptyVideoBufferError surface preserved (W-01 free-text rename only).
- CONTEXT.md amendment provenance verified intact (B-01 grep checks all
pass — no file mutation by this plan; the orchestrator landed the
amendment at plan-creation time in commit
|
|||
| 35db6c2357 |
feat(01-08): swap mergeVideoSegments -> await remuxSegments at call site
- src/background/index.ts now imports remuxSegments from './webm-remux'
and awaits it in createArchive instead of synchronously calling the
retired file-concat mergeVideoSegments.
- mergeVideoSegments function declaration deleted entirely; only a
retirement comment remains naming Plan 01-08 D-14-remux as the
superseding decision.
- EmptyVideoBufferError throw paths preserved on (a) zero segments
AND (b) zero-byte output. Error message free-text changed from
"merged video blob is zero bytes" to "remuxed video blob is zero
bytes"; pre-flight grep (W-01 fix from plan checker pass)
confirmed no downstream consumer matches on the legacy string —
request-id-protocol.test.ts asserts on error.code ('empty-video-
buffer'), not the free-text message.
- createArchive remains async (was already declared async); saveArchive
already awaits createArchive so no upstream signature changes.
- Stale comment in decodeBufferSegments referencing mergeVideoSegments
updated to reflect the new remux pipeline (Rule 3: keep forward-
references accurate).
- CONTEXT.md amendment provenance verified intact via 4 grep checks
(B-01 fix from plan checker, folded from retired Task 6):
(a) D-14-remux disambiguated marker present (1 match)
(b) original D-13 line preserved (1 match)
(c) D-17-port-lifecycle amendment intact (1 match)
(d) webm-remux.ts replaces citation present (1 match)
No CONTEXT.md mutation by this task — verify-only step.
- npm run build exit 0; main SW bundle 374.56 KB (108.44 KB gzipped,
matches the d13 library survey's ~100 KB estimate for ts-ebml +
webm-muxer combined).
- Full suite: 13 files / 60 GREEN + 2 RED (webm-playback duration
assertions waiting on Task 5 fixture regen). tsc exit 0.
|
|||
| 41e94d5daa |
feat(01-08): implement remuxSegments — single-EBML WebM remux via ts-ebml + webm-muxer
Drives all 5 RED tests in tests/background/webm-remux.test.ts to GREEN.
- Parses each VideoSegment via ts-ebml Decoder; tracks current Cluster
Timestamp; extracts each SimpleBlock's VP9 frame(s) + keyframe flag
+ segment-local timestamp via tools.ebmlBlock.
- Re-emits all frames through a single webm-muxer Muxer<ArrayBufferTarget>
configured with type:'webm', codec:'V_VP9', and adjusted monotonic
timestamps (segmentBaseMs + cluster.Timestamp + block.timecode,
microseconds for the muxer).
- Picks track info (PixelWidth, PixelHeight, optional CodecPrivate)
from first segment that exposes them; falls back to 1024x768 with
a logged warning per Task 5's failure-mode (e).
- Defensive: empty input -> empty Blob (Test 5); sort by timestamp
ascending (mirrors retired mergeVideoSegments order discipline).
- 434 LOC including extensive JSDoc per project style; 8 small named
helpers, no nested mega-functions.
- Empirically: 3-segment fixture -> 912 frames in 29.954 s,
1_643_057 bytes (single-EBML); ffprobe duration=29.94s, count_frames=912.
- Logging via new Logger('Remux'); no console.* anywhere; no as any;
no @ts-ignore.
Full suite: 13 files / 60 GREEN + 2 RED (webm-playback duration assertions
still failing against the stale fixture — Task 4 swaps the call site,
Task 5 regenerates the fixture). tsc exit 0.
|
|||
| 407e683e9b |
test(01-08): RED unit tests for remuxSegments — single-EBML + monotonic + frame-count + size + empty
- 5 RED tests pinning the contract for src/background/webm-remux.ts (created in Task 3). All fail with "module missing" today — the Task 3 GREEN gate. - Test 1: exactly 1 EBML header + 1 Segment magic in output. - Test 2: output size within [0.7x, 1.3x] of input sum. - Test 3: ffprobe format=duration >= 25_000 ms (skip-if-no-ffprobe). - Test 4: ffprobe -count_frames in [905, 912] (per-seg sum 912 ± 3 boundary partial-frame drops, I-01 tightening). - Test 5: empty input -> empty Blob (defense-in-depth). - Fixture sliced at d13-confirmed byte offsets (0 / 509038 / 970967); verified against committed last_30sec.webm at Task 2 land time. - Baseline counts: 13 files / 62 tests / 7 failed (2 webm-playback + 5 new webm-remux) | 55 passed. tsc exit 0. |
|||
| 503531485c |
feat(01-08): install ts-ebml + webm-muxer; pin SW-compat via deps test
- Add ts-ebml ^3.0.2 (parse half) and webm-muxer ^5.1.4 (write half) per
CONTEXT.md amendment D-14-remux; both MIT, both verified SW-compatible
in the d13 debug-session library survey.
- tests/background/webm-remux-deps.test.ts pins two contracts:
(a) named exports surface (Muxer + ArrayBufferTarget + Decoder).
(b) both libraries import cleanly when window/document are absent on
globalThis — guards the published dist against accidentally
acquiring DOM globals on the hot path that would crash the
Chrome service-worker runtime.
- Note: webm-muxer 5.1.4 upstream-deprecated in favor of Mediabunny; the
pinned version still meets the d13 architectural requirement
(single-EBML output via addVideoChunkRaw). Migration to Mediabunny is
out of scope for Plan 01-08 and would require a new ADR.
- Baseline 53 GREEN + 2 new GREEN; tsc clean; 2 webm-playback duration
RED still pending (drive to GREEN in Tasks 3-5).
|
|||
| 2e499d7387 |
docs(01): add Plans 01-08 / 01-09 / 01-10 (amended Phase 1 charter)
Plans cover the post-D-13 architecture and the auto-start UX charter expansion that landed during 2026-05-16 UAT: - Plan 01-08 — WebM remux via ts-ebml@3.0.2 + webm-muxer@5.1.4. Replaces the broken file-concat in mergeVideoSegments with a real single-EBML remux. Drives the 2 RED tests in tests/offscreen/webm-playback.test.ts to GREEN. Regenerates the canonical fixture against the remuxer. 5 tasks (4 TDD + 1 operator empirical checkpoint), wave 1. - Plan 01-09 — Whole-desktop constraint (displaySurface:'monitor', cursor:'always') + post-grant validation, chrome.action.onClicked direct toolbar invocation, chrome.action badge state machine (REC/OFF/ERROR), chrome.runtime.onStartup notification + recovery notification on onUserStoppedSharing, popup scoped to SAVE-only. 17 new test assertions across 4 test files. smoke.sh updated to auto-select an entire screen. 5 tasks (4 TDD + 1 operator empirical checkpoint), wave 2 (depends on 01-08). - Plan 01-10 — chrome.runtime.onInstalled welcome tab on first install via chrome.storage.local guard; vanilla welcome.html/ts/css bundle with single "Начать запись" button consuming install-time activation. Uses centralized Logger pattern. 4 tasks (3 TDD + 1 operator empirical checkpoint), wave 3 (depends on 01-09). CONTEXT.md amendment block appended with 4 disambiguated decisions: - D-14-remux: WebM remux supersedes D-13 file-concat - D-15-display-surface: whole-desktop + cursor visibility lifted from Phase 5 deferral - D-16-toolbar: toolbar onClicked + popup SAVE-only + badge state machine + onStartup/recovery notifications - D-17-onboarding: welcome tab on first install (distinct from D-17-port-lifecycle from Option C) The earlier D-17 port-lifecycle heading also renamed to hyphenated form for cross-ref consistency. Plan-check loop: 3 iterations (initial + 2 revisions). Iteration 1 surfaced 11 findings (2 BLOCKER + 6 WARNING + 3 INFO); all addressed via revision iter 1 with checker-recommended fixes. Iteration 2 surfaced 3 derivative regressions (literal-string grep anchors from the iter-1 fixes did not match live CONTEXT.md); all addressed in iter 2 with empirically-validated literals. Iteration 3 PASSED clean. Validation: gsd-sdk frontmatter.validate + verify.plan-structure both return valid=True for all 3 plans. Plan 01-08 Task 4 verify-chain grep tested end-to-end against live CONTEXT.md (exit 0). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| bc310d98cf |
revert(01): reopen Phase 1 — D-13 multi-EBML-concat is unplayable
REQ-video-ring-buffer flipped from [x] back to [ ]. ROADMAP.md Phase 1 row reverted from [x] Closed 2026-05-15 to [ ] reopened 2026-05-16. STATE.md status flipped phase_complete → phase_reopened with full historical narrative preserved. Root cause (confirmed at byte level by gsd-debugger 2026-05-16): D-13's concat-of-self-contained-WebM-segments architecture produces a 3-EBML-header WebM that standards-compliant Matroska parsers (mpv, ffmpeg, Chrome HTMLMediaElement) play only as the first segment (~9.94 s) and silently drop the remaining 2 segments. Confirmed via operator mpv drag-drop test of BOTH the canonical 2026-05-15 closure fixture and the 2026-05-16 UAT-produced fixture — both exhibit the same broken playback. The 2026-05-15 "operator-confirmed clean Chrome playback" assessment was insufficient: it verified the file plays without freezing but did not measure total duration. Phase 1's primary deliverable (REQ-video-ring-buffer / SPEC §10 #7) is therefore NOT satisfied. Fix path chosen by user: ts-ebml (parse) + webm-muxer (write) to replace mergeVideoSegments file-concat with real single-EBML remux. Will land as Plan 01-08 via fresh /gsd-plan-phase ceremony. RED test landed in tests/offscreen/webm-playback.test.ts (2 new assertions on container-format-duration + ffmpeg-full-decode-duration). 2 failures, 53 baseline tests still GREEN. Option C port-lifecycle refactor (debug session empty-archive-port-race, commits 674c415..f0871c0) DID land cleanly and is retained — that fix was orthogonal and correctly resolved the silent-empty-archive symptom that previously masked this deeper bug. Debug session: .planning/debug/d13-multi-ebml-concat-unplayable.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| f1026954fc | test(01): UAT BLOCKER #2 — D-13 multi-EBML-concat plays only ~9 s; both committed fixture and UAT output exhibit same broken playback. Phase 1 architecture finding. |