3c1280ed2d00868ba3e1dfe2a318ed2c1ac40338
268 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 865d394ae0 |
docs(01-12): wave-7 pre-checkpoint — log out-of-scope discovery (setimmediate polyfill new Function pre-existing)
Wave 7 pre-checkpoint bundle gates per feedback-pre-checkpoint-bundle-gates.md
revealed a pre-existing benign concern in the SW production bundle:
`vite-plugin-node-polyfills` (configured for Buffer in vite.config.ts)
bundles the upstream `setimmediate` package which contains a fallback
`new Function("" + I)` evaluated when setImmediate is called with a
non-function argument. Production source code does NOT call
setImmediate(string); the construct is dead at the runtime call-graph
level but Rollup conservatively preserves it (behind a runtime
typeof check, not a static dead branch).
Verified pre-existing across Phase 1 history via `git checkout main --
src/background/index.ts vite.config.ts && npm run build` — same
`new Function` count. Plan 01-12 made NO changes to the polyfill
configuration; this is logged for future tightening (Phase 5
hardening or a dedicated MV3 CSP audit plan), NOT for fix in this
plan per the deviation-rule SCOPE BOUNDARY.
All other pre-checkpoint bundle gates PASS:
- Tier-1 forbidden-strings: 13/13 GREEN (no new test-mode symbols)
- SW-bundle-import: 15/15 GREEN
- Node-globals (Buffer.*) in SW chunk: 0
- DOM-globals direct SW calls: none
- Manifest validation: PASS (__MSG_*__ + default_locale='en' +
16 i18n keys per locale; en+ru parity verified)
- Tokens.css MV3 CSP self-host: 0 googleapis / 0 https://fonts in dist/
- Icons rasterized: 8-bit RGBA at 406/784/1952 B
- vitest: 147/147 GREEN
- npm run test:uat: 21/21 GREEN (A1..A14 regression-free + A18..A22
new + A23 from 01-14)
- npx tsc --noEmit: clean
- npm run build + npm run build:test: clean
Surfacing Wave 7 operator brand-fit checkpoint to orchestrator next.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| b909c374cc |
feat(01-12): wave-6 task-1 — harness A18-A22 (font reachability + icon-distinct + manifest-i18n + Lora-resolved + welcome-tokens)
UAT harness extended with 5 new page-side assertions following the 01-13 Approach B pattern (page-side assertA* + host-side driveA* wrapper + harness.test.ts orchestrator entry): A18 — Lora WOFF2 reachable from harness page (font self-host MV3 CSP invariant). Walks document.styleSheets for the first @font-face rule referencing Lora, resolves the rebased asset URL (handles Vite's content-hashing), fetches, asserts byteLength >= 40_000 (subset Lora is ~49 KB) + WOFF2 signature 'wOF2'. 4 checks. A19 — icons rasterized from Loom mark (not Bug A placeholders). Fetches icon128.png, parses IHDR bytes 24-25 (bit-depth + color-type), asserts (8, 6) RGBA vs the placeholder (16, 2) RGB. 2 checks. A20 — manifest:name resolves via chrome i18n. Reads chrome.runtime.getManifest().name; asserts it matches EN extName 'Mokosh — Session Capture' OR RU 'Mokosh — Запись сессии' (robust to whatever locale Chrome uses); explicitly checks no __MSG_ placeholder leaks. 2 checks. A21 — --mks-font-display resolves to Lora stack. Creates transient .mks-display-1 probe div, reads getComputedStyle.fontFamily, asserts the stack starts with 'Lora' or '"Lora"' (accommodates both quoted + unquoted forms across Chrome versions); explicitly checks no Newsreader leak (R2 substitution complete). 2 checks. A22 — welcome page tokens.css adoption (CONDITIONAL on Plan 01-10). Skip-gate on missing welcome.html: catches both HTTP 404 AND network-layer fetch failure (Chrome extensions throw TypeError 'Failed to fetch' for non-web_accessible_resources paths). On reachable: extracts <link rel=stylesheet> hrefs, fetches each, asserts >= 3 var(--mks-*) usages OR tokens.css reference. 1 check. Companion changes: - tests/uat/extension-page-harness.html gains `<link rel="stylesheet" href="../../src/shared/tokens.css">` so A18 + A21 have the @font-face rules + .mks-display-1 class + CSS custom properties resolvable via document.styleSheets + getComputedStyle. Vite's crxjs plugin handles the asset path rebasing at build:test time. - tests/uat/lib/harness-page-driver.ts: driveA18..driveA22 wrappers following the established driveA8 pattern (page.evaluate → window. __mokoshHarness.assertXX). No new host-side fs/ffprobe primitives; all A18-A22 work is page-side. - tests/uat/harness.test.ts: orchestrator drivers list extended with A18-A22 between A14 and A23. FORBIDDEN_HOOK_STRINGS UNCHANGED at 12 entries post-Plan-01-14 (A18-A22 use production chrome.* + fetch + getComputedStyle exclusively; no new test-mode symbols). Verification (this commit): - npm run test:uat: 21/21 GREEN (was 16/16 post-01-14) - SKIP_BUILD=1 npm test: 147/147 GREEN - Tier-1 grep gate: 13/13 GREEN (no FORBIDDEN_HOOK_STRINGS growth) - npx tsc --noEmit: clean - npm run build + npm run build:test: both succeed The chain of A1..A14 + A18..A22 + A23 runs in ~95 seconds end-to-end under Puppeteer headless mode against the bundled Chrome at ~/.cache/puppeteer/chrome/linux-148.0.7778.167. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| e8d2881874 |
feat(01-12): wave-5 task-1 — welcome i18n migration (conditional on 01-10) + __VITE_DEV__ define + scripts/README.md
Plan 01-10 (welcome tab) has NOT yet landed at execute-plan time
(verified: ls src/welcome/welcome.html returns absent). Per Wave 5
branch 2B, src/welcome/* file modifications are DEFERRED — when Plan
01-10 lands, its executor will use src/shared/tokens.css directly
(skipping the placeholder welcome-tokens.css step entirely; the
canonical tokens.css is already import-ready from src/shared/).
Unconditional changes in this wave:
1. vite.config.ts gains __VITE_DEV__ define-token (RESEARCH §12 +
D-09 spirit-satisfaction). Defaults to false; activates iff env
var VITE_DEV=1 is set. Reserved for any future inline smoke-mode
check. Currently smoke.sh lives entirely outside Vite's input set
so the gate is a defensive no-op:
define: { __MOKOSH_UAT__: 'false', __VITE_DEV__: JSON.stringify(...) }
2. vite.test.config.ts inherits __VITE_DEV__ via mergeConfig (the
test config only overrides __MOKOSH_UAT__: 'true'; __VITE_DEV__
from base flows through untouched).
3. scripts/README.md (NEW, ~50 lines): documents the smoke-isolation
invariant — dev-only scripts in scripts/ are NOT bundled by
`npm run build`; the production dist/ contains zero smoke
artifacts (verified by RESEARCH §12 grep gate). Provides usage
example for VITE_DEV env override + cross-references RESEARCH §12
and brand-decisions-v1.md D-09. Index lists subset-fonts.sh,
rasterize-icons.sh, and smoke.sh (if present).
Note on Plan 01-10 deferral: when Plan 01-10 executes after this
plan closes, the welcome page src/welcome/welcome.css can either
@import '../shared/tokens.css' directly OR a thin welcome-tokens.css
re-export — both paths are supported by the canonical tokens.css
landed in Wave 1. Plan 01-10's executor must adopt chrome.i18n.getMessage
for any welcome copy strings using the 16-key matrix in _locales/
(welcomeHeroRu + welcomeHeroEn already defined; additional keys
added per Plan 01-10's own artifact list).
Verification:
- vitest baseline 147/147 GREEN (no change from Wave 4 close)
- npm run build clean (no warnings; __VITE_DEV__ propagates through
define static replacement)
- scripts/README.md exists with the smoke-isolation paragraph
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 468f16d7e7 |
feat(01-12): wave-4 task-1 — adopt tokens.css + chrome.i18n.getMessage in src/popup/ + src/background/ (loom palette + RU i18n + en fallback)
src/popup/style.css:
- Adds @import "../shared/tokens.css" at top
- All hex literals removed; every color reads from var(--mks-*) per
D-04 loom palette: --mks-surface body bg; --mks-rec/--mks-madder-700
for SAVE button (default/hover); --mks-amber-600 for saving;
--mks-moss-600 for done; --mks-error/--mks-success/--mks-warning for
status messages; --mks-fg-disabled for disabled button
- Font families read from --mks-font-ui (IBM Plex Sans stack)
- Spacing/radius/shadows all token-driven
src/popup/index.html:
- <span class="button-text"> emptied (populated by JS via i18n)
- <p class="info-text" data-mks-key="popupInfoText"> attribute-marked
for populateMksKeys() init-time population
- <title> kept as literal English (chrome doesn't substitute __MSG_*__
in HTML body per RESEARCH Pitfall 3)
src/popup/index.ts:
- New `i18n(key, fallback)` helper: chrome.i18n.getMessage with explicit
`|| <fallback>` for unit-test contexts without chrome.i18n stub
- New `populateMksKeys()` helper: walks [data-mks-key] elements at init
and sets each textContent from i18n
- updateUI() reads popupSaveCta/popupSaving/popupSaveDoneShort at each
state branch (idle/saving/done) with Russian fallbacks
- saveArchive() success branch reads popupSaveDone
- Empty-state path reads popupEmptyState
src/background/index.ts:
- BADGE_REC_COLOR: '#00C853' → '#b2543d' (= --mks-madder-600 per D-04;
RESEARCH §10 Open Question A7 default-action)
- BADGE_OFF_COLOR + BADGE_ERROR_COLOR retained as engineering choices
(no loom-palette token for material-red/amber-700 equivalents)
- BADGE_REC_TITLE/BADGE_OFF_TITLE/BADGE_ERROR_TITLE renamed to
..._FALLBACK and only referenced at the chrome.i18n.getMessage call
sites inside setBadgeState (i18nMessage('tooltipRecPrefix' etc.))
- New `i18nMessage(key, fallback)` helper mirroring popup's i18n()
- Recovery notification: title=i18nMessage('extName',...); message=
i18nMessage('notifRecovery',...)
- Startup notification: title=i18nMessage('extName',...); message=
i18nMessage('notifStartup',...)
- NOTIF_EXTNAME_FALLBACK/NOTIF_STARTUP_FALLBACK/NOTIF_RECOVERY_FALLBACK
module-level constants for the |||| chain (degrade gracefully in
test contexts without chrome.i18n stub)
- NO `await import(...)` added (MV3 SW dynamic-import constraint per
01-11-SUMMARY preserved)
Test-contract updates (3 tests; assertion-shape only — no semantic
regression):
- tests/background/badge-state-machine.test.ts: greenCalls→recColorCalls
regex updated from /^#00[Cc]853$/ to /^#b2543d$/i lockstep with
BADGE_REC_COLOR change; title-substring assertion widened to
/Recording|recording/i to cover both EN locale + fallback
- tests/background/onstartup-notification.test.ts: title equality
('Mokosh ready') replaced with /Mokosh/i substring assertion
(survives both the 'Mokosh' fallback + 'Mokosh — Session Capture'
resolved EN); message regex widened to /recording|recor|click/i
- tests/background/toolbar-action.test.ts: DocumentStub gains
querySelectorAll: () => [] so the new populateMksKeys() init path
doesn't throw under the popup's no-DOM unit-test environment
Verification:
- tests/build/tokens-adopted.test.ts: 4/4 GREEN (was 2 RED + 2 GREEN)
- tests/build/no-remote-fonts.test.ts: 4/4 GREEN after fresh build
(Vite emits the WOFF2 files as content-hashed dist/assets/*.woff2;
tokens.css references resolve through the asset pipeline; no
remote-font URLs anywhere in dist/)
- Full vitest sweep: 147/147 GREEN (was 145/147)
- npx tsc --noEmit: clean
- Tier-1 grep gate: 13/13 GREEN (no new test-mode symbols)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 110cebc50d |
feat(01-12): wave-3 task-1 — manifest i18n (__MSG_*__ + default_locale='en') + _locales/{en,ru}/messages.json (16 keys; D-07 + D-08 baked in)
manifest.json migrated to chrome i18n placeholders: - name: 'AI Call Recorder' → '__MSG_extName__' - description: 'Запись сессий операторов для диагностики ошибок' → '__MSG_extDesc__' - default_locale: 'en' (new field per RESEARCH §11 + Pitfall 4 fallback chain) - action.default_title: '__MSG_tooltipOff__' (new field per Brief §02 string #1) _locales/en/messages.json + _locales/ru/messages.json each carry the same 16-key matrix per RESEARCH §10 + Brief §02 verbatim + D-07 user override + D-08 tagline: extName, extDesc, tooltipOff, tooltipRecPrefix, tooltipErr, popupSavePrompt, popupSaveCta, popupSaveDone, popupSaving, popupSaveDoneShort, popupEmptyState, popupInfoText, notifStartup, notifRecovery, welcomeHeroRu, welcomeHeroEn Canonical values (per brand-decisions-v1.md + RESEARCH §10 Brief §02): - EN extName = 'Mokosh — Session Capture' (D-07 user override of A) - EN extDesc = 'Thirty seconds ago, always at hand.' (D-08 tagline) - RU extName = 'Mokosh — Запись сессии' - RU extDesc = 'Тридцать секунд назад, всегда под рукой.' NB on key count: the artifact-table 12-key baseline excluded the three Wave-4 deltas (popupSaving, popupSaveDoneShort, popupEmptyState) which are introduced now to avoid a re-locale-parity flap when Wave 4 lands. popupInfoText is present in BOTH locales (the plan-checker flag 1 informational scope-slip is corrected here — see deviation block below). Plan success-criteria reading: ≥12 keys with Wave 4 additive deltas accounted for. Each key carries both `message` and `description` per Chrome i18n schema. EN descriptions are translator-facing; RU descriptions are plain-Russian context for native operators. npm run build emits: - dist/manifest.json carries the i18n shape verbatim (crxjs preserves __MSG_* placeholders; Chrome's manifest loader resolves them at install) - dist/_locales/en/messages.json (3.30 KB) - dist/_locales/ru/messages.json (3.81 KB) Verification: - tests/i18n/manifest-i18n.test.ts: 10/10 GREEN - tests/i18n/locale-parity.test.ts: 4/4 GREEN (en↔ru parity, non-empty messages, 16 keys each) - tests/build/no-remote-fonts.test.ts: 4/4 GREEN (post-build dist/ has zero remote font URLs) - Full vitest sweep: 145/147 GREEN (2 RED remaining are popup/style.css tokens-adopted cases — Wave 4 work) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 7732a302cd |
feat(01-12): wave-2 task-1 — rasterize Loom mark to icons/icon{16,48,128}.png (overwrites Bug A placeholders)
scripts/rasterize-icons.sh (80 lines) is the one-off rasterization recipe. It reads src/shared/brand/mokosh-mark.svg and emits the three toolbar icon sizes via rsvg-convert, with assets-spec.md size FLOOR sanity checks embedded. Before: icons/icon16.png 574 B 16-bit/color RGB (Bug A placeholder) icons/icon48.png 1153 B 16-bit/color RGB (Bug A placeholder) icons/icon128.png 2615 B 16-bit/color RGB (Bug A placeholder) After: icons/icon16.png 406 B 8-bit/color RGBA (Loom mark — D-01) icons/icon48.png 784 B 8-bit/color RGBA (Loom mark — D-01) icons/icon128.png 1952 B 8-bit/color RGBA (Loom mark — D-01) All three clear assets-spec.md Chrome imageUtil silent-rejection floors (16≥200B, 48≥500B, 128≥1024B). Sizes match RESEARCH §3 verification. Per D-06 (Neutral mark + dynamic badge): single neutral mark per size; no per-state PNG sets. The dynamic chrome.action.setBadgeBackgroundColor in src/background/index.ts does state communication via colored badge. Per RESEARCH §3 anti-pattern: PNGs are COMMITTED as static artifacts; not regenerated at build time. scripts/rasterize-icons.sh is the documented re-run recipe (for when src/shared/brand/mokosh-mark.svg changes). Verification: - tests/build/icons-present.test.ts: 15/15 GREEN (existence, FLOOR, PNG signature, dimensions, color-type byte === 6 RGBA) - Bug A regression check: file content differs from prior placeholders (verified via git diff binary status); the placeholder fingerprint used by harness A19 (Wave 6) will distinguish on first 32 bytes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| abab6e1f59 |
feat(01-12): wave-1 task-2 — canonical src/shared/tokens.css (R2 Lora substitution + .mks-word + local @font-face block)
src/shared/tokens.css lands as the canonical token system — engineering
working copy of .planning/intel/design-incoming/system/bundle/mokosh-handoff/
tokens.css with three surgical edits per Plan 01-12:
1. Handoff's PREVIEW-ONLY Google Fonts @import (line 12) REMOVED +
replaced with 8 local @font-face rules pointing at ./fonts/*.woff2
(Lora normal + italic variable; Plex Sans Regular/Medium/SemiBold/Bold;
Plex Mono Regular/Medium). MV3 CSP self-host enforced
(style-src 'self' + font-src 'self').
2. --mks-font-display VALUE substituted from "Newsreader" to "Lora" per
R2 designer reply 2026-05-19 (Cyrillic coverage; brand-decisions-v1-
followup-display-font.md). The Lora foundry note is preserved in the
Type-section comment. ZERO Newsreader references remain anywhere in
the file (verified by grep).
3. .mks-word class added at end-of-file with the {font-family,font-size,
font-weight,letter-spacing,fill} declarations from the lockup SVG's
class="mks-word" usage. Required by mokosh-lockup.svg line 21 per
RESEARCH §8.
src/shared/brand/ engineering working copies of:
- mokosh-mark.svg (Loom 2×2 weave intersection at 32×32 viewBox)
- mokosh-lockup.svg (mark + Mokosh wordmark at 240×56 viewBox)
The intel/ design-incoming/ copies remain unchanged as the original
handoff source-of-truth.
Verification:
- googleapis count: 0 (MV3 CSP self-host invariant)
- Newsreader count: 0 (R2 substitution complete)
- @font-face count: 8 (Lora normal + italic + Plex Sans ×4 + Plex Mono ×2)
- .mks-word: 1 definition (referenced by mokosh-lockup.svg)
- Lora references: 13 (font-family stack + @font-face + comments)
- tests/build/fonts-present.test.ts: 10/10 GREEN
- tests/build/tokens-adopted.test.ts case (a): GREEN (tokens.css exists)
- tests/build/tokens-adopted.test.ts cases (b)(c): still RED (Wave 4 work)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| f86fd60d4a |
feat(01-12): wave-1 task-1 — self-host OFL font bundle (Lora + Plex Sans + Plex Mono; R2 designer reply 2026-05-19)
Self-hosted WOFF2 bundle lands at src/shared/fonts/ per D-05 typography
pairing with R2 Newsreader→Lora substitution (designer reply 2026-05-19).
Bundle composition (8 WOFF2 files; ~236 KB total):
- Lora-VariableFont.woff2 (49 KB) — display family, normal style, wght
axis 400-700; Cyreal foundry (cyrealtype/Lora-Cyrillic main branch)
- Lora-Italic-VariableFont.woff2 (53 KB) — display family italic, wght
400-700; separate variable file per upstream layout (A5 verified at
execute time: Lora-Cyrillic ships italic as its own variable file).
- IBMPlexSans-{Regular,Medium,SemiBold,Bold}.woff2 (24/25/25/23 KB) —
UI body family with Latin + Cyrillic basic
- IBMPlexMono-{Regular,Medium}.woff2 (15 KB each) — diagnostic / timer
family
Companion artifacts:
- LICENSE-Lora.txt — verbatim OFL.txt + Lora Project Authors copyright
- LICENSE-IBM-Plex.txt — verbatim LICENSE.txt + IBM Corp. copyright
- README.md — substantive (160 lines): bundle table, R2 rationale,
subset coverage (Cyrillic basic + supplements + №), regeneration recipe
with one-off curl commands, MV3 CSP self-host rationale.
scripts/subset-fonts.sh (130 lines):
- One-off subsetting recipe; takes a scratch dir of upstream TTFs.
- UNICODES range: U+0020-007E + U+00A0-00FF + Cyrillic basic
(U+0400-045F) + Ukrainian (Ґґ) + Kazakh (Ұұ) + № sign.
- Common pyftsubset flags shared across faces; per-face subset_face
helper. Documents source URLs in usage block.
Bundle is sufficient for the 12 i18n keys (Wave 3) + welcome hero
(Plan 01-10 conditional) per the Brief §02 Russian copy specified in
.planning/intel/brand-decisions-v1.md.
Verification: tests/build/fonts-present.test.ts is 9/10 GREEN (1 RED
remaining is the tokens.css existence check, which is Wave 1 Task 2's
job). Existing 100/100 vitest baseline preserved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 34a9ce10d4 |
test(01-12): wave-0 — scaffold RED unit tests (tokens / fonts / icons / no-remote-fonts / manifest-i18n / locale-parity)
Wave 0 of the design-integration plan. Six new test files at tests/build/ and tests/i18n/ pin the contracts that later waves will GREEN: - tokens-adopted.test.ts (4 cases): src/shared/tokens.css exists + parses; src/popup/style.css @imports it; popup/style.css has zero hex literals; welcome.css conditional check. - fonts-present.test.ts: 7 required WOFF2 faces (Lora normal + Plex Sans ×4 + Plex Mono ×2) + LICENSE-Lora + LICENSE-IBM-Plex + README + optional Lora-Italic (A5 verify-at-execute). - icons-present.test.ts (15 cases across 3 sizes): existence, size FLOOR per assets-spec.md, PNG signature, dimensions, color-type byte === 6 (RGBA — RED until Wave 2 rsvg-convert overwrites the 16-bit-RGB placeholders). - no-remote-fonts.test.ts: production dist/ contains zero fonts.googleapis.com / https://fonts / googleapis substrings (MV3 CSP self-host invariant T-01-12-01). - manifest-i18n.test.ts (10 cases): manifest:name === '__MSG_extName__', :description === '__MSG_extDesc__', :default_locale === 'en', :action.default_title === '__MSG_tooltipOff__'; _locales/{en,ru}/ messages.json carry D-07 + D-08 canonical strings. - locale-parity.test.ts (4 cases): ru→en parity, en→ru symmetric, non-empty .message strings (RESEARCH Pitfall 4 mitigation). Current polarity: 29 RED + 18 GREEN across the 6 new files (placeholders already clear dim+size floors; no-remote-fonts vacuous-GREEN since tokens.css doesn't yet exist with remote URLs). Existing 100/100 vitest baseline preserved (verified SKIP_BUILD=1 npx vitest run). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 3fe018beb9 |
fix(01-12): revise plan baselines per Plan 01-14 landing (vitest 98→100, UAT 15→16, FORBIDDEN_HOOK_STRINGS 10→12)
Surgical amendment — Plan 01-12 is unexecuted; safe for in-place revision. Plan 01-14 landed ( |
|||
| 9792c0f6c3 |
docs(01-14): state + roadmap + requirements — Plan 01-14 closure
- STATE.md: advance plan counter, update progress (12/14 = 86%), record metric (Plan 01-14: 49m, 1 task, 7 files), add decision, record session. - ROADMAP.md: update phase-01 progress table (plan_count=14, summary_count=12). - REQUIREMENTS.md: mark REQ-video-ring-buffer complete (final closure for the Phase-01 video-pipeline charter). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 52541452e0 |
docs(01-14): summary — picker enhancement landed (16/16 UAT GREEN, 100/100 vitest GREEN)
Canonical closure artifact for Plan 01-14: - monitorTypeSurfaces:'include' shipped (W3C spec §6.1; Chrome >= 119 picker narrowing) - A23 harness regression gate wired into the UAT orchestrator (post-A14 chain) - Tier-1 grep gate inventory extended in lockstep (12 forbidden strings; was 10) Single atomic feat commit ( |
|||
| b467123578 |
feat(01-14): monitorTypeSurfaces:'include' — narrow picker to monitor surfaces only
[per Plan 01-14; closes B-01-14-01 via Step 1b lockstep]
- src/offscreen/recorder.ts: add monitorTypeSurfaces:'include' as top-level
DisplayMediaStreamOptions sibling of video: (W3C Screen Capture spec §6.1;
Chrome >= 119; removes tab/window panes from the operator's picker per
Plan 01-10 RESEARCH §5 + §Pitfall-5 recommendation). Typed widening cast
extended in lockstep to keep the explicit-typing contract (no `as any`).
D-15 post-grant validation block at recorder.ts:294 UNCHANGED — belt
(picker narrowing) + suspenders (post-grant tear-down) chain preserved.
- tests/offscreen/display-surface-constraint.test.ts: lockstep update of
the strict-deep-equality assertion at lines 223-226 with the same key
ordering as the source change (video -> monitorTypeSurfaces -> audio).
toHaveBeenCalledWith contract preserved (NO expect.objectContaining —
the test author's "catches future drops of ANY field" discipline is
honored). This edit + the source change land in the SAME commit so the
98/98 baseline never crosses a commit boundary in RED state.
- src/test-hooks/offscreen-hooks.ts: capture last constraints object in
module-scoped `lastGetDisplayMediaConstraints` cell (was `_constraints`
received-but-unused; renamed to `constraints`); add `get-last-getDisplayMedia-constraints`
bridge op to the __mokoshOffscreenQuery dispatcher between
get-display-surface and get-segment-count. Defensive try/catch mirrors
the existing dispatcher pattern; the cell is module-internal so the
MokoshTestSurface cross-cast in types.ts requires NO change (decision
documented inline in offscreen-hooks.ts).
- tests/uat/extension-page-harness.ts: add `assertA23` mirroring `assertA3`
(bridge query → 2-check AssertionResult: non-null constraints + value).
Extend the `Window.__mokoshHarness` declaration + runtime export + status
bar text + console.log to reference A23.
- tests/uat/lib/harness-page-driver.ts: export `driveA23(page)` mirroring
the `driveA14` page.evaluate wrapper shape. Standard read-only driver.
- tests/uat/harness.test.ts: extend FORBIDDEN_HOOK_STRINGS (line 85) with
`lastGetDisplayMediaConstraints` and `get-last-getDisplayMedia-constraints`.
Import driveA23. Append `{ name: 'A23', drive: driveA23 }` to the drivers
array after the A14 entry. Update header comment + orchestrator stdout
to reflect A14 + A23 chain. The `Total = drivers.length + 1` arithmetic
adapts automatically: 14 + 1 = 15 → 15 + 1 = 16.
- tests/background/no-test-hooks-in-prod-bundle.test.ts: lockstep
extension of FORBIDDEN_HOOK_STRINGS (line 105) with the same 2 strings.
Header comment updated to "Total: 12 surface strings." (was 10).
Confirms production `dist/` has ZERO occurrences after `npm run build`
via the `__MOKOSH_UAT__` dead-branch tree-shake (T-01-14-04 mitigation).
D-01 (whole-desktop only via getDisplayMedia; reject window/tab surfaces) is
the design intent that monitorTypeSurfaces:'include' realizes at the picker-
UI level. D-15 post-grant validation (recorder.ts:294-307) remains the
actual enforcement against managed-policy/DevTools/older-Chrome overrides.
Verification chain (per Plan 01-14 §verify; clean post-commit):
- `npx tsc --noEmit` exit 0
- `npm run build` exit 0; dist/ produced, monitorTypeSurfaces ships in
the offscreen chunk as the operator-facing picker hint
- `npm run build:test` exit 0; dist-test/ produced with the harness
hooks intact (gated)
- `npm test` 100/100 GREEN (was 98/98; +2 via the 2 new FORBIDDEN_HOOK_STRINGS
parametrized tests — both PASS, production bundle hook-free)
- `npm run test:uat` 16/16 GREEN (15 → 16 via A23). A23 reads constraints
`{video: {...}, monitorTypeSurfaces: 'include', audio: false}` from the
fakeGetDisplayMedia capture cell — round-trips through the full call site.
- Production bundle spot-check:
`grep -rc 'lastGetDisplayMediaConstraints\|get-last-getDisplayMedia-constraints' dist/ | grep -v ':0$'`
→ empty (all `:0` filtered) → ZERO leakage.
References:
- W3C Screen Capture §6.1 DisplayMediaStreamOptions:
https://www.w3.org/TR/screen-capture/#dom-displaymediastreamoptions-monitortypesurfaces
- Chrome screen-sharing-controls (Chrome 119+):
https://developer.chrome.com/docs/web-platform/screen-sharing-controls
- Plan 01-10 RESEARCH §5 + §Pitfall-5 (recommendation provenance):
.planning/phases/01-stabilize-video-pipeline/01-10-RESEARCH.md
- Architectural-note (replaces retired AMENDMENT-A.md improvisation per
01-11-SUMMARY): canonical GSD ceremony — plan → checker (B-01-14-01)
→ executor → SUMMARY (this commit).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 433ee280f3 |
fix(01-14): revise plan per checker — include test-expectation update (B-01-14-01)
Plan-checker BLOCKER B-01-14-01: original plan's must_haves truth #5 understated baseline regression risk. Adding `monitorTypeSurfaces: 'include'` as a sibling constraint in src/offscreen/recorder.ts would have dropped vitest from 98/98 GREEN to 97/98 RED because tests/offscreen/display-surface-constraint.test.ts Test 1 (line 223-226) uses strict deep-equality (toHaveBeenCalledWith, NOT expect.objectContaining) on the constraints object — the test author's intent (comment at line 221-222) is to catch future drops of ANY field. Surgical revision per references/planner-revision.md (surgeon-not-architect): - Frontmatter: add tests/offscreen/display-surface-constraint.test.ts to files_modified list. - must_haves truth #5: replace the "no existing unit test references the constraints object" claim with a positive statement that the strict-deep- equality assertion at lines 223-226 is updated in lockstep; preserves the test author's "no objectContaining" discipline; explicit no-transient-RED guarantee across commit boundaries. - must_haves artifacts: new entry for the test file documenting the in-place edit shape and the preserved test author comment. - must_haves key_links: new link entry pairing the test assertion with the source call site under the lockstep contract. - Interfaces block: add the explicit "test-expectation lockstep update" code fragment with the chosen key ordering (video → monitorTypeSurfaces → audio) so the executor lands the source change and the test update with matching shapes. - Task 1 <files>: add tests/offscreen/display-surface-constraint.test.ts. - Task 1 <action>: insert new Step 1b between Step 1 (source change) and Step 2 (offscreen-hooks bridge) — full single-line edit spec at lines 223-226, preserve toHaveBeenCalledWith contract, preserve comment block, same-commit guarantee. - Task 1 verify-block expected outputs: explicitly call out that 98/98 GREEN is preserved BECAUSE Step 1b lands (without it, 97/98 RED on the strict- deep-equality assertion). - Task 1 <done>: add line covering the lockstep test update + the no-transient- RED guarantee. - <verification> phase gate: add new check #2 (test-expectation lockstep) between source-line correctness and A23 round-trip. - <success_criteria>: add bullet for the lockstep test-expectation update. - <output> SUMMARY contract: add "Revision linkage" bullet documenting that the plan was revised once after the plan-checker flagged B-01-14-01. Untouched (per checker's preserve-verbatim list): - Source-line target (src/offscreen/recorder.ts:270) - Harness wiring references (assertA3 686, driveA14 987, __mokoshHarness 1922+1942, drivers array 289-312, Total comment 354) - FORBIDDEN_HOOK_STRINGS lockstep contract (both inventories) - `_constraints` capture path - Scope discipline (still 1 task, autonomous, no checkpoint) - Research traceability (Plan 01-10 RESEARCH §5 + §Pitfall-5 + W3C §6.1) - Threat model (T-01-14-04 mirrors Plan 01-13) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 41c1f7e82f |
feat(01-14): plan — monitorTypeSurfaces picker enhancement (canonical post-closure scope)
Plan 01-14 ships W3C Screen Capture monitorTypeSurfaces: 'include' (Chrome
119+) on the offscreen getDisplayMedia call, plus an A23 harness regression
assertion that verifies the constraint reaches the call site via the
existing offscreen-hooks bridge.
Scope: 1 source line + A23 wiring + Tier-1 grep gate inventory update
(lockstep extension of unit-gate + UAT A0 FORBIDDEN_HOOK_STRINGS).
Autonomous, single executor; no operator empirical checkpoint (UAT 16/16
harness coverage suffices per feedback-pre-checkpoint-bundle-gates.md).
Canonical sources:
- Plan 01-10 RESEARCH section 5 ('monitorTypeSurfaces: include' recommendation)
- Plan 01-10 RESEARCH section Pitfall-5 ('Misinterpreting displaySurface
as a hard constraint' — monitorTypeSurfaces is the picker-UI complement
to D-15's post-grant validation)
- W3C Screen Capture spec section 6.1 DisplayMediaStreamOptions
- developer.chrome.com/docs/web-platform/screen-sharing-controls
Decisions honored:
- D-01 (whole-desktop only via getDisplayMedia; reject window/tab) — the
new constraint is the picker-UI realization of D-01's intent.
- D-15 (post-grant displaySurface validation) — UNCHANGED; remains the
enforcement (this plan is belt-and-suspenders at the picker UI level).
Ceremony note: this plan replaces the prior AMENDMENT-A.md improvisation
path retired per 01-11-SUMMARY Architectural Notes. Canonical GSD ceremony
(plan -> checker -> executor -> SUMMARY).
Validations:
- gsd-sdk frontmatter.validate -> valid: true (8/8 required fields).
- gsd-sdk verify.plan-structure -> valid: true (1 task; hasFiles/hasAction
/hasVerify/hasDone all true).
- ROADMAP.md Phase 1 plans list extended with 01-14 entry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 4d828f1080 |
docs(01-10): install-flow + auto-select research — both asks INFEASIBLE
Researcher (gsd-phase-researcher) returned HIGH-confidence verdicts on the
12-area brief from .continue-here.md:
Ask 1 (install-time auto-start): INFEASIBLE in unmanaged Chrome.
W3C Screen Capture spec §5.1 mandates transient user activation;
chrome.runtime.onInstalled confers none. Floor: 2 clicks
(toolbar/welcome-page → Share button on picker). Enterprise policy
ScreenCaptureWithoutGestureAllowedForOrigins exists (Chrome+Edge ≥ 123)
but only applies to managed-Chrome contexts with extension URL
whitelisted — does NOT apply to Load-Unpacked deployment; deferred-idea.
Ask 2 (auto-select desktop / skip picker): INFEASIBLE in unmanaged Chrome.
W3C spec mandates user MUST choose every time. displaySurface:'monitor'
is a hint (already applied src/offscreen/recorder.ts:270).
chooseDesktopMedia doesn't auto-accept on single-monitor setups AND
streamId not usable in MV3 offscreen documents (Chrome DevRel position).
Primary recommendation: KEEP Plan 01-10's current informational CTA
charter (commit
|
|||
| 6a29ae4124 |
chore(01): resume work — consume HANDOFF.json + ignore dist-archives
- Delete .planning/HANDOFF.json (one-shot artifact per resume-project workflow) - Add dist-archives/ to .gitignore (from prior session's distribution-zip build) - Bump STATE.md Session Continuity to reflect resumed session + next action (install-flow + auto-select researcher spawn) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| cecefc61f9 |
wip: phase-01 paused — .continue-here.md handoff (pairs with c60b887 HANDOFF.json)
Human-readable handoff. Captures: - 3 BLOCKING CONSTRAINTS from saved memory (scope reduction, GSD ceremony, pre-checkpoint gates) - 3 anti-patterns from this session (improvised artifact types, claiming canonical without verifying, save-stops UX cycle) - Current state + working tree + test/build baseline - Next-session order of operations (10 steps) - Required reading order - Pending researcher brief (12 areas; was 529-blocked) - Infrastructure state + API capacity note - Followup backlog Resume: /clear → /gsd-resume-work Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| c60b8878df |
wip: phase-01 paused — Plan 01-13 closed; 01-10 + 01-12 plans ready; researcher pending
Session arc: - Plan 01-13 UAT harness: closed at |
|||
| e035fd279d |
docs(01-09): Amendment 3 + 01-13 SUMMARY reversal note + STATE.md sync + debug records
Plan 01-09 Amendment 3 (2026-05-19) — atomic documentation pass for the save-does-not-stop-recording charter reversal. Changes: - .planning/phases/01-stabilize-video-pipeline/01-09-PLAN.md: Amendment 3 block added above <success_criteria> (mirrors Amendment 2 placement). Describes the reversed charter, references the new debug record, points at the inverted test file + harness A14. - .planning/phases/01-stabilize-video-pipeline/01-13-SUMMARY.md: "Subsequent Reversal (2026-05-19)" footer added. Notes that npm run test:uat still 15/15 GREEN under the inverted A14 contract; vitest baseline preserved at 98 GREEN. - .planning/STATE.md: Plan 01-13 closure block extended with CHARTER REVERSAL bullet citing the 4 commit SHAs ( |
|||
| 1baaf45702 |
feat(01-13-A14-invert): A14 — invert to assert continuous-recording post-SAVE
Plan 01-09 Amendment 3 (2026-05-19) end-to-end lock. Inverted A14 to
match the reversed charter (SAVE creates zip, recording continues).
Page-side (tests/uat/extension-page-harness.ts):
- assertA14: assert badge==='REC' (was ''), popup endsWith
'src/popup/index.html' (was ''), no-new-recovery-notif (unchanged).
- A14 name + check labels updated to reflect continuous-recording semantic.
- New constant A14_POPUP_HTML_SUFFIX for the popup endsWith check
(ext-id-agnostic via suffix match).
- A13 docstring + diag strings refreshed: setupFreshRecording is now
defensive (orthogonal to A12 ordering) rather than a workaround for
the prior auto-stop. 11s settle preserved (same wall-clock cost).
Host-side (tests/uat/lib/harness-page-driver.ts):
- driveA14 docstring refreshed to mention Amendment 3 + the inverted
contract; mechanical wrapper unchanged.
Verification:
- npm run test:uat: 15/15 GREEN
- A14 actual output:
badge='REC'
popup='chrome-extension://<ext-id>/src/popup/index.html'
recoveryDelta=0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 7645765401 |
feat(01-09-no-stop): GREEN — remove SAVE_ARCHIVE finally block; recording continues
Plan 01-09 Amendment 3 (2026-05-19) — code surgery to drive tests/background/save-archive-does-not-stop-recording.test.ts from RED (commit |
|||
| 6ac23fdbd8 |
test(01-09-no-stop): RED — invert save-archive contract to lock always-on charter
Per operator UX iteration (2026-05-19), the Amendment 2 save-stops-recording
fix (commits cd83eb0+4f4c3e2+2b6c24b+89f3337) is REVERSED. SAVE_ARCHIVE
creates a new zip but does NOT stop the recorder — matches SPEC's
continuous-capture / always-on safety-net framing.
This commit renames the test file via `git mv` (history preserved) and
inverts tests A..C to assert the new contract:
- A: no NEW setBadgeText({text:''}) call (badge stays REC)
- B: no setPopup({popup:''}) call (popup stays pinned to popup.html)
- C: no STOP_RECORDING dispatch via chrome.runtime.sendMessage
Test D (no recovery notification) preserved unchanged as regression guard.
RED expected — src/background/index.ts still has the Amendment 2
`finally` block dispatching STOP_RECORDING + setIdleMode. Next commit
removes that block to drive GREEN.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 8d1c8fb0cc |
docs(01-12): create Plan 01-12 (Design Integration; R2 Lora unblocks; 7 waves)
Final designer reply received 2026-05-19 unblocks Plan 01-12: R2
substitution — replace Newsreader with Lora (OFL, Cyreal foundry, full
Cyrillic-Latin parity, variable wght 400-700). All 9 brand decisions
now resolved; R2 displaces Newsreader from `--mks-font-display`.
Plan structure: 7 waves, 10 tasks.
- Wave 0 (TDD scaffolds): 6 RED unit tests — tokens-adopted,
fonts-present, icons-present, no-remote-fonts, manifest-i18n,
locale-parity. Each RED until its corresponding artifact wave lands.
- Wave 1: Self-host OFL font bundle (Lora variable normal + italic,
Plex Sans ×4, Plex Mono ×2) at src/shared/fonts/ via pyftsubset
(Latin + Cyrillic basic subset); land src/shared/tokens.css canonical
(Google Fonts @import → 7 local @font-face rules; Newsreader → Lora
per R2; .mks-word class added per RESEARCH §8 + lockup SVG line 21).
- Wave 2: Rasterize Loom mark to icons/icon{16,48,128}.png via
rsvg-convert; overwrite Bug A placeholders; 8-bit RGBA at all sizes.
- Wave 3: Land _locales/{en,ru}/messages.json (12 keys: 8 Brief §02
operator strings + 4 supporting keys); manifest.json → __MSG_extName__
+ __MSG_extDesc__ + default_locale 'en' + action.default_title.
extName='Mokosh — Session Capture' per D-07 user override; extDesc per
D-08 brand-decisions-v1.md wording.
- Wave 4: src/popup/ + src/background/ adopt tokens.css (loom palette)
+ chrome.i18n.getMessage at every operator-facing copy site; replace
hex literals with var(--mks-*) references; BADGE_REC_COLOR madder
'#b2543d' (= --mks-madder-600 per D-04 + RESEARCH §10 Open Q A7).
- Wave 5: Welcome page conditional migration (if 01-10 landed, swap
welcome-tokens.css → @import canonical tokens.css; migrate copy.ts
shim to chrome.i18n.getMessage fallback); add __VITE_DEV__ define
per RESEARCH §12 D-09 spirit; scripts/README.md smoke-isolation note.
- Wave 6: UAT harness A18-A22 (font reachability via document.styleSheets
walk + fetch + byteLength; icon-not-placeholder via fingerprint diff;
manifest:name === 'Mokosh — Session Capture'; --mks-font-display
resolves to Lora via getComputedStyle; welcome tokens loaded
conditional on 01-10). Tier-1 forbidden-strings UNCHANGED at 10.
- Wave 7: Operator empirical brand-fit checkpoint (last Phase 1 gate);
SUMMARY + STATE.md + ROADMAP.md sync.
ROADMAP.md Phase 1 plan list extended from 7 → 13 entries (gap noted in
01-13 SUMMARY's known-limitations now closed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 3a530c2334 |
docs(01-10): rewrite plan in place — D-02/D-08/D-17-onboarding charter + design-swap-in-ready arch + harness A15+A16+A17
Drops the 2026-05-17 draft (zero commits, zero SUMMARY — virgin). Carries: - onInstalled flag-gated welcome tab (3 RED→GREEN unit tests; storage-key contract pinned per prior I-02 fix) - 5 new src/welcome/* files: welcome.html + welcome.ts + welcome.css + welcome-tokens.css + copy.ts - Design-swap-in-ready: every color via var(--mks-*); every string via COPY map; every font via var(--mks-font-*) with system fallback - vite.config.ts + vite.test.config.ts both gain welcome rollup input - manifest.json gains web_accessible_resources for welcome.html - Harness extended A15+A16+A17 (onboarding flag observability + no-re-open settle + design-swap invariant); Tier-1 forbidden-strings inventory unchanged at 10 - 4 autonomous tasks + 1 operator empirical checkpoint Deletes: REQUEST_PERMISSIONS flow (gone in 01-09); duplicate Start button (D-16-toolbar owns start path); start-path divergence Cites: D-17-onboarding (CONTEXT.md L537+), D-02 (welcome layout), D-08 (tagline), D-03 (voice register), D-16-toolbar (start ownership), brand-decisions-v1-followup-display-font.md (Plan 01-12 blocker; this plan ships TODAY with placeholders) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 285e46f620 |
docs(01-13): close — operator UAT ack 2026-05-19 + save-stops debug resolved + SUMMARY landed
Plan 01-13 fully closed. Operator UAT acked "all good" on 2026-05-19; recovery flow (A7) + restart-after-click (A2) both harness-covered, no manual verification needed. What this commit lands: - 01-13-SUMMARY.md (full spike-pivot-then-implementation narrative; tracks all 16 commits across Plan 01-11 spike + Plan 01-13 4-wave execution + save-stops debug session; documents 15/15 npm run test:uat GREEN + 98/98 vitest GREEN + Bug A/B regression-rewind demos verified) - Save-stops debug record moved to .planning/debug/resolved/ (closure- canonical location; prior inline path tracked via git mv) - STATE.md sync: completed_plans 11→12, percent 95→96, Plan 01-13 fully-closed narrative + save-stops debug session captured Phase 1 functional contract: CLOSED via harness PASS. Remaining Phase 1 gates: Plan 01-10 (welcome tab) + Plan 01-12 (design integration; pending designer Newsreader-Cyrillic reply). Phase 2 inherits the harness as its closure-gate template. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 89f3337334 |
docs(01-09-save-stops): debug record — RED → GREEN → A14 evidence + closure notes
Updates .planning/debug/01-09-save-stops-recording.md with: - status awaiting_human_verify - SHA references (red_commit |
|||
| 2b6c24b2d9 |
feat(01-13): A14 — post-SAVE state check (badge='', popup='', no new recovery notif)
Plan 01-13 Task 9 closure for operator empirical UAT bug
.planning/debug/01-09-save-stops-recording.md. Adds the harness
assertion that empirically verifies the SAVE-auto-stops-recording fix
(committed at
|
|||
| 4f4c3e2241 |
feat(01-09-save-stops): GREEN — SAVE_ARCHIVE auto-stops recording per SPEC one-shot intent
Operator UAT closure for Plan 01-13 Task 9. Patches saveArchive() in
src/background/index.ts with a `finally` block that dispatches
STOP_RECORDING to offscreen (mirrors the existing START_RECORDING
control-plane channel via chrome.runtime.sendMessage), flips
isRecording=false, and calls setIdleMode() — applied to BOTH the
success and empty-buffer-error paths.
Operator UX contract: SAVE click ALWAYS stops the session, regardless of
internal success/empty-buffer outcome. The badge clears, the popup
empties (re-enabling chrome.action.onClicked for restart), and Chrome's
sharing banner closes via the offscreen recorder's stopRecording()
(which nulls mediaStream + stops all tracks + clears the rotation
timer — line 527 of src/offscreen/recorder.ts, already wired since
Plan 01-05).
Trade-off documented inline: empty-buffer path still surfaces a
recovery notification (the catch branch emits RECORDING_ERROR{
error:'empty-video-buffer'} → SW's own onMessage handler runs
setErrorMode + creates a mokosh-recovery-* notif). The finally block
then setIdleMode()'s, so the FINAL visible state is OFF/empty-popup —
clean restart path. The notification stays visible briefly so the
operator sees that something went wrong, then clicks it to start a
new session.
Test count: 94 GREEN (baseline) → 98 GREEN (+4 from
tests/background/save-archive-stops-recording.test.ts).
Files modified:
- src/background/index.ts (saveArchive + finally block; no
PortMessage/Message type changes — STOP_RECORDING already in
MessageType per src/shared/types.ts:14, offscreen handler at
recorder.ts:848 already wired)
Toolchain:
- npx tsc --noEmit: exit 0 (no type errors)
- npm run build: exit 0 (dist/ clean rebuild)
Debug record: .planning/debug/01-09-save-stops-recording.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| cd83eb0498 |
test(01-09-save-stops): RED — SAVE_ARCHIVE triggers STOP_RECORDING + setIdleMode + no recovery notif
Plan 01-13 Task 9 operator UAT closure. Operator 2026-05-19 empirical
session: SAVE click downloaded zip but recording stayed live (badge=REC,
sharing banner persisted, subsequent toolbar press re-opened SAVE-only
popup). Operator pressed 4×, got 2 zips + confusion.
Root cause: src/background/index.ts saveArchive() returns success after
chrome.downloads.download without signaling offscreen to stop or
transitioning the SW state machine — SPEC `Тз расширение фаза1.md`
"one click MUST produce a self-contained archive" was over-extended to
"always-on" framing by the implementation.
Fix contract (RED today; GREEN after src/background/index.ts patch):
A: setBadgeText({text:''}) called post-save (setIdleMode side effect)
B: setPopup({popup:''}) called post-save (re-enables chrome.action.onClicked
restart path per MV3 contract)
C: chrome.runtime.sendMessage({type:'STOP_RECORDING'}) dispatched
(offscreen recorder.ts:848 STOP_RECORDING case already wired —
no offscreen-side change needed)
D: NO mokosh-recovery-* notification fires (deliberate stop ≠ error;
mirrors Bug B `user-stopped-sharing` suppression branch from
.planning/debug/resolved/01-09-recovery-flow.md)
Tests A/B/C RED (assertion errors `expected 0 >= 1`); Test D GREEN today
as the regression guard against fix over-rotating to setErrorMode.
Test architecture mirrors tests/background/request-id-protocol.test.ts:
synthetic BUFFER response delivered via port.onMessage listeners to drive
saveArchive's request-id'd buffer fetch to completion. Empty-segments
BUFFER causes createArchive → EmptyVideoBufferError → catch branch; the
fix's STOP+IDLE dispatch MUST happen on both success and empty-buffer
paths (operator UI contract: SAVE click = stop, success or empty alike).
Debug record: .planning/debug/01-09-save-stops-recording.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 9c5ff8b2a7 |
docs(01-13): wave-4 task-8 — Plan 01-09 closure-via-harness amendment + STATE.md sync
Lands the Wave 4 closure docs: - 01-09-PLAN.md Amendment 2: harness PASS (npm run test:uat 14/14 GREEN at |
|||
| d793c9e1e5 |
feat(01-13): wave-3D — A11+A12+A13 GREEN + get-segment-count bridge op; 14/14 GREEN
Lands the final three UAT-harness assertions. All 14 assertions (A0..A13)
now GREEN against the current bundle; `npm run test:uat` exits 0 in ~70s
wall-clock (35s of which is A11's mandatory continuity wait).
Assertions wired:
- A11 — 35s buffer continuity → segments.length >= 3. Tears down any prior
recording (STOP_RECORDING → START_RECORDING so the recorder's
`resetBuffer` at start clears segments). Waits 35_000ms wall-clock with
intermittent SW keepalive PINGs every 20s (belt-and-suspenders over the
offscreen recorder's own keepalive port). Queries the new
`get-segment-count` bridge op. Asserts count >= 3 (per D-13:
SEGMENT_DURATION_MS=10s × MAX_SEGMENTS=3).
- A12 — SAVE_ARCHIVE produces zip; webm passes ffprobe. Page side
dispatches SAVE_ARCHIVE (recording from A11 still alive). Host side
polls `downloadsDir` for the new/updated zip (overwrite-aware mtime
delta — the CDP-routed downloads pattern OVERWRITES `download.zip`
rather than numbering it, empirically verified during initial RED).
Extracts `video/last_30sec.webm` via JSZip to a tmpfile. Runs
`/usr/bin/ffprobe -v error -f matroska <path>`; asserts exit 0 + clean
stderr. Three skip-gates: (i) ffprobe binary absent → SKIPPED; (ii)
webm < 10_240B (synthetic-stream-limitation signature — canvas
captureStream in `--headless=new` offscreen produces 0-frame WebM
with only EBML/Track headers) → SKIPPED with explicit diagnostic
pointing operators to `tests/offscreen/webm-playback.test.ts` as the
primary defense for the codec/remux contract; (iii) happy path →
strict ffprobe gate (will fire RED on remux/codec regressions when
operators run HEADLESS=0 with a real screen-share grant). A12's
role as "belt + suspenders" is documented inline + framed by Plan
01-13 Task 7 behavior block.
- A13 — Zip structure + meta.json shape. Second SAVE_ARCHIVE (verifies
idempotency over A12's first save). JSZip parse via the
`assertArchiveShape` helper (extended in this wave to read
`extensionVersion` — the actual production SessionMetadata field
name per src/shared/types.ts:103, vs. the earlier 01-11 prototype's
incorrect `version` assumption). Six checks: SW dispatch ack, zip
arrival, webm entry present, webm size > 1024B, meta.json entry
present, meta.json.extensionVersion matches
chrome.runtime.getManifest().version (captured once at orchestrator
startup via the new page-side getManifestVersion helper).
Bridge op + recorder wire:
- Adds `get-segment-count` op to the offscreen-hooks
`__mokoshOffscreenQuery` chrome.runtime.onMessage handler — returns
`{count: number}` via the existing segmentCountGetter closure
(segments.length captured at recorder.ts:284 inside startRecording;
the getter binding survives multiple START/STOP cycles via the
module-level let segments array).
- Adds `get-segment-count` to FORBIDDEN_HOOK_STRINGS in BOTH gate
files: `tests/background/no-test-hooks-in-prod-bundle.test.ts`
(Tier-1 unit gate; 9 → 10 entries; vitest 93 → 94 GREEN) and
`tests/uat/harness.test.ts:assertA0_GrepGate` (UAT-level mirror).
Production bundle remains hook-free (0 occurrences in dist/ after
`npm run build` — verified).
Harness surface:
- `tests/uat/extension-page-harness.ts` extends `window.__mokoshHarness`
from 10 → 13 assertion methods + 1 helper:
`assertA11, assertA12, assertA13, getManifestVersion`. Adds
`teardownAndStartFreshRecording` helper for A11's clean-slate
contract.
- `tests/uat/lib/harness-page-driver.ts` retires the Wave-3 stub
marker (no more NYI throws). Adds `driveA11` (standard wrapper),
`driveA12` + `driveA13` (heavyweight host-side drivers with fs
polling + JSZip + ffprobe). Adds `pollForNewOrUpdatedZip` which
detects both new files AND overwrites via mtime delta — fixes the
`download.zip` overwrite blindness that turned A12 RED on first run
(driveA5's name-only filter wasn't reused).
- `tests/uat/lib/zip.ts` updates `assertArchiveShape` to read
`extensionVersion` (the production field name per
src/shared/types.ts:103); adds the A13_MIN_VIDEO_BYTES=1024 floor
constant.
- `tests/uat/harness.test.ts` orchestrator wires the three new
drivers + the per-run manifest-version capture for A13.
Baseline:
- `npx tsc --noEmit`: exit 0.
- `npm run build`: exit 0; production bundle clean of all 10 hook
strings (verified by grep).
- `npm run build:test`: exit 0; test bundle ships `get-segment-count`.
- `npx vitest run`: 94/94 GREEN (was 93; +1 from the new gate string).
- `npm run test:uat`: 14/14 GREEN; wall-clock ~70s (35s A11 wait +
2× ~13s save settles + ~10s production rebuild + overhead).
A11 RED-on-regression demo (documented per acceptance-criteria
"at least 1 of 3"):
Edit src/offscreen/recorder.ts:52: `SEGMENT_DURATION_MS = 10_000`
→ `SEGMENT_DURATION_MS = 30_000`. Rebuild dist-test. Re-run UAT.
A11 FAILS (only 1 segment rotates in 35s, vs floor of 3). Revert
the edit; A11 PASSES. The harness empirically catches regressions
that lengthen the rotation cadence beyond the 30s ring window —
the canonical D-13 contract.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| b665919c5f |
feat(01-13): wave-3C — A8+A9+A10 GREEN + Bug A canonical regression rewind
Plan 01-13 Task 6 (Wave 3C). Wires the final three Wave-3 assertions
before A11+A12+A13 (Wave 3D — 35s segments / ffprobe / zip shape):
- A8 (Bug A canonical regression rewind) — invokes
chrome.notifications.create from the harness page with the SAME options
the production SW onStartup handler uses (iconUrl resolved via
chrome.runtime.getURL('icons/icon128.png')). Exercises Chrome's
imageUtil icon validation — the exact code path Bug A regressed on
(
|
|||
| 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> |