commit 110b295a7bcac242338b3cb8546d7ed69523eed9 Author: LeRatierBretonnier Date: Thu May 21 23:08:34 2026 +0200 CLose story 1.2 diff --git a/_bmad-output/brainstorming/brainstorming-session-2026-05-19-221747.md b/_bmad-output/brainstorming/brainstorming-session-2026-05-19-221747.md new file mode 100644 index 0000000..5526de6 --- /dev/null +++ b/_bmad-output/brainstorming/brainstorming-session-2026-05-19-221747.md @@ -0,0 +1,300 @@ +--- +stepsCompleted: [1, 2, 3, 4] +session_active: false +workflow_completed: true +ideas_generated: [21] +inputDocuments: [] +session_topic: 'FoundryVTT v14 webcam view manager module' +session_goals: 'Granular webcam visibility control per player, individual enable/disable, efficient and practical UI, who-sees-who selection between connected players' +selected_approach: 'ai-recommended' +techniques_used: ['Question Storming', 'SCAMPER Method', 'Role Playing'] +ideas_generated: [] +context_file: '' +--- + +# Brainstorming Session Results + +**Facilitator:** Morr +**Date:** 2026-05-19 + +## Session Overview + +**Topic:** FoundryVTT v14 webcam view manager module +**Goals:** Granular webcam visibility control per player, individual enable/disable, efficient and practical UI, who-sees-who selection between connected players + +### Session Setup + +_Fresh session initialized. No context file provided._ + +## Technique Selection + +**Approach:** AI-Recommended Techniques +**Analysis Context:** FoundryVTT v14 webcam view manager module with focus on granular player-to-player visibility control and practical UI design + +**Recommended Techniques:** + +- **Question Storming:** Surface hidden requirements, edge cases, and unknowns before generating solutions — defines the correct problem space +- **SCAMPER Method:** Systematic 7-lens feature/UI idea generation (Substitute, Combine, Adapt, Modify, Put to other use, Eliminate, Reverse) +- **Role Playing:** Embody GM, players, streamers, mobile users to ground ideas in real human perspective + +**AI Rationale:** This sequence moves from problem definition → systematic generation → human validation. Ideal for a technical product with multiple user roles and complex interaction states. + +--- + +## Technique Execution Results + +### Phase 1: Question Storming + +**Interactive Focus:** Surfacing hidden requirements, edge cases, and architectural constraints before generating solutions. + +**Key Questions Generated:** +- Who has permission to change visibility? Only GM, or can players hide themselves? +- What happens when a player joins mid-session — visible by default or hidden? +- Should visibility be symmetric or asymmetric (GM sees all, players see what's allowed)? +- What's the "panic button" — how does GM instantly hide all cams? +- How does this interact with FoundryVTT's existing AV system (`game.webrtc`)? +- If a webcam disconnects, does its slot disappear or leave a placeholder? +- What if it disconnects mid-combat — does the layout reflow and disrupt players? +- Is a disconnected cam different from a hidden cam? +- What about reconnection — does it snap back to previous state or reset? +- What if the GM's own webcam disconnects? +- Should the UI communicate WHY a feed is absent? (Camera off vs Player disconnected vs GM hid this feed) +- Where does this UI live? Sidebar? Floating overlay? Popout? +- On a screen with 8 players, how do you display a visibility matrix without a spreadsheet nightmare? +- Should there be presets? ("Cinematic mode", "Social mode") +- What visual feedback tells a player their own feed is hidden from someone? +- Should players know they're being hidden, or is it silent? +- Does FoundryVTT's WebRTC even support per-viewer stream filtering, or must visibility be faked client-side? +- How do settings persist across sessions? +- What happens in combat vs exploration — should the module have scene-aware presets? + +**Breakthrough Insight — The Visibility Matrix:** +The module's data model isn't `Map` — it's `Map>`. Every player has a visibility setting *per observer*. This is the architectural heart of "who sees what." + +**Participant State Machine (8 states identified):** +- `active` — Camera on, feed visible to permitted viewers +- `hidden` — Camera on, but GM/module has hidden it +- `self-muted` — Player turned off their own cam voluntarily +- `offline` — Player's connection dropped entirely +- `cam-lost` — Player connected but camera device failed +- `reconnecting` — Transitional, feed expected to return +- `never-connected` — Player joined with no camera +- `ghost` — Secretly observing (GM special state) + +**3 Core Tensions Identified:** +1. Asymmetric visibility — the matrix is powerful but complex to display +2. Silent vs announced changes — trust and social design +3. Technical layer — is filtering real (WebRTC) or cosmetic (CSS hide)? + +--- + +### Phase 2: SCAMPER Method + +**Interactive Focus:** Systematic 7-lens feature and UI idea generation. + +#### S — Substitute +Explored substituting the toggle metaphor with spatial/contextual alternatives. Key insight: the **seating chart metaphor** maps naturally to how TTRPG players think about the table. + +#### C — Combine +Explored combining with combat tracker, scene transitions, character status. Key insight: **Combat Cinematics Mode** — the module acts as a live director during combat, auto-spotlighting the active combatant. + +**Decision captured:** Dedicated popout window for the seating chart UI — low-frequency tool, opened at session start, closed during play. + +#### A — Adapt +Adapted patterns from video conferencing (spotlight/pin), broadcast control rooms (switcher), and theater (lighting states). Key insight: **Stage Lighting States** — "Wash/Focus/Blackout" maps perfectly to TTRPG dramatic vocabulary. + +#### M — Modify +Scaled up (token-anchored floating cams) and distorted (HP-reactive cam styling). Key insight: **Token-Anchored Floating Cams** — player faces float above tokens on the canvas. + +#### P — Put to Other Uses +Discovered unexpected applications: NPC presence tiles, spectator curtain for streamers, murder mystery reveals. Key insight: **Spectator Curtain** opens the module to the actual-play streaming market. + +#### E — Eliminate +Stripped features to find the irreducible core. Key insight: **Progressive Enhancement Architecture** — Level 1 (right-click) → Level 2 (Director's Board) → Level 3 (popout). MVP might be just Level 1. + +#### R — Reverse +Flipped the control model. Key insight: **Pull Visibility Model** — nothing visible until someone actively requests to see it. Also: **Reaction Clip System** for players without webcams. + +--- + +### Phase 3: Role Playing + +**Interactive Focus:** Embodying 4 stakeholder personas to stress-test all concepts. + +**Persona 1 — Marcus (Veteran GM):** +- Wants zero-clicks during play — automation is king +- Scene-aware presets match his prep workflow perfectly +- Every automation needs a one-click GM override +- Reaction cam needs per-scene disable option + +**Persona 2 — Sofia (Privacy-Conscious Player):** +- Pull visibility model respects player agency +- Reaction clip system includes her when bandwidth is poor +- HP-reactive cam sizing must be opt-in per player +- Reaction cam auto-popup is a privacy risk without opt-out + +**Persona 3 — Jake (Actual-Play Streamer):** +- Spectator curtain is critical — audience vs player views must be independent +- Keyboard shortcuts essential during live broadcast +- Browser Source API (OBS-ready tile URLs) transforms the module into a production stack +- Seating chart popout needs instant keyboard shortcut access + +**Persona 4 — Alex (New Player):** +- Seating chart metaphor instantly understood; visibility matrix jargon was not +- Plain language everywhere — "Show/Hide/Spotlight" not "Wash/Focus/Blackout" +- Automation without feedback is terrifying — needs toast notifications +- Portrait fallback for bad/missing cameras reduces anxiety + +**Key Tension Resolved:** +Marcus wants drama (reaction cam), Sofia wants privacy (no surprise reveals). +→ **Resolution:** Reaction cam is opt-in at character/player setup. Consent-aware design. + +--- + +## Complete Idea Inventory (21 Concepts) + +| # | Concept | Source | Theme | +|---|---|---|---| +| 1 | The Living Table | SCAMPER-S | Core Architecture | +| 2 | Table Manager Popout | SCAMPER-S | Core Architecture | +| 3 | Scene-Aware Visibility Presets | SCAMPER-C | Automation | +| 4 | Combat Cinematics Mode | SCAMPER-C | Automation + Cinematic | +| 5 | The Reaction Cam | SCAMPER-C | Cinematic | +| 6 | The Director's Board | SCAMPER-A | Cinematic | +| 7 | Stage Lighting States | SCAMPER-A | Cinematic | +| 8 | Token-Anchored Floating Cams | SCAMPER-M | Cinematic | +| 9 | HP-Reactive Cam Styling | SCAMPER-M | Cinematic | +| 10 | NPC Presence Tiles | SCAMPER-P | Extended Presence | +| 11 | Spectator Curtain | SCAMPER-P | Streaming | +| 12 | Zero-UI Automation Mode | SCAMPER-E | Automation | +| 13 | Progressive Enhancement Architecture | SCAMPER-E | Core Architecture | +| 14 | Pull Visibility Model | SCAMPER-R | Core Architecture | +| 15 | Reaction Clip System | SCAMPER-R | Privacy + Extended Presence | +| 16 | Player Privacy Preferences Panel | Role Playing | Privacy | +| 17 | Opt-In Reaction Cam | Role Playing | Privacy + Cinematic | +| 18 | Dual Layout System | Role Playing | Streaming | +| 19 | Browser Source API | Role Playing | Streaming | +| 20 | Contextual Action Notifications | Role Playing | Automation | +| 21 | Vocabulary Tiers | Role Playing | UX / Onboarding | + +--- + +## Idea Organization and Prioritization + +### Thematic Clusters + +**🏗️ Theme 1: Core Architecture** +*What the module IS — foundational data model and UI structure* +- #1 The Living Table, #2 Table Manager Popout, #13 Progressive Enhancement, #14 Pull Visibility Model + +**🤖 Theme 2: Automation & Intelligence** +*The module as silent director — configure once, run forever* +- #3 Scene-Aware Presets, #4 Combat Cinematics Mode, #12 Zero-UI Automation Mode, #20 Contextual Notifications + +**🎬 Theme 3: Cinematic & Dramatic Effects** +*Transforms sessions into cinematic experiences* +- #5 Reaction Cam, #6 Director's Board, #7 Stage Lighting States, #8 Token-Anchored Cams, #9 HP-Reactive Styling, #17 Opt-In Reaction Cam + +**🔒 Theme 4: Privacy & Player Agency** +*No surprises — players always in control* +- #15 Reaction Clip System, #16 Player Privacy Panel, #17 Opt-In Reaction Cam, #21 Vocabulary Tiers + +**📺 Theme 5: Streaming & Extended Presence** +*Actual-play production without a technical director* +- #10 NPC Presence Tiles, #11 Spectator Curtain, #18 Dual Layout System, #19 Browser Source API + +--- + +### ⭐ North Star Feature (Confirmed by User) + +> **As a GM, I can click a player's webcam tile and toggle it visible/hidden for all players — instantly, reliably, one click.** + +Everything else is either a smarter trigger for this toggle, a richer display of its state, an extension of who controls it, or an extra surface for its result. + +--- + +### Priority Stack + +**🥇 Day 1 — The Core Toggle** +- GM clicks a player's cam tile → cam hidden/shown for all players +- State persists through player reconnection +- Plain visual indicator: "this cam is hidden by GM" +- Right-click context menu on existing AV tiles (Level 1 — zero new UI) +- FoundryVTT v14 API scope: hook into `game.webrtc` / existing AV tiles, store state in `game.settings` or socket broadcast + +**🥈 Week 1–2 — Make it practical** +- #2 Table Manager Popout (bulk management for 6+ players) +- #3 Scene-Aware Presets (configure at prep, not during play) +- #20 Contextual Notifications ("GM hid Sofia's cam") +- #16 Player Privacy Panel (opt-out of effects) +- #21 Vocabulary Tiers (plain language default, power terms in advanced mode) + +**🥉 Later — Make it powerful** +- #4 Combat Cinematics Mode + #17 Opt-In Reaction Cam +- #6 Director's Board + #7 Stage Lighting States +- #12 Zero-UI Automation Mode +- #1 Living Table / full seating chart UI +- #11 Spectator Curtain + #18 Dual Layout + #19 Browser Source API +- #8 Token-Anchored Cams, #9 HP-Reactive Styling, #10 NPC Presence Tiles, #15 Reaction Clip System + +--- + +## Action Planning + +### Day 1 Implementation Roadmap + +**1. Research — FoundryVTT v14 AV API** +- Read `game.webrtc` API and AVMaster hooks +- Identify how existing cam tiles are rendered in the DOM +- Determine if visibility is real (WebRTC track disable) or cosmetic (CSS) +- Check `game.settings` for persistent storage approach +- Review `socketlib` or native socket options for broadcasting state to all clients + +**2. Build — Core Toggle (MVP)** +- Register module in `module.json` for FoundryVTT v14 +- Hook into AV cam tile render to inject right-click context menu +- Implement `setPlayerCamVisible(playerId, visible)` function +- Broadcast state change via socket to all connected clients +- Each client applies visibility to their local cam tile render +- Persist state in `game.settings` (world-level) +- Add visual indicator on hidden tiles (icon overlay or opacity change) + +**3. Test — Edge Cases from Question Storming** +- Player disconnects while hidden → reconnects → state restored correctly +- GM cam hidden → what happens to GM's own view? +- Player with no camera → graceful fallback (portrait shown) +- Mid-combat hide → layout stable, no reflow disruption + +**4. Ship — Level 1** +- Zero new UI surface — pure right-click enhancement +- Works on all existing FoundryVTT AV layouts +- Document the 8-state participant model for future development + +--- + +## Session Summary and Insights + +**Key Achievements:** +- 21 breakthrough concepts generated across 3 creative techniques +- 5 thematic clusters identified covering the full product surface +- North star feature confirmed and scoped for Day 1 implementation +- 4 user personas stress-tested all concepts — critical tensions resolved +- Clear 3-tier priority stack established (Day 1 → Week 1-2 → Later) +- FoundryVTT v14 technical scope defined for MVP + +**Breakthrough Moments:** +- The seating chart metaphor reframed the UI from "settings panel" to "spatial social experience" +- The visibility matrix (`Map>`) clarified the true data model complexity +- The 4-persona analysis revealed 3 distinct user tiers (GM power, player privacy, streamer production) +- Progressive Enhancement Architecture ensures the module is useful at every complexity level +- Reaction cam opt-in resolved the Marcus/Sofia privacy tension elegantly + +**Creative Facilitation Narrative:** +The session began with a simple goal — webcam visibility control — and evolved into a session cinematography platform. Question Storming surfaced the architectural complexity (8 participant states, visibility matrix). SCAMPER transformed the concept from utility to product, discovering the seating chart metaphor, combat cinematics, and streaming tools. Role Playing grounded the ideas in human reality, revealing the privacy-first design principle and the 4-level user tier model. The final anchor back to "GM toggle = Day 1" ensures the ambitious vision stays rooted in a shippable, focused MVP. + +**Your Next Steps:** +1. Read FoundryVTT v14 AV API documentation (`game.webrtc`, `AVMaster`, cam tile hooks) +2. Build the right-click toggle on existing cam tiles (Day 1 MVP) +3. Test the 8 participant state transitions +4. Plan the Table Manager Popout and Scene Presets for Week 1–2 +5. Keep the 21-concept inventory as the product backlog for future sprints diff --git a/_bmad-output/implementation-artifacts/1-1-module-scaffold-cicd-pipeline-and-design-token-system.md b/_bmad-output/implementation-artifacts/1-1-module-scaffold-cicd-pipeline-and-design-token-system.md new file mode 100644 index 0000000..249761f --- /dev/null +++ b/_bmad-output/implementation-artifacts/1-1-module-scaffold-cicd-pipeline-and-design-token-system.md @@ -0,0 +1,581 @@ +# Story 1.1: Module Scaffold, CI/CD Pipeline & Design Token System + +Status: done + +## Story + +As a **developer**, +I want a fully configured module scaffold with the complete `--sp-*` design token system, contract files, tooling, and CI, +so that every subsequent story builds on enforced boundaries, verified tooling, and a stable design language. + +## Acceptance Criteria + +1. **Given** the repository is checked out fresh **When** `npm install && npm run lint && npm run typecheck && npm run test` are executed **Then** all commands exit 0 **And** `npm run build` produces `module.zip` containing `module.json`, `scripts/`, `styles/`, `templates/`, `lang/` + +2. **Given** the module is installed in FoundryVTT v14 **When** FoundryVTT loads **Then** the module activates with no console errors and `game.modules.get('video-view-manager').active === true` + +3. **Given** a developer writes an exported function without a JSDoc comment **When** `npm run lint` runs **Then** `eslint` reports a `jsdoc/require-jsdoc` violation + +4. **Given** a source file imports from a restricted layer **When** `npm run lint` runs **Then** `import/no-restricted-paths` reports a boundary violation + +5. **Given** a Gitea push is made **When** the CI workflow runs **Then** lint, typecheck, and test all run; a failing test fails the workflow + +6. **Given** a developer writes module CSS using a Foundry `--color-*`/`--font-*`/`--border-*` token directly inside `.scrying-pool` CSS **When** the linting convention is enforced **Then** a violation is reported — all Foundry tokens must be aliased through `--sp-*` + +7. **Given** a developer renders any participant state **When** they look up the token system in `styles/scrying-pool.less` **Then** all token layers are defined: + - Layer 1: SP semantic aliases (`--sp-surface`, `--sp-border`, `--sp-text-primary`, `--sp-text-secondary`, `--sp-accent`, `--sp-focus`) mapping to Foundry tokens with hardcoded fallbacks + - Layer 2: SP Participant State tokens for all 8 states (`active`, `hidden`, `self-muted`, `offline`, `cam-lost`, `reconnecting`, `never-connected`, `ghost`) + - Layer 3: SP Urgency + Motion tokens (`--sp-urgency-director`, `--sp-urgency-awareness`, `--sp-fade-hide`, `--sp-pulse-reconnecting`, `--sp-shimmer-degraded`, `--sp-toast-delay`) + - **And** the `VisibilityBadge` `:root` exception is documented: badge tokens declared on `:root` because badge mounts outside `.scrying-pool` root + - **And** all animated token usages gated under `@media (prefers-reduced-motion: no-preference)` + +8. **Given** the 4 contract files exist **When** a story imports `src/contracts/visibility-matrix.js` **Then** it exports a canonical shape constant, a factory function (`createVisibilityMatrix()`), and a guard/validator function (`isValidVisibilityMatrix(data)`) **And** the same factory + validator pattern applies to `socket-message.js`, `pending-op.js`, `scene-preset.js` + +## Tasks / Subtasks + +- [x] Task 1: Initialize npm project and install devDependencies (AC: #1) + - [x] 1.1 Run `npm init -y` and configure `package.json` with exact scripts block (see Dev Notes) + - [x] 1.2 Install all devDependencies with pinned versions (see Dev Notes) + - [x] 1.3 Verify `npm install` exits 0 with lock file generated + +- [x] Task 2: Create root config files (AC: #1, #3, #4) + - [x] 2.1 Create `tsconfig.json` with `checkJs`, `strict`, `noEmit`, `ESNext` target, `module: ESNext`, `moduleResolution: node16`, `allowJs: true` + - [x] 2.2 Create `.eslintrc.js` with `jsdoc/require-jsdoc` on all exported symbols and `import/no-restricted-paths` zones for all 6 boundary rules (see Dev Notes) + - [x] 2.3 Create `vitest.config.js` with happy-dom environment, path aliases, coverage config + - [x] 2.4 Create `.gitignore` excluding `dist/`, `node_modules/`, `*.zip` + +- [x] Task 3: Create `module.json` v14 manifest (AC: #2) + - [x] 3.1 Set `id: "video-view-manager"`, title, version from `package.json`, v14 compatibility block + - [x] 3.2 Register `esmodules: ["module.js"]`, `styles: ["dist/styles/scrying-pool.css"]`, `languages: [{ lang: "en", name: "English", path: "lang/en.json" }]` + +- [x] Task 4: Create `scripts/package.mjs` release script (AC: #1) + - [x] 4.1 Read version from `package.json`; write into `module.json` at release time + - [x] 4.2 Produce `module.zip` containing: `module.json`, `module.js`, `dist/`, `lang/`, `templates/`, `src/` + - [x] 4.3 Single version source of truth — `package.json` only; never manually edit `module.json` version field + +- [x] Task 5: Create `module.js` entry point stub (AC: #2) + - [x] 5.1 Empty orchestrator that registers `Hooks.once('init', () => {})` and `Hooks.once('ready', () => {})` + - [x] 5.2 No business logic — wiring only; add `[ScryingPool]` console log to confirm load + - [x] 5.3 Export nothing (module entry point, not a library) + +- [x] Task 6: Create the 4 contract files in `src/contracts/` (AC: #8) + - [x] 6.1 `src/contracts/visibility-matrix.js` — typedef + `createVisibilityMatrix()` + `isValidVisibilityMatrix()` + - [x] 6.2 `src/contracts/socket-message.js` — typedef + `createSocketMessage()` + `isValidSocketMessage()` + - [x] 6.3 `src/contracts/pending-op.js` — typedef + `createPendingOp()` + `isValidPendingOp()` + - [x] 6.4 `src/contracts/scene-preset.js` — typedef + `createScenePreset()` + `isValidScenePreset()` + - [x] 6.5 All validators: reject unknown keys, throw `TypeError` with field name on violation; timestamps as finite non-negative integers; id fields non-empty strings; nullable fields explicit `null` + +- [x] Task 7: Create design token LESS system (AC: #6, #7) + - [x] 7.1 Create `styles/scrying-pool.less` entry point with `@import` references only + - [x] 7.2 Create `styles/tokens/_base.less` — Layer 1 SP semantic aliases (6 tokens + hardcoded fallbacks) + - [x] 7.3 Create `styles/tokens/_states.less` — Layer 2 all 8 participant states + `pending` (9 total); color + icon + shape per state; LESS map `@sp-states` (see Dev Notes for exact map) + - [x] 7.4 Create `styles/tokens/_motion.less` — Layer 3/4 urgency + motion tokens; `--sp-fade-hide`, `--sp-pulse-reconnecting`, `--sp-shimmer-degraded`, `--sp-toast-delay`; all animated tokens under `@media (prefers-reduced-motion: no-preference)` + - [x] 7.5 Create `styles/tokens/_focus.less` — module-wide focus ring; high-contrast outer ring + inner offset + - [x] 7.6 Add `:root` block for `VisibilityBadge` exception with documenting comment + - [x] 7.7 Create `styles/components/` stubs (empty files with scope comment): `_participant-card.less`, `_roster-strip.less`, `_directors-board.less`, `_scene-preset-panel.less`, `_notification.less`, `_player-badge.less`, `_player-panel.less` + - [x] 7.8 Verify `npm run build` produces `dist/styles/scrying-pool.css` + +- [x] Task 8: Create test infrastructure (AC: #1) + - [x] 8.1 Create `tests/helpers/foundryAdapterMock.js` — `createFoundryAdapterMock(overrides={})` canonical mock; covers `settings`, `socket`, `users`, `scenes`, `notifications`, `webrtc: null`, `hooks` + - [x] 8.2 Create `tests/fixtures/socket-payloads.js` — `Object.freeze`'d stub with valid + malformed shapes (missing `opId`, wrong enum, extra keys) + - [x] 8.3 Create `tests/fixtures/visibility-states.js`, `state-store-snapshots.js`, `scene-preset.js`, `pending-op.js`, `foundry-adapter.js` — all `Object.freeze`'d + - [x] 8.4 Create contract test files in `tests/unit/contracts/` for all 4 contracts — test factory happy path, validator rejections + +- [x] Task 9: Create Gitea CI workflow (AC: #5) + - [x] 9.1 Create `.gitea/workflows/ci.yml` — runs on every push; steps: `npm ci`, `npm run lint`, `npm run typecheck`, `npm run test` + - [x] 9.2 Failing test must fail the workflow (non-zero exit propagates) + +- [x] Task 10: Create `lang/en.json` i18n skeleton (AC: #1) + - [x] 10.1 Create with empty-but-valid JSON `{}` — or a top-level `"video-view-manager"` namespace stub + - [x] 10.2 Register in `module.json` as `languages` array entry + +- [x] Task 11: Create `templates/` stubs (AC: #1) + - [x] 11.1 Create minimal stub `.hbs` files: `directors-board.hbs`, `participant-card.hbs`, `roster-strip.hbs`, `scene-preset-panel.hbs`, `player-panel.hbs` + +- [x] Task 12: Verify full pipeline (AC: #1, #3, #4) + - [x] 12.1 `npm run lint` exits 0 on clean code; reports violation on missing JSDoc export + - [x] 12.2 `npm run typecheck` exits 0 + - [x] 12.3 `npm run test` exits 0 (contract tests pass) + - [x] 12.4 `npm run build` produces `dist/styles/scrying-pool.css` + - [x] 12.5 `npm run release` produces `module.zip` + +### Review Findings + +- [x] [Review][Decision] Empty module entry point — Hooks registered with empty implementations, no actual operations (RESOLVED: Keep as stub for story 1.1) +- [x] [Review][Decision] Production zip includes source — `src/` included in INCLUDE list for production module.zip (RESOLVED: Keep as-is, standard for Foundry modules) +- [x] [Review][Decision] Hardcoded module.json version — Violates single source of truth (managed by package.json) (RESOLVED: Keep hardcoded, synced at release time) +- [x] [Review][Patch] Missing zip dependency [scripts/package.mjs:47] +- [x] [Review][Patch] Build dependency not declared [package.json:14] (less is already installed, lessc available) +- [x] [Review][Patch] Incomplete .gitignore [.gitignore:1-4] +- [x] [Review][Patch] No error handling for file reads [scripts/package.mjs:22,24] +- [x] [Review][Patch] Zip command injection risk [scripts/package.mjs:43-44] +- [x] [Review][Patch] Incomplete lint scope [package.json:10] +- [x] [Review][Patch] Missing ts-nocheck on module.js [module.js:1] +- [x] [Review][Patch] Overly restrictive import zones [eslint.config.js:42-91] (Added import/resolver settings) + +--- + +**Group 2a Findings (package-lock.json):** +- [x] [Review][Patch] Missing foundry-vtt-types dependency — Added to package.json and package-lock.json per AC#1 +- [x] [Review][Patch] Lock file out of sync — Added zip dependency to package-lock.json root devDependencies + +--- + +**Group 2b Findings (package-lock.json lines 2401-4967):** +- [x] [Review][Patch] Invalid placeholder hash for zip — Replaced with real SHA-512 hash from npm registry +- [x] [Review][Patch] Missing integrity hash for foundry-vtt-types — Added integrity field using npm registry version +- [x] [Review][Patch] foundry-vtt-types not SHA-pinned — Changed to npm registry version 13.346.0-beta.20250812191140 +- [x] [Review][Patch] Missing integrity hashes — All entries now have integrity fields +- [x] [Review][Patch] Root package missing license field — Added MIT license to root entry +- [x] [Review][Patch] No root package engine specification — Added Node.js >=18.0.0 engines field +- [x] [Review][Decision] Non-pinned devDependencies — Use caret ranges vs exact pinned versions per spec (DEFERRED: Address when version pinning strategy finalized) +- [x] [Review][Defer] Vulnerable dependency CVE-2023-43645 — flat-cache-4.0.1 (verify vulnerability) + +--- + +**Group 2b Re-review Findings:** +- [x] [Review][Patch] Beta dependency risk — Changed foundry-vtt-types from 13.346.0-beta to stable 9.280.1 +- [x] [Review][Patch] Version range too permissive — Changed @types/node from ^22.0.0 to 22.x per spec +- [x] [Review][Decision] Non-pinned devDependencies — Caret ranges violate exact version requirement per Task 1.2 (RESOLVED: Keep caret ranges, lock file provides exact resolved versions) +- [ ] [Review][Defer] Version conflicts — Multiple versions: make-dir (5.7.2, 6.3.1, 7.8.0), debug (4.4.3, 3.2.7) +- [x] [Review][Dismiss] Platform-specific optionals — 83+ optional packages (expected npm behavior) +- [x] [Review][Dismiss] Postinstall script risk — esbuild/fsevents (expected npm behavior) +- [x] [Review][Dismiss] Nested node_modules — 9 nested paths (expected npm behavior) +- [x] [Review][Dismiss] Engine requirement mismatch — Root >=18.0.0 vs dep ranges (expected npm behavior) +- [x] [Review][Dismiss] Peer dependency breadth — eslint-plugin-import range (expected npm behavior) +- [x] [Review][Dismiss] Optional peer without fallback — happy-dom requires @types/node (expected npm behavior) +- [x] [Review][Dismiss] Packages with install scripts — esbuild/fsevents expected behavior + +--- + +**Group 3 Findings (Source Contracts):** +- [x] [Review][Patch] Whitespace strings pass validation — Added .trim() to string checks [src/contracts/pending-op.js] +- [x] [Review][Patch] Non-finite timeoutId passes — Added Number.isFinite() check [src/contracts/pending-op.js] +- [x] [Review][Patch] Missing canonical shape constant — Added PENDING_OP_VERSION [src/contracts/pending-op.js] +- [x] [Review][Patch] Incomplete validator — Added finite non-negative check for timeoutId [src/contracts/pending-op.js] +- [x] [Review][Decision] Factory signature deviation — Positional params vs Partial input (RESOLVED: Keep positional params for ergonomics, matches other contracts) +- [x] [Review][Dismiss] No max string length validation — Expected behavior [src/contracts/pending-op.js:54-64] +- [x] [Review][Dismiss] Future issuedAt allowed — Expected behavior [src/contracts/pending-op.js:66] +- [x] [Review][Dismiss] Unvalidated createPendingOp — Expected behavior [src/contracts/pending-op.js:35] + +--- + +**Group 4 Findings (Styles & Tokens):** +- [x] [Review][Patch] Focus indicator removal — Added visible fallback comment [styles/tokens/_focus.less:15] +- [x] [Review][Patch] Reduced motion kills animations — Replaced universal selector, removed !important [styles/tokens/_motion.less] +- [x] [Review][Patch] Universal selector performance — Replaced with specific selectors [styles/tokens/_motion.less] +- [x] [Review][Patch] Overuse of !important — Removed from reduced motion rules [styles/tokens/_motion.less] +- [x] [Review][Patch] Focus ring clipped by overflow — Added overflow warning comment [styles/tokens/_focus.less:17] +- [x] [Review][Patch] Missing --sp-urgency-awareness token — Added to Layer 3 [styles/tokens/_motion.less] +- [x] [Review][Patch] urgency-director in wrong layer — Moved from Layer 1 to Layer 3 [styles/tokens/_motion.less] +- [x] [Review][Decision] Empty component files — 7 files with only comments (story 1.5+ stubs) (RESOLVED: Keep as-is) +- [x] [Review][Decision] No light theme support — Only dark theme colors defined (RESOLVED: Keep dark-only) +- [x] [Review][Decision] Shadow DOM scope issue — :root badge tokens inaccessible (RESOLVED: Keep as-is, mounts to Foundry AV tile) +- [x] [Review][Dismiss] WCAG contrast warnings — Documented in spec as intentional (icon/shape only) +- [x] [Review][Dismiss] Nested var() compatibility — Acceptable browser support + +--- + +**Group 5 Findings (Templates):** +- [x] [Review][Patch] Missing accessibility — Added role="region" and aria-label to all template divs [templates/*.hbs] +- [x] [Review][Patch] Non-ASCII character — Replaced em-dash `—` with ASCII hyphen `-` in comments [templates/*.hbs] +- [x] [Review][Patch] Missing data attributes — Added data-component for JS targeting [templates/*.hbs] +- [x] [Review][Dismiss] Non-semantic class name — "scrying-pool" is module root (intentional per spec) +- [x] [Review][Dismiss] Generic class name — BEM-like component names inside .scrying-pool (intentional per architecture) +- [x] [Review][Dismiss] Empty template structure — Intentional stubs for story 1.5+ (Task 11.1) +- [x] [Review][Dismiss] Incomplete delivery — False positive (all 5 templates present) + +--- + +**Group 6 Findings (Tests):** +- [x] [Review][Patch] Story 1.2 test file pollutes scope — Removed tests/unit/foundry/FoundryAdapter.test.js +- [x] [Review][Patch] Story 1.2 fixture pollutes scope — Removed tests/fixtures/foundry-adapter.js +- [x] [Review][Patch] Redundant fixture duplicates mock — Removed tests/fixtures/foundry-adapter.js (with AA-02) +- [x] [Review][Patch] Unfrozen mutable array — Fixed contents: [] → Object.freeze([]) in foundry-adapter.js stub (merged with AA-02 removal) +- [x] [Review][Patch] Non-string state values not tested — Added Symbol and object tests to visibility-matrix.test.js +- [x] [Review][Patch] Non-finite updatedAt not tested — Added NaN and Infinity tests to scene-preset.test.js +- [x] [Review][Patch] Missing revision fixture — Added missingRevision to socket-payloads.js +- [x] [Review][Patch] Empty event string not tested — Added test in socket-message.test.js +- [x] [Review][Patch] String timeoutId not tested — Added test in pending-op.test.js +- [x] [Review][Patch] Mock inconsistency — Aligned users.current() with users.get() in foundryAdapterMock.js +- [x] [Review][Defer] MAX_PAYLOAD_BYTES size boundary — Story 1.2+ SocketHandler responsibility +- [x] [Review][Defer] Prototype pollution in matrix — Covered by unknown key rejection +- [x] [Review][Defer] Multi-track stream test — Story 1.2+ WebRTC surface +- [x] [Review][Defer] Invalid snapshot fixtures — Story 1.3+ StateStore +- [x] [Review][Dismiss] Numeric/special char matrix keys — userId always string per contract +- [x] [Review][Dismiss] Empty userId in matrix — Already tested in visibility-matrix.test.js +- [x] [Review][Dismiss] Long preset name fixture — Edge case, not critical +- [x] [Review][Dismiss] Negative issuedAt test — Existing test covers +- [x] [Review][Dismiss] Populated game state fixture — Tests can customize mock +- [x] [Review][Dismiss] Large matrix stress test — Performance concern, not correctness +- [x] [Review][Dismiss] Circular reference in gameWebrtc — Story 1.2+ WebRTC +- [x] [Review][Dismiss] Boolean/number payload values — Type validation handles +- [x] [Review][Dismiss] Undefined parameters in createScenePreset — Factory handles defaults +- [x] [Review][Dismiss] Mock methods not vi.fn by default — Test utility concern, not bug + +--- + +**Group 7 Findings (CI/CD & i18n):** +- [x] [Review][Decision] pull_request trigger scope — Kept as enhancement beyond Task 9 spec (AC5 still met) [.gitea/workflows/ci.yml] +- [x] [Review][Decision] Build step scope — Kept as enhancement beyond Task 9 spec [.gitea/workflows/ci.yml] +- [x] [Review][Patch] Unpinned action versions — Pinned to commit SHAs [.gitea/workflows/ci.yml:9,13] +- [x] [Review][Patch] Overly permissive branch triggers — Restricted from ["**"] to ["main"] [.gitea/workflows/ci.yml:3-7] +- [x] [Review][Patch] Missing npm audit — Added security audit step [.gitea/workflows/ci.yml:24-25] +- [x] [Review][Patch] Empty i18n breaks Foundry — Added namespace stub {"video-view-manager": {}} [lang/en.json] +- [x] [Review][Defer] Missing failure notifications — Story 1.2+ (EC-002) +- [x] [Review][Defer] Missing build artifact upload — Story 1.2+ (EC-006) +- [x] [Review][Defer] No Node.js matrix testing — Story 1.2+ (EC-004) +- [x] [Review][Defer] Missing concurrency control — Story 1.2+ (EC-008) +- [x] [Review][Defer] Missing i18n schema validation — Story 1.2+ (EC-I18N-003) +- [x] [Review][Dismiss] ubuntu-latest not pinned — Story 1.2+ (EC-003) +- [x] [Review][Dismiss] Missing coverage upload — Story 1.2+ (EC-005) +- [x] [Review][Dismiss] Translation fallback — Handled by Foundry i18n system (EC-I18N-002) +- [x] [Review][Dismiss] Missing encoding spec — UTF-8 default in Node.js (EC-I18N-005) + +## Dev Notes + +### npm scripts — exact definitions + +```json +"scripts": { + "build": "lessc styles/scrying-pool.less dist/styles/scrying-pool.css", + "watch": "chokidar 'styles/**/*.less' -c 'lessc styles/scrying-pool.less dist/styles/scrying-pool.css'", + "typecheck": "tsc --noEmit", + "lint": "eslint src/ module.js", + "test": "vitest run", + "test:watch": "vitest", + "release": "node scripts/package.mjs" +} +``` + +### devDependencies — exact pinned versions + +```bash +npm install --save-dev \ + less@4.6.4 \ + chokidar@5.0.0 \ + vitest@2.1.8 \ + happy-dom@20.x \ + typescript@5.9.3 \ + @league-of-foundry-developers/foundry-vtt-types@{pin to a specific commit SHA for v14} \ + @types/node@22.x \ + eslint \ + eslint-plugin-jsdoc \ + eslint-plugin-import +``` + +> ⚠️ `foundry-vtt-types` MUST be pinned to a specific commit SHA, not `#main`. Document the SHA and the Foundry v14 version it targets in a comment in `tsconfig.json` or `package.json`. +> `chokidar` is for LESS watch only — `less --watch` does NOT detect changes in `@import`-ed partials. + +### Import boundary rules (hard — must be wired into `.eslintrc.js`) + +``` +src/core/ → may import: src/contracts/, src/utils/ ONLY +src/foundry/ → may import: src/contracts/, src/utils/ +src/notifications/ → may import: src/core/, src/contracts/, src/utils/ +src/presets/ → may import: src/core/, src/contracts/, src/utils/ +src/ui/ → may import: src/core/, src/contracts/, src/utils/ +src/contracts/ → no internal imports +src/utils/ → no internal imports +module.js → may import: all of src/ +``` + +❌ `src/core/` importing `src/foundry/`, `src/ui/`, `src/notifications/`, or `src/presets/` is a hard violation. +❌ `src/foundry/` importing `src/core/` or `src/ui/` is a hard violation. + +These must be configured as `import/no-restricted-paths` zones — not aspirational documentation. + +### Contract file pattern (apply to all 4) + +```js +/** @typedef {{ opId: string, userId: string, targetState: string, + * previousState: string, issuedAt: number, timeoutId: number|null }} PendingOp */ + +/** + * @param {Partial} input + * @returns {PendingOp} + */ +export function createPendingOp(input) { ... } + +/** + * @param {unknown} dto + * @returns {PendingOp} + * @throws {TypeError} with field name on violation + */ +export function isValidPendingOp(dto) { ... } +``` + +Validator rules: +- Reject unknown keys +- Timestamps: finite, non-negative integer +- Id fields: non-empty string +- Arrays default `[]` +- Nullable fields explicit `null` (never `undefined`) + +### LESS state map — exact shape for `styles/tokens/_states.less` + +```less +@sp-states: { + active: { color: @sp-color-active, icon: '\f06e'; shape: solid; }; + hidden: { color: @sp-color-hidden, icon: '\f070'; shape: dashed; }; + self-muted: { color: @sp-color-muted, icon: '\f131'; shape: solid; }; + offline: { color: @sp-color-offline, icon: '\f00d'; shape: none; }; + cam-lost: { color: @sp-color-warning, icon: '\f03d'; shape: dashed; }; + reconnecting: { color: @sp-color-info, icon: '\f021'; shape: pulse; }; + never-connected: { color: @sp-color-neutral, icon: '\f068'; shape: none; }; + ghost: { color: @sp-color-ghost, icon: '\f2be'; shape: dotted; }; + pending: { color: @sp-color-neutral, icon: '\f110'; shape: pulse; }; +}; +``` + +State colour values (from UX spec): + +| State | Hex | WCAG AA | +|---|---|---| +| `active` | `#4a9e6b` | ✅ | +| `hidden` | `#6b7280` | ⚠️ Icon/shape only | +| `self-muted` | `#8b92a5` | ✅ | +| `offline` | `#4b5563` | ⚠️ Icon/shape only | +| `cam-lost` | `#9ca3af` | ✅ | +| `reconnecting` | `#c8982a` | ✅ | +| `never-connected` | `#374151` | ⚠️ Icon/shape only | +| `ghost` | `#1f2937` | ⚠️ Icon/shape only | + +> ⚠️ States marked "Icon/shape only" MUST NOT appear as text or small-pill foreground — colour is supplementary; icon + shape carry the primary signal (WCAG requirement). + +### Layer 1 SP semantic alias tokens (exact values from UX spec) + +```css +:root { + --sp-surface: var(--sp-theme-surface, var(--color-bg-option, #141618)); + --sp-text-primary: var(--sp-theme-text-primary, var(--color-text-primary, #dde2e8)); + --sp-text-secondary: var(--sp-theme-text-muted, var(--color-text-secondary, #7a8390)); + --sp-accent: var(--sp-theme-accent, var(--color-warm-2, #4a9e6b)); + --sp-focus: var(--sp-theme-focus, var(--color-focus-outline, #63c287)); + --sp-urgency-director: var(--sp-theme-urgency, #c8982a); /* NO Foundry error/warn token */ +} +``` + +> ⚠️ `--sp-urgency-director` MUST NOT inherit Foundry's error/warn colours. A director cue is a deliberate stage direction, not a failure. + +### State token naming — three sub-tokens per state + +Each state provides three CSS custom properties: +``` +--sp-state-{name}-text +--sp-state-{name}-border +--sp-state-{name}-bg +``` + +### VisibilityBadge `:root` exception + +Badge (`PlayerStatusBadge`) is mounted outside the `.scrying-pool` root DOM node, directly onto Foundry's AV tile DOM. Badge state tokens MUST be declared on `:root` so they are available outside the module's root. Add a prominent comment explaining this architectural exception. + +### Naming conventions (enforce across all files) + +- JS files (classes/modules): PascalCase — `StateStore.js`, `FoundryAdapter.js` +- Utility/helper files: camelCase — `uuid.js` +- Contract files: kebab-case — `socket-message.js`, `pending-op.js` +- Test files: `{SourceFile}.test.js` — `StateStore.test.js` +- Named exports only — `export class StateStore {}`, never `export default` +- World settings prefix: `scrying-pool.` — never `video-view-manager.X`, `sp.X`, `vvm.X` +- Socket events prefix: `scrying-pool.` +- CSS prefix: `.sp-` or scoped under `.scrying-pool` +- Console prefix: `[ScryingPool]` on ALL console calls +- Public API returns `null` not `undefined` for "not found" + +### Constructor rule + +```js +// ✅ constructor(adapter) { this._adapter = adapter; } +// init() { this._adapter.hooks.once('ready', () => this._onReady()); } +// ❌ constructor(adapter) { adapter.hooks.once('ready', ...); } +``` +Lifecycle registration belongs in `module.js` (owns `Hooks.once('init')` and `Hooks.once('ready')`). Individual module constructors must be side-effect free. + +### Test pattern — canonical mock + +```js +import { createFoundryAdapterMock } from '../helpers/foundryAdapterMock.js' +const adapter = createFoundryAdapterMock({ settings: { get: () => 'custom' } }) +``` +**No ad-hoc stubs.** All tests use `createFoundryAdapterMock` — the canonical mock factory is established in this story and reused by all subsequent tests. + +### Fixture pattern + +```js +// All fixtures are Object.freeze'd +export const SOCKET_PAYLOADS = Object.freeze({ ... }) +// Include negative/invalid fixtures for every validateX() rejection branch +``` + +### socket-payloads.js fixture shape (stub at this stage) + +Must include at minimum: +- Valid intent payload (`scrying-pool.visibility.set`) +- Valid echo payload (`scrying-pool.visibility.updated`) +- Malformed: missing `opId` +- Malformed: wrong enum value for state +- Malformed: extra unknown keys + +### State precedence (for VisibilityManager stories — document here for context) + +``` +pending > cam-lost > reconnecting > offline > never-connected > self-muted > hidden > ghost > active +``` + +CSS never handles multi-state conflicts — VisibilityManager/RoleRenderer resolve before rendering. + +### module.json v14 manifest required fields + +```json +{ + "id": "video-view-manager", + "title": "Video View Manager (Scrying Pool)", + "version": "0.1.0", + "compatibility": { "minimum": "14", "verified": "14" }, + "esmodules": ["module.js"], + "styles": ["dist/styles/scrying-pool.css"], + "languages": [{ "lang": "en", "name": "English", "path": "lang/en.json" }] +} +``` + +> ⚠️ `version` field in `module.json` is managed by `scripts/package.mjs` at release time. Do NOT edit it manually during development. + +### Project Structure Notes + +This is story 1.1 — the project root is currently empty. Create the full directory structure from scratch: + +``` +video-view-manager/ +├── module.json +├── module.js ← stub (Hooks.once init/ready only) +├── package.json +├── tsconfig.json +├── vitest.config.js +├── .eslintrc.js +├── .gitignore +├── .gitea/workflows/ci.yml +├── scripts/package.mjs +├── src/ +│ ├── contracts/ ← 4 contract files (full implementation) +│ └── utils/uuid.js ← stub (opId generation for PendingOp, later stories) +├── styles/ +│ ├── scrying-pool.less ← @import entry point only +│ ├── tokens/ +│ │ ├── _base.less +│ │ ├── _states.less +│ │ ├── _motion.less +│ │ └── _focus.less +│ └── components/ ← 7 stub LESS files +├── templates/ ← 5 stub .hbs files +├── lang/en.json +└── tests/ + ├── helpers/foundryAdapterMock.js + ├── fixtures/ ← 6 fixture files (all Object.freeze'd) + └── unit/contracts/ ← 4 contract test files +``` + +**No `src/core/`, `src/foundry/`, `src/ui/` files yet** — those are Story 1.2+. Import boundary ESLint rules must still be configured now so they catch violations as soon as those files are created. + +### References + +- Architecture — scaffold decisions: [Source: _bmad-output/planning-artifacts/architecture.md#Starter Template Evaluation] +- Architecture — project structure: [Source: _bmad-output/planning-artifacts/architecture.md#Complete Project Directory Structure] +- Architecture — naming + enforcement: [Source: _bmad-output/planning-artifacts/architecture.md#Naming Patterns] +- Architecture — import boundaries: [Source: _bmad-output/planning-artifacts/architecture.md#Import Boundary Rule] +- Architecture — contract format: [Source: _bmad-output/planning-artifacts/architecture.md#Contracts] +- Architecture — test patterns: [Source: _bmad-output/planning-artifacts/architecture.md#Test Patterns] +- Architecture — LESS/CSS patterns: [Source: _bmad-output/planning-artifacts/architecture.md#LESS / CSS Patterns] +- UX spec — design token layers: [Source: _bmad-output/planning-artifacts/ux-design-specification.md#Layer 1–4] +- UX spec — state colour values: [Source: _bmad-output/planning-artifacts/ux-design-specification.md#state colour table] +- UX spec — CSS scoping discipline: [Source: _bmad-output/planning-artifacts/ux-design-specification.md#Linting convention] +- Epics — Story 1.1 ACs + deliverables: [Source: _bmad-output/planning-artifacts/epics.md#Story 1.1] + +## Dev Agent Record + +### Agent Model Used + +Claude Sonnet 4.6 (claude-sonnet-4.6) via GitHub Copilot CLI + +### Debug Log References + +- ESLint 9.x uses flat config (`eslint.config.js`), not `.eslintrc.js` as story spec states. Used flat config with `@eslint/js`, browser globals, and FoundryVTT globals declared explicitly. +- LESS `*/` in block comments causes ParseError — switched all LESS `/** */` comments to `//` line comments. +- `tsconfig.json`: `moduleResolution: "node16"` requires `module: "Node16"` — changed to `moduleResolution: "bundler"` (TS 5+ only, compatible with ESNext module). +- Contract validators: destructuring `unknown` requires intermediate `Record` cast for TypeScript strict mode. +- `scripts/package.mjs`: `catch(err)` needs `err instanceof Error ? err.message : String(err)` under strict TS. +- Test fixture/helper files with untyped stub functions use `// @ts-nocheck` pragma — deliberate, test infrastructure only. +- `globals` package available transitively via ESLint — used for `globals.browser` in flat config. + +### Completion Notes List + +- ✅ Task 1: `package.json` initialized, `npm install` exits 0; Node 24.14.1 / npm 11.4.0 +- ✅ Task 2: `tsconfig.json` (moduleResolution: bundler), `eslint.config.js` (flat config), `vitest.config.js`, `.gitignore` +- ✅ Task 3: `module.json` v14 manifest with all required fields +- ✅ Task 4: `scripts/package.mjs` release script; produces `module.zip` (14 KB) at v0.1.0 +- ✅ Task 5: `module.js` entry stub with `Hooks.once('init')` and `Hooks.once('ready')` +- ✅ Task 6: All 4 contract files fully implemented with factory + validator; all validators reject unknown keys, throw `TypeError` with field name +- ✅ Task 7: Full LESS token system; `npm run build` → `dist/styles/scrying-pool.css` ✅; VisibilityBadge `:root` exception documented +- ✅ Task 8: 49 unit tests across 4 contract test files; all pass; `tests/helpers/foundryAdapterMock.js` canonical mock established +- ✅ Task 9: `.gitea/workflows/ci.yml` — lint + typecheck + test + build on every push +- ✅ Task 10: `lang/en.json` — valid `{}` skeleton +- ✅ Task 11: 5 Handlebars template stubs created +- ✅ Task 12: Full pipeline — `lint` ✅ `typecheck` ✅ `test` ✅ `build` ✅ `release` ✅ + +**Deviations from story spec:** +- `eslint.config.js` (flat config) used instead of `.eslintrc.js` (ESLint 9 dropped `.eslintrc.*` support) +- `moduleResolution: "bundler"` instead of `"node16"` (node16 requires module: Node16) +- `foundry-vtt-types` not installed — `Hooks` declared via minimal `src/types/foundry-globals.d.ts` instead + +### File List + +- `package.json` +- `package-lock.json` +- `tsconfig.json` +- `eslint.config.js` +- `vitest.config.js` +- `.gitignore` +- `module.json` +- `module.js` +- `scripts/package.mjs` +- `src/types/foundry-globals.d.ts` +- `src/utils/uuid.js` +- `src/contracts/visibility-matrix.js` +- `src/contracts/socket-message.js` +- `src/contracts/pending-op.js` +- `src/contracts/scene-preset.js` +- `styles/scrying-pool.less` +- `styles/tokens/_base.less` +- `styles/tokens/_states.less` +- `styles/tokens/_motion.less` +- `styles/tokens/_focus.less` +- `styles/components/_participant-card.less` +- `styles/components/_roster-strip.less` +- `styles/components/_directors-board.less` +- `styles/components/_scene-preset-panel.less` +- `styles/components/_notification.less` +- `styles/components/_player-badge.less` +- `styles/components/_player-panel.less` +- `lang/en.json` +- `templates/directors-board.hbs` +- `templates/participant-card.hbs` +- `templates/roster-strip.hbs` +- `templates/scene-preset-panel.hbs` +- `templates/player-panel.hbs` +- `.gitea/workflows/ci.yml` +- `tests/helpers/foundryAdapterMock.js` +- `tests/fixtures/socket-payloads.js` +- `tests/fixtures/visibility-states.js` +- `tests/fixtures/state-store-snapshots.js` +- `tests/fixtures/scene-preset.js` +- `tests/fixtures/pending-op.js` +- `tests/fixtures/foundry-adapter.js` +- `tests/unit/contracts/visibility-matrix.test.js` +- `tests/unit/contracts/socket-message.test.js` +- `tests/unit/contracts/pending-op.test.js` +- `tests/unit/contracts/scene-preset.test.js` + +### Change Log + +- 2025-05-21: Story 1.1 implementation complete — full scaffold, CI, LESS token system, contracts, test infrastructure (49 tests, all pass; full pipeline lint/typecheck/test/build/release exits 0) \ No newline at end of file diff --git a/_bmad-output/implementation-artifacts/1-2-webrtc-spike-track-disabling-api-validation.md b/_bmad-output/implementation-artifacts/1-2-webrtc-spike-track-disabling-api-validation.md new file mode 100644 index 0000000..a3e403c --- /dev/null +++ b/_bmad-output/implementation-artifacts/1-2-webrtc-spike-track-disabling-api-validation.md @@ -0,0 +1,289 @@ +# Story 1.2: WebRTC Spike — Track Disabling API Validation + +Status: done + +## Story + +As a **developer**, +I want to determine the FoundryVTT v14 WebRTC API capability and freeze the `FoundryAdapter.webrtc` interface contract, +so that Story 1.3 can implement against a stable interface without ambiguity. + +## Acceptance Criteria + +1. **Given** FoundryVTT v14 running with AV enabled **When** the spike probe code executes **Then** the result is exactly one of three documented outcomes: `"track-disable"` (track.enabled = false confirmed on a remote inbound stream), `"css-fallback"` (programmatic track access unavailable; CSS/DOM hiding sufficient), or `"unsupported"` (neither approach reliably available) **And** the outcome is recorded as a code comment in `src/foundry/FoundryAdapter.js` with the FoundryVTT version tested against. + +2. **Given** the spike outcome is determined **When** `src/foundry/FoundryAdapter.js` is examined **Then** the `FoundryAdapter.webrtc` interface contract is frozen: either `{ disableTrack(userId): void, enableTrack(userId): void }` or `null` **And** the `probeCapability()` static method documents which `game.webrtc` properties were checked and their availability. + +3. **Given** the spike file exists **When** `module.js` runs its `Hooks.once('init')` handler **Then** `scrying-pool.webrtcMode` world setting is registered with `choices: ["track-disable", "css-fallback", "unsupported"]` (no default — set by probe outcome at first load). + +4. **Given** outcome is `"track-disable"` **When** a participant is hidden **Then** `FoundryAdapter.webrtc.disableTrack(userId)` disables the inbound track (`track.enabled = false`) and no inbound video bandwidth is consumed. + +5. **Given** outcome is `"css-fallback"` or `"unsupported"` **When** a participant is hidden **Then** CSS/DOM hiding is applied (cosmetic only; bandwidth still consumed). This path is the safe fallback. + +6. **Given** `game.webrtc` is `null` or absent **When** `FoundryAdapter.probeCapability()` runs **Then** it returns `"unsupported"`, sets `webrtc` to `null`, no errors are thrown, no code attempts any WebRTC access. + +7. **Given** the probe outcome is `"track-disable"` **When** `tests/unit/foundry/FoundryAdapter.test.js` runs **Then** tests verify: (a) interface shape parity with the canonical mock, (b) `disableTrack` calls `track.enabled = false` on the retrieved connection, (c) error path returns `null` gracefully and logs `[ScryingPool]` warning, (d) `null` game.webrtc path returns `null` without errors. + +## Tasks / Subtasks + +- [x] Task 1: Create `src/foundry/FoundryAdapter.js` skeleton with probe (AC: #1, #2, #6) + - [x] 1.1 Create `src/foundry/` directory + - [x] 1.2 Implement `FoundryAdapter` class — constructor side-effect-free; `static probeCapability(gameWebrtc)` method that inspects `game.webrtc` and returns one of the three outcome strings + - [x] 1.3 Implement `webrtc` property: if probe outcome is `"track-disable"`, return `{ disableTrack(userId), enableTrack(userId) }` object; otherwise return `null` + - [x] 1.4 **Actually run the probe** in a live FoundryVTT v14 session (or investigate the API surface via documentation/source) and record the outcome as a code comment at the top of the class + - [x] 1.5 Add outcome comment block: `// OQ-1 Spike Result: — FoundryVTT v` + +- [x] Task 2: Register `scrying-pool.webrtcMode` world setting in `module.js` (AC: #3) + - [x] 2.1 Update `module.js` `Hooks.once('init')` stub to call `game.settings.register('scrying-pool', 'webrtcMode', { ... })` + - [x] 2.2 Setting config: `scope: 'world'`, `config: false` (internal, not shown in settings UI), `type: String`, `choices: { 'track-disable': ..., 'css-fallback': ..., 'unsupported': ... }`, `default: 'css-fallback'` + - [x] 2.3 Add setting key constant to `src/foundry/FoundryAdapter.js`: `static SETTING_WEBRTC_MODE = 'webrtcMode'` (+ `static SETTINGS_NS = 'scrying-pool'`) + +- [x] Task 3: Update `tests/helpers/foundryAdapterMock.js` for webrtc interface (AC: #7) + - [x] 3.1 Add `webrtc: null` default path (already present — verify it remains the canonical default) + - [x] 3.2 Document that overriding webrtc with `{ disableTrack: vi.fn(), enableTrack: vi.fn() }` simulates the `"track-disable"` outcome + +- [x] Task 4: Write `tests/unit/foundry/FoundryAdapter.test.js` (AC: #7) + - [x] 4.1 Create `tests/unit/foundry/` directory + - [x] 4.2 Test: `probeCapability(null)` returns `"unsupported"` without errors + - [x] 4.3 Test: `probeCapability` with a mock `game.webrtc` that has `client.getMediaStreamForUser` → returns `"css-fallback"` (OQ-1 spike result: track-disable not achievable) + - [x] 4.4 Test: `probeCapability` with a mock `game.webrtc` that lacks `client.getMediaStreamForUser` → returns `"unsupported"` + - [x] 4.5 Test: `disableTrack(userId)` calls `track.enabled = false` on the resolved track + - [x] 4.6 Test: `disableTrack(userId)` when `getMediaStreamForUser()` returns `null` → logs `[ScryingPool] warn` and does not throw + - [x] 4.7 Test: `enableTrack(userId)` restores `track.enabled = true` + - [x] 4.8 Test: FoundryAdapter interface shape matches `createFoundryAdapterMock()` surface keys + +- [x] Task 5: Verify pipeline (AC: all) + - [x] 5.1 `npm run lint` exits 0 — `src/foundry/` imports only `src/contracts/` and `src/utils/` + - [x] 5.2 `npm run typecheck` exits 0 + - [x] 5.3 `npm run test` exits 0 (all FoundryAdapter tests pass) + +## Dev Notes + +### This Is a Spike — Deliverable Is a Decision + Skeleton, Not a Full Implementation + +Story 1.2 is **a spike story**. The primary output is: +1. A documented, **frozen** `FoundryAdapter.webrtc` interface decision (`track-disable` or `null`) +2. The minimal `src/foundry/FoundryAdapter.js` skeleton with the probe code +3. The `scrying-pool.webrtcMode` setting registration + +**Story 1.3 builds the full FoundryAdapter** (settings, socket, users, scenes, notifications, hooks surfaces). Do NOT implement the full adapter here — only the webrtc probe portion. + +### File Path: Use Architecture-Canonical Path + +The epics file references `src/adapters/foundry-adapter.js` but the **architecture document is canonical** — use `src/foundry/FoundryAdapter.js`. PascalCase filename matches the class name and the naming convention established in Story 1.1. + +### Import Boundary — HARD RULE + +`src/foundry/` may only import: +- `src/contracts/` +- `src/utils/` + +No imports from `src/core/`, `src/ui/`, `src/notifications/`, or `src/presets/`. ESLint will catch violations automatically (wired in Story 1.1). + +### FoundryAdapter Class Pattern + +```js +// src/foundry/FoundryAdapter.js + +// OQ-1 Spike Result: — FoundryVTT v14.x — +// + +/** + * Sole gateway to game.* APIs. Feature-detects WebRTC availability. + * @module foundry/FoundryAdapter + */ +export class FoundryAdapter { + // Constructor must be side-effect free (architecture rule) + constructor() { + this.webrtc = null; // Set after probeCapability() call + } + + /** + * Probes game.webrtc for track-disabling capability. + * @param {unknown} gameWebrtc - game.webrtc value (may be null/undefined) + * @returns {'track-disable'|'css-fallback'|'unsupported'} + */ + static probeCapability(gameWebrtc) { ... } +} +``` + +### module.js Integration (Minimal for This Story) + +The `module.js` `Hooks.once('init')` stub currently has no body. This story adds only the setting registration. The full wiring (construct FoundryAdapter, StateStore, SocketHandler) is Story 1.3. + +```js +// module.js — update Hooks.once('init') to: +Hooks.once("init", () => { + console.log("[ScryingPool] init — module loading"); + game.settings.register("video-view-manager", "webrtcMode", { + scope: "world", + config: false, + type: String, + default: "css-fallback", + }); +}); +``` + +### WebRTC Probe Investigation Guide + +If you cannot run a live FoundryVTT instance, investigate the API surface this way: + +1. **Check FoundryVTT v14 source / GitHub** for `AVMaster`, `WebRTCInterface`, and `game.webrtc` type definitions +2. **Check `@league-of-foundry-developers/foundry-vtt-types`** for v14 type stubs (not installed — see Story 1.1 deviation, use `src/types/foundry-globals.d.ts`) +3. **Probe sequence to try in browser console when Foundry is running with AV:** + ```js + // Step 1: Is game.webrtc available? + console.log(game.webrtc); + // Step 2: Can we get connections? + console.log(typeof game.webrtc?.getConnection); + // Step 3: Can we reach RTCPeerConnection? + const conn = game.webrtc?.getConnection?.(game.users.players[0]?.id); + console.log(conn instanceof RTCPeerConnection); + // Step 4: Can we access receivers / tracks? + const receivers = conn?.getReceivers?.(); + console.log(receivers, receivers?.[0]?.track); + // Step 5: Can we disable a track? + const track = receivers?.[0]?.track; + if (track) { track.enabled = false; console.log('track-disable confirmed'); } + ``` +4. Document outcome as a comment at the top of `FoundryAdapter.js` + +### probeCapability() Logic Pattern + +```js +static probeCapability(gameWebrtc) { + if (!gameWebrtc) return "unsupported"; + if (typeof gameWebrtc.getConnection !== "function") return "css-fallback"; + // Try to detect track access without a real peer (may need to run with AV active) + // If getConnection signature exists AND returns RTCPeerConnection-like → "track-disable" + // Conservative default if structural probe is inconclusive → "css-fallback" + return "css-fallback"; // UPDATE AFTER SPIKE +} +``` + +### disableTrack / enableTrack Pattern (if track-disable confirmed) + +```js +// webrtc surface (only if probeCapability returns "track-disable") +this.webrtc = { + /** + * Disables the inbound video/audio track for a participant (no bandwidth consumed). + * @param {string} userId + */ + disableTrack(userId) { + try { + const conn = game.webrtc.getConnection(userId); + const track = conn?.getReceivers()?.[0]?.track; + if (track) track.enabled = false; + else console.warn("[ScryingPool] disableTrack: no track found for", userId); + } catch (err) { + console.error("[ScryingPool] disableTrack failed:", err); + } + }, + enableTrack(userId) { + try { + const conn = game.webrtc.getConnection(userId); + const track = conn?.getReceivers()?.[0]?.track; + if (track) track.enabled = true; + else console.warn("[ScryingPool] enableTrack: no track found for", userId); + } catch (err) { + console.error("[ScryingPool] enableTrack failed:", err); + } + }, +}; +``` + +### Test File Pattern + +Follow established test patterns from Story 1.1: + +```js +// tests/unit/foundry/FoundryAdapter.test.js +// @ts-nocheck +import { describe, it, expect, vi } from "vitest"; +import { FoundryAdapter } from "../../../src/foundry/FoundryAdapter.js"; +import { createFoundryAdapterMock } from "../../helpers/foundryAdapterMock.js"; + +describe("FoundryAdapter.probeCapability", () => { + it("returns unsupported when gameWebrtc is null", () => { + expect(FoundryAdapter.probeCapability(null)).toBe("unsupported"); + }); + // ... +}); +``` + +Use `vi.fn()` for all stubs. No ad-hoc stubs — extend `createFoundryAdapterMock` as needed. + +### Toolchain Deviations from Story 1.1 (Carry Forward) + +- **ESLint flat config:** `eslint.config.js` (NOT `.eslintrc.js` — ESLint 9 dropped legacy config) +- **TypeScript:** `moduleResolution: "bundler"` (NOT `"node16"`) +- **No foundry-vtt-types package** — FoundryVTT globals declared in `src/types/foundry-globals.d.ts` +- **Node:** 24.14.1 / npm 11.4.0 + +### Hard Exit Rule + +**Story 1.3 must not start until this story is merged and `FoundryAdapter.webrtc` interface is frozen.** The outcome comment in `src/foundry/FoundryAdapter.js` and the `scrying-pool.webrtcMode` setting are the gate. + +### Review Findings from Story 1.1 (Context Only — Do Not Fix in This Story) + +The following open review items from Story 1.1 are tracked but NOT in scope for Story 1.2 (they belong to the code review workflow): +- `[Review][Patch]` items in `scripts/package.mjs`, `.gitignore`, `package.json` — out of scope +- `[Review][Decision]` items about source inclusion in zip — out of scope +- Fix them in the Story 1.1 code review pass, not here + +### Project Structure Notes + +**New files this story creates:** +``` +src/foundry/FoundryAdapter.js ← NEW (spike skeleton + webrtc probe only) +tests/unit/foundry/FoundryAdapter.test.js ← NEW +``` + +**Files modified this story:** +``` +module.js ← Add scrying-pool.webrtcMode setting registration +``` + +No other files should be touched. `src/foundry/` directory must be created. + +### References + +- Epics — Story 1.2 ACs: [Source: _bmad-output/planning-artifacts/epics.md#Story 1.2] +- Architecture — FoundryAdapter surface contract: [Source: _bmad-output/planning-artifacts/architecture.md#FoundryAdapter Surface Contract] +- Architecture — OQ-1 open question: [Source: _bmad-output/planning-artifacts/architecture.md#Open Question OQ-1] +- Architecture — import boundary rules: [Source: _bmad-output/planning-artifacts/architecture.md#Import Boundary Rule] +- Architecture — error handling by layer: [Source: _bmad-output/planning-artifacts/architecture.md#Error Handling by Layer] +- Architecture — test patterns: [Source: _bmad-output/planning-artifacts/architecture.md#Test Patterns] +- Architecture — naming patterns: [Source: _bmad-output/planning-artifacts/architecture.md#Naming Patterns] +- Architecture — constructor side-effect-free rule: [Source: _bmad-output/planning-artifacts/architecture.md#Constructor Rule] +- Story 1.1 — toolchain deviations: [Source: _bmad-output/implementation-artifacts/1-1-module-scaffold-cicd-pipeline-and-design-token-system.md#Dev Agent Record] +- Story 1.1 — canonical mock established: [Source: _bmad-output/implementation-artifacts/1-1-module-scaffold-cicd-pipeline-and-design-token-system.md#Dev Notes] + +## Dev Agent Record + +### Agent Model Used + +Claude Sonnet 4.6 (claude-sonnet-4.6) via GitHub Copilot CLI + +### Debug Log References + +- **Task 2.1/2.3 correction:** Story spec had `game.settings.register('video-view-manager', ...)` and `SETTING_WEBRTC_MODE = 'video-view-manager.webrtcMode'`. Architecture convention specifies `scrying-pool.{key}` namespace — corrected to `game.settings.register('scrying-pool', 'webrtcMode', ...)` and `SETTING_WEBRTC_MODE = 'webrtcMode'` + `SETTINGS_NS = 'scrying-pool'`. +- **Task 4.3 correction:** Story spec test expected `probeCapability` to return `"track-disable"` when `getConnection()` was present. Spike research revealed `getConnection()` doesn't exist in FoundryVTT v14 AVMaster, and `track.enabled = false` doesn't stop bandwidth. Test updated to reflect spike outcome: `client.getMediaStreamForUser` present → `"css-fallback"`. + +### Completion Notes List + +- **OQ-1 resolved: `css-fallback`** — FoundryVTT v14 AVMaster has no `getConnection(userId)` method. Remote stream access goes via `game.webrtc.client.getMediaStreamForUser(userId)` (public AVClient abstract API). `track.enabled = false` on remote inbound tracks does NOT stop WebRTC bandwidth (RTP packets keep arriving). CSS/DOM cosmetic hiding is the honest implementation path. `FoundryAdapter.webrtc = null` in production. +- `src/foundry/FoundryAdapter.js` created: side-effect-free constructor, `static probeCapability(gameWebrtc)`, `static buildWebRTCSurface(gameWebrtc)` (forward-compatibility / tested documentation), `static SETTINGS_NS`, `static SETTING_WEBRTC_MODE`. Full OQ-1 spike comment block at file top. +- `module.js` updated: `Hooks.once('init')` now registers `scrying-pool.webrtcMode` world setting (scope: world, config: false, default: css-fallback, choices documented). +- `tests/helpers/foundryAdapterMock.js` updated: `webrtc` JSDoc comment now documents the OQ-1 outcome and track-disable override pattern. +- `tests/unit/foundry/FoundryAdapter.test.js` created: 18 tests — 5 for `probeCapability`, 7 for `buildWebRTCSurface` (disableTrack/enableTrack), 3 for constructor, 3 for interface shape parity with canonical mock. +- Pipeline: `npm run lint` ✅ (0 new errors), `npm run typecheck` ✅ (exits 0), `npm run test` ✅ (67/67 pass, no regressions). + +### File List + +- `src/foundry/FoundryAdapter.js` — CREATED +- `tests/unit/foundry/FoundryAdapter.test.js` — CREATED +- `module.js` — MODIFIED (added scrying-pool.webrtcMode setting registration) +- `tests/helpers/foundryAdapterMock.js` — MODIFIED (updated webrtc JSDoc comment) +- `_bmad-output/implementation-artifacts/sprint-status.yaml` — MODIFIED (status: in-progress → review) diff --git a/_bmad-output/implementation-artifacts/bmad-review-acceptance-auditor-prompt.md b/_bmad-output/implementation-artifacts/bmad-review-acceptance-auditor-prompt.md new file mode 100644 index 0000000..fc7ff8f --- /dev/null +++ b/_bmad-output/implementation-artifacts/bmad-review-acceptance-auditor-prompt.md @@ -0,0 +1,829 @@ +# Acceptance Auditor Review Layer + +## ROLE +You are **Acceptance Auditor** — a compliance checker. You review code changes against the specification and acceptance criteria. You must verify that the implementation matches the spec intent. + +## INPUTS +1. **Diff** (below) +2. **Spec file**: `_bmad-output/implementation-artifacts/1-1-module-scaffold-cicd-pipeline-and-design-token-system.md` +3. **Context docs**: Any documents referenced in the spec's frontmatter `context` field (none in this case) + +## MISSION +Check for: +- **Violations of acceptance criteria** (AC #1, #2, #3... from the spec) +- **Deviations from spec intent** (implementation does something different than described) +- **Missing implementation** (spec says X should exist/behave a certain way, but it's not in the diff) +- **Contradictions** (code violates constraints stated in the spec) + +For each finding, identify: +- Which AC or spec section it violates +- Exact evidence from the diff +- The expected vs actual behavior + +## OUTPUT FORMAT +Output ONLY a Markdown list of findings. No preamble, no summary. Each finding: +```markdown +- **[AC#X / SPEC]** Short title — file:line — what violates which AC + evidence +``` + +## SPEC CONTENT +# Story 1.1: Module Scaffold, CI/CD Pipeline & Design Token System + +Status: review + +## Story + +As a **developer**, +I want a fully configured module scaffold with the complete `--sp-*` design token system, contract files, tooling, and CI, +so that every subsequent story builds on enforced boundaries, verified tooling, and a stable design language. + +## Acceptance Criteria + +1. **Given** the repository is checked out fresh **When** `npm install && npm run lint && npm run typecheck && npm run test` are executed **Then** all commands exit 0 **And** `npm run build` produces `module.zip` containing `module.json`, `scripts/`, `styles/`, `templates/`, `lang/` + +2. **Given** the module is installed in FoundryVTT v14 **When** FoundryVTT loads **Then** the module activates with no console errors and `game.modules.get('video-view-manager').active === true` + +3. **Given** a developer writes an exported function without a JSDoc comment **When** `npm run lint` runs **Then** `eslint` reports a `jsdoc/require-jsdoc` violation + +4. **Given** a source file imports from a restricted layer **When** `npm run lint` runs **Then** `import/no-restricted-paths` reports a boundary violation + +5. **Given** a Gitea push is made **When** the CI workflow runs **Then** lint, typecheck, and test all run; a failing test fails the workflow + +6. **Given** a developer writes module CSS using a Foundry `--color-*`/`--font-*`/`--border-*` token directly inside `.scrying-pool` CSS **When** the linting convention is enforced **Then** a violation is reported — all Foundry tokens must be aliased through `--sp-*` + +7. **Given** a developer renders any participant state **When** they look up the token system in `styles/scrying-pool.less` **Then** all token layers are defined: + - Layer 1: SP semantic aliases (`--sp-surface`, `--sp-border`, `--sp-text-primary`, `--sp-text-secondary`, `--sp-accent`, `--sp-focus`) mapping to Foundry tokens with hardcoded fallbacks + - Layer 2: SP Participant State tokens for all 8 states (`active`, `hidden`, `self-muted`, `offline`, `cam-lost`, `reconnecting`, `never-connected`, `ghost`) + - Layer 3: SP Urgency + Motion tokens (`--sp-urgency-director`, `--sp-urgency-awareness`, `--sp-fade-hide`, `--sp-pulse-reconnecting`, `--sp-shimmer-degraded`, `--sp-toast-delay`) + - **And** the `VisibilityBadge` `:root` exception is documented: badge tokens declared on `:root` because badge mounts outside `.scrying-pool` root + - **And** all animated token usages gated under `@media (prefers-reduced-motion: no-preference)` + +8. **Given** the 4 contract files exist **When** a story imports `src/contracts/visibility-matrix.js` **Then** it exports a canonical shape constant, a factory function (`createVisibilityMatrix()`), and a guard/validator function (`isValidVisibilityMatrix(data)`) **And** the same factory + validator pattern applies to `socket-message.js`, `pending-op.js`, `scene-preset.js` + +## Tasks / Subtasks + +- [x] Task 1: Initialize npm project and install devDependencies (AC: #1) + - [x] 1.1 Run `npm init -y` and configure `package.json` with exact scripts block (see Dev Notes) + - [x] 1.2 Install all devDependencies with pinned versions (see Dev Notes) + - [x] 1.3 Verify `npm install` exits 0 with lock file generated + +- [x] Task 2: Create root config files (AC: #1, #3, #4) + - [x] 2.1 Create `tsconfig.json` with `checkJs`, `strict`, `noEmit`, `ESNext` target, `module: ESNext`, `moduleResolution: node16`, `allowJs: true` + - [x] 2.2 Create `.eslintrc.js` with `jsdoc/require-jsdoc` on all exported symbols and `import/no-restricted-paths` zones for all 6 boundary rules (see Dev Notes) + - [x] 2.3 Create `vitest.config.js` with happy-dom environment, path aliases, coverage config + - [x] 2.4 Create `.gitignore` excluding `dist/`, `node_modules/`, `*.zip` + +- [x] Task 3: Create `module.json` v14 manifest (AC: #2) + - [x] 3.1 Set `id: "video-view-manager"`, title, version from `package.json`, v14 compatibility block + - [x] 3.2 Register `esmodules: ["module.js"]`, `styles: ["dist/styles/scrying-pool.css"]`, `languages: [{ lang: "en", name: "English", path: "lang/en.json" }]` + +- [x] Task 4: Create `scripts/package.mjs` release script (AC: #1) + - [x] 4.1 Read version from `package.json`; write into `module.json` at release time + - [x] 4.2 Produce `module.zip` containing: `module.json`, `module.js`, `dist/`, `lang/`, `templates/`, `src/` + - [x] 4.3 Single version source of truth — `package.json` only; never manually edit `module.json` version field + +- [x] Task 5: Create `module.js` entry point stub (AC: #2) + - [x] 5.1 Empty orchestrator that registers `Hooks.once('init', () => {})` and `Hooks.once('ready', () => {})` + - [x] 5.2 No business logic — wiring only; add `[ScryingPool]` console log to confirm load + - [x] 5.3 Export nothing (module entry point, not a library) + +- [x] Task 6: Create the 4 contract files in `src/contracts/` (AC: #8) + - [x] 6.1 `src/contracts/visibility-matrix.js` — typedef + `createVisibilityMatrix()` + `isValidVisibilityMatrix()` + - [x] 6.2 `src/contracts/socket-message.js` — typedef + `createSocketMessage()` + `isValidSocketMessage()` + - [x] 6.3 `src/contracts/pending-op.js` — typedef + `createPendingOp()` + `isValidPendingOp()` + - [x] 6.4 `src/contracts/scene-preset.js` — typedef + `createScenePreset()` + `isValidScenePreset()` + - [x] 6.5 All validators: reject unknown keys, throw `TypeError` with field name on violation; timestamps as finite non-negative integers; id fields non-empty strings; nullable fields explicit `null` + +- [x] Task 7: Create design token LESS system (AC: #6, #7) + - [x] 7.1 Create `styles/scrying-pool.less` entry point with `@import` references only + - [x] 7.2 Create `styles/tokens/_base.less` — Layer 1 SP semantic aliases (6 tokens + hardcoded fallbacks) + - [x] 7.3 Create `styles/tokens/_states.less` — Layer 2 all 8 participant states + `pending` (9 total); color + icon + shape per state; LESS map `@sp-states` (see Dev Notes for exact map) + - [x] 7.4 Create `styles/tokens/_motion.less` — Layer 3/4 urgency + motion tokens; `--sp-fade-hide`, `--sp-pulse-reconnecting`, `--sp-shimmer-degraded`, `--sp-toast-delay`; all animated tokens under `@media (prefers-reduced-motion: no-preference)` + - [x] 7.5 Create `styles/tokens/_focus.less` — module-wide focus ring; high-contrast outer ring + inner offset + - [x] 7.6 Add `:root` block for `VisibilityBadge` exception with documenting comment + - [x] 7.7 Create `styles/components/` stubs (empty files with scope comment): `_participant-card.less`, `_roster-strip.less`, `_directors-board.less`, `_scene-preset-panel.less`, `_notification.less`, `_player-badge.less`, `_player-panel.less` + - [x] 7.8 Verify `npm run build` produces `dist/styles/scrying-pool.css` + +- [x] Task 8: Create test infrastructure (AC: #1) + - [x] 8.1 Create `tests/helpers/foundryAdapterMock.js` — `createFoundryAdapterMock(overrides={})` canonical mock; covers `settings`, `socket`, `users`, `scenes`, `notifications`, `webrtc: null`, `hooks` + - [x] 8.2 Create `tests/fixtures/socket-payloads.js` — `Object.freeze`'d stub with valid + malformed shapes (missing `opId`, wrong enum, extra keys) + - [x] 8.3 Create `tests/fixtures/visibility-states.js`, `state-store-snapshots.js`, `scene-preset.js`, `pending-op.js`, `foundry-adapter.js` — all `Object.freeze`'d + - [x] 8.4 Create contract test files in `tests/unit/contracts/` for all 4 contracts — test factory happy path, validator rejections + +- [x] Task 9: Create Gitea CI workflow (AC: #5) + - [x] 9.1 Create `.gitea/workflows/ci.yml` — runs on every push; steps: `npm ci`, `npm run lint`, `npm run typecheck`, `npm run test` + - [x] 9.2 Failing test must fail the workflow (non-zero exit propagates) + +- [x] Task 10: Create `lang/en.json` i18n skeleton (AC: #1) + - [x] 10.1 Create with empty-but-valid JSON `{}` — or a top-level `"video-view-manager"` namespace stub + - [x] 10.2 Register in `module.json` as `languages` array entry + +- [x] Task 11: Create `templates/` stubs (AC: #1) + - [x] 11.1 Create minimal stub `.hbs` files: `directors-board.hbs`, `participant-card.hbs`, `roster-strip.hbs`, `scene-preset-panel.hbs`, `player-panel.hbs` + +- [x] Task 12: Verify full pipeline (AC: #1, #3, #4) + - [x] 12.1 `npm run lint` exits 0 on clean code; reports violation on missing JSDoc export + - [x] 12.2 `npm run typecheck` exits 0 + - [x] 12.3 `npm run test` exits 0 (contract tests pass) + - [x] 12.4 `npm run build` produces `dist/styles/scrying-pool.css` + - [x] 12.5 `npm run release` produces `module.zip` + +## Dev Notes + +### npm scripts — exact definitions + +```json +"scripts": { + "build": "lessc styles/scrying-pool.less dist/styles/scrying-pool.css", + "watch": "chokidar 'styles/**/*.less' -c 'lessc styles/scrying-pool.less dist/styles/scrying-pool.css'", + "typecheck": "tsc --noEmit", + "lint": "eslint src/ module.js", + "test": "vitest run", + "test:watch": "vitest", + "release": "node scripts/package.mjs" +} +``` + +### devDependencies — exact pinned versions + +```bash +npm install --save-dev \ + less@4.6.4 \ + chokidar@5.0.0 \ + vitest@2.1.8 \ + happy-dom@20.x \ + typescript@5.9.3 \ + @league-of-foundry-developers/foundry-vtt-types@{pin to a specific commit SHA for v14} \ + @types/node@22.x \ + eslint \ + eslint-plugin-jsdoc \ + eslint-plugin-import +``` + +> ⚠️ `foundry-vtt-types` MUST be pinned to a specific commit SHA, not `#main`. Document the SHA and the Foundry v14 version it targets in a comment in `tsconfig.json` or `package.json`. +> `chokidar` is for LESS watch only — `less --watch` does NOT detect changes in `@import`-ed partials. + +### Import boundary rules (hard — must be wired into `.eslintrc.js`) + +``` +src/core/ → may import: src/contracts/, src/utils/ ONLY +src/foundry/ → may import: src/contracts/, src/utils/ +src/notifications/ → may import: src/core/, src/contracts/, src/utils/ +src/presets/ → may import: src/core/, src/contracts/, src/utils/ +src/ui/ → may import: src/core/, src/contracts/, src/utils/ +src/contracts/ → no internal imports +src/utils/ → no internal imports +module.js → may import: all of src/ +``` + +❌ `src/core/` importing `src/foundry/`, `src/ui/`, `src/notifications/`, or `src/presets/` is a hard violation. +❌ `src/foundry/` importing `src/core/` or `src/ui/` is a hard violation. + +These must be configured as `import/no-restricted-paths` zones — not aspirational documentation. + +### Contract file pattern (apply to all 4) + +```js +/** @typedef {{ opId: string, userId: string, targetState: string, + * previousState: string, issuedAt: number, timeoutId: number|null }} PendingOp */ + +/** + * @param {Partial} input + * @returns {PendingOp} + */ +export function createPendingOp(input) { ... } + +/** + * @param {unknown} dto + * @returns {PendingOp} + * @throws {TypeError} with field name on violation + */ +export function isValidPendingOp(dto) { ... } +``` + +Validator rules: +- Reject unknown keys +- Timestamps: finite, non-negative integer +- Id fields: non-empty string +- Arrays default `[]` +- Nullable fields explicit `null` (never `undefined`) + +### LESS state map — exact shape for `styles/tokens/_states.less` + +```less +@sp-states: { + active: { color: @sp-color-active, icon: '\f06e'; shape: solid; }; + hidden: { color: @sp-color-hidden, icon: '\f070'; shape: dashed; }; + self-muted: { color: @sp-color-muted, icon: '\f131'; shape: solid; }; + offline: { color: @sp-color-offline, icon: '\f00d'; shape: none; }; + cam-lost: { color: @sp-color-warning, icon: '\f03d'; shape: dashed; }; + reconnecting: { color: @sp-color-info, icon: '\f021'; shape: pulse; }; + never-connected: { color: @sp-color-neutral, icon: '\f068'; shape: none; }; + ghost: { color: @sp-color-ghost, icon: '\f2be'; shape: dotted; }; + pending: { color: @sp-color-neutral, icon: '\f110'; shape: pulse; }; +}; +``` + +State colour values (from UX spec): + +| State | Hex | WCAG AA | +|---|---|---| +| `active` | `#4a9e6b` | ✅ | +| `hidden` | `#6b7280` | ⚠️ Icon/shape only | +| `self-muted` | `#8b92a5` | ✅ | +| `offline` | `#4b5563` | ⚠️ Icon/shape only | +| `cam-lost` | `#9ca3af` | ✅ | +| `reconnecting` | `#c8982a` | ✅ | +| `never-connected` | `#374151` | ⚠️ Icon/shape only | +| `ghost` | `#1f2937` | ⚠️ Icon/shape only | + +> ⚠️ States marked "Icon/shape only" MUST NOT appear as text or small-pill foreground — colour is supplementary; icon + shape carry the primary signal (WCAG requirement). + +### Layer 1 SP semantic alias tokens (exact values from UX spec) + +```css +:root { + --sp-surface: var(--sp-theme-surface, var(--color-bg-option, #141618)); + --sp-text-primary: var(--sp-theme-text-primary, var(--color-text-primary, #dde2e8)); + --sp-text-secondary: var(--sp-theme-text-muted, var(--color-text-secondary, #7a8390)); + --sp-accent: var(--sp-theme-accent, var(--color-warm-2, #4a9e6b)); + --sp-focus: var(--sp-theme-focus, var(--color-focus-outline, #63c287)); + --sp-urgency-director: var(--sp-theme-urgency, #c8982a); /* NO Foundry error/warn token */ +} +``` + +> ⚠️ `--sp-urgency-director` MUST NOT inherit Foundry's error/warn colours. A director cue is a deliberate stage direction, not a failure. + +### State token naming — three sub-tokens per state + +Each state provides three CSS custom properties: +``` +--sp-state-{name}-text +--sp-state-{name}-border +--sp-state-{name}-bg +``` + +### VisibilityBadge `:root` exception + +Badge (`PlayerStatusBadge`) is mounted outside the `.scrying-pool` root DOM node, directly onto Foundry's AV tile DOM. Badge state tokens MUST be declared on `:root` so they are available outside the module's root. Add a prominent comment explaining this architectural exception. + +### Naming conventions (enforce across all files) + +- JS files (classes/modules): PascalCase — `StateStore.js`, `FoundryAdapter.js` +- Utility/helper files: camelCase — `uuid.js` +- Contract files: kebab-case — `socket-message.js`, `pending-op.js` +- Test files: `{SourceFile}.test.js` — `StateStore.test.js` +- Named exports only — `export class StateStore {}`, never `export default` +- World settings prefix: `scrying-pool.` — never `video-view-manager.X`, `sp.X`, `vvm.X` +- Socket events prefix: `scrying-pool.` +- CSS prefix: `.sp-` or scoped under `.scrying-pool` +- Console prefix: `[ScryingPool]` on ALL console calls +- Public API returns `null` not `undefined` for "not found" + +### Constructor rule + +```js +// ✅ constructor(adapter) { this._adapter = adapter; } +// init() { this._adapter.hooks.once('ready', () => this._onReady()); } +// ❌ constructor(adapter) { adapter.hooks.once('ready', ...); } +``` +Lifecycle registration belongs in `module.js` (owns `Hooks.once('init')` and `Hooks.once('ready')`). Individual module constructors must be side-effect free. + +### Test pattern — canonical mock + +```js +import { createFoundryAdapterMock } from '../helpers/foundryAdapterMock.js' +const adapter = createFoundryAdapterMock({ settings: { get: () => 'custom' } }) +``` +**No ad-hoc stubs.** All tests use `createFoundryAdapterMock` — the canonical mock factory is established in this story and reused by all subsequent tests. + +### Fixture pattern + +```js +// All fixtures are Object.freeze'd +export const SOCKET_PAYLOADS = Object.freeze({ ... }) +// Include negative/invalid fixtures for every validateX() rejection branch +``` + +### socket-payloads.js fixture shape (stub at this stage) + +Must include at minimum: +- Valid intent payload (`scrying-pool.visibility.set`) +- Valid echo payload (`scrying-pool.visibility.updated`) +- Malformed: missing `opId` +- Malformed: wrong enum value for state +- Malformed: extra unknown keys + +### State precedence (for VisibilityManager stories — document here for context) + +``` +pending > cam-lost > reconnecting > offline > never-connected > self-muted > hidden > ghost > active +``` + +CSS never handles multi-state conflicts — VisibilityManager/RoleRenderer resolve before rendering. + +### module.json v14 manifest required fields + +```json +{ + "id": "video-view-manager", + "title": "Video View Manager (Scrying Pool)", + "version": "0.1.0", + "compatibility": { "minimum": "14", "verified": "14" }, + "esmodules": ["module.js"], + "styles": ["dist/styles/scrying-pool.css"], + "languages": [{ "lang": "en", "name": "English", "path": "lang/en.json" }] +} +``` + +> ⚠️ `version` field in `module.json` is managed by `scripts/package.mjs` at release time. Do NOT edit it manually during development. + +### Project Structure Notes + +This is story 1.1 — the project root is currently empty. Create the full directory structure from scratch: + +``` +video-view-manager/ +├── module.json +├── module.js ← stub (Hooks.once init/ready only) +├── package.json +├── tsconfig.json +├── vitest.config.js +├── .eslintrc.js +├── .gitignore +├── .gitea/workflows/ci.yml +├── scripts/package.mjs +├── src/ +│ ├── contracts/ ← 4 contract files (full implementation) +│ └── utils/uuid.js ← stub (opId generation for PendingOp, later stories) +├── styles/ +│ ├── scrying-pool.less ← @import entry point only +│ ├── tokens/ +│ │ ├── _base.less +│ │ ├── _states.less +│ │ ├── _motion.less +│ │ └── _focus.less +│ └── components/ ← 7 stub LESS files +├── templates/ ← 5 stub .hbs files +├── lang/en.json +└── tests/ + ├── helpers/foundryAdapterMock.js + ├── fixtures/ ← 6 fixture files (all Object.freeze'd) + └── unit/contracts/ ← 4 contract test files +``` + +**No `src/core/`, `src/foundry/`, `src/ui/` files yet** — those are Story 1.2+. Import boundary ESLint rules must still be configured now so they catch violations as soon as those files are created. + +### References + +- Architecture — scaffold decisions: [Source: _bmad-output/planning-artifacts/architecture.md#Starter Template Evaluation] +- Architecture — project structure: [Source: _bmad-output/planning-artifacts/architecture.md#Complete Project Directory Structure] +- Architecture — naming + enforcement: [Source: _bmad-output/planning-artifacts/architecture.md#Naming Patterns] +- Architecture — import boundaries: [Source: _bmad-output/planning-artifacts/architecture.md#Import Boundary Rule] +- Architecture — contract format: [Source: _bmad-output/planning-artifacts/architecture.md#Contracts] +- Architecture — test patterns: [Source: _bmad-output/planning-artifacts/architecture.md#Test Patterns] +- Architecture — LESS/CSS patterns: [Source: _bmad-output/planning-artifacts/architecture.md#LESS / CSS Patterns] +- UX spec — design token layers: [Source: _bmad-output/planning-artifacts/ux-design-specification.md#Layer 1–4] +- UX spec — state colour values: [Source: _bmad-output/planning-artifacts/ux-design-specification.md#state colour table] +- UX spec — CSS scoping discipline: [Source: _bmad-output/planning-artifacts/ux-design-specification.md#Linting convention] +- Epics — Story 1.1 ACs + deliverables: [Source: _bmad-output/planning-artifacts/epics.md#Story 1.1] + +## Dev Agent Record + +### Agent Model Used + +Claude Sonnet 4.6 (claude-sonnet-4.6) via GitHub Copilot CLI + +### Debug Log References + +- ESLint 9.x uses flat config (`eslint.config.js`), not `.eslintrc.js` as story spec states. Used flat config with `@eslint/js`, browser globals, and FoundryVTT globals declared explicitly. +- LESS `*/` in block comments causes ParseError — switched all LESS `/** */` comments to `//` line comments. +- `tsconfig.json`: `moduleResolution: "node16"` requires `module: "Node16"` — changed to `moduleResolution: "bundler"` (TS 5+ only, compatible with ESNext module). +- Contract validators: destructuring `unknown` requires intermediate `Record` cast for TypeScript strict mode. +- `scripts/package.mjs`: `catch(err)` needs `err instanceof Error ? err.message : String(err)` under strict TS. +- Test fixture/helper files with untyped stub functions use `// @ts-nocheck` pragma — deliberate, test infrastructure only. +- `globals` package available transitively via ESLint — used for `globals.browser` in flat config. + +### Completion Notes List + +- ✅ Task 1: `package.json` initialized, `npm install` exits 0; Node 24.14.1 / npm 11.4.0 +- ✅ Task 2: `tsconfig.json` (moduleResolution: bundler), `eslint.config.js` (flat config), `vitest.config.js`, `.gitignore` +- ✅ Task 3: `module.json` v14 manifest with all required fields +- ✅ Task 4: `scripts/package.mjs` release script; produces `module.zip` (14 KB) at v0.1.0 +- ✅ Task 5: `module.js` entry stub with `Hooks.once('init')` and `Hooks.once('ready')` +- ✅ Task 6: All 4 contract files fully implemented with factory + validator; all validators reject unknown keys, throw `TypeError` with field name +- ✅ Task 7: Full LESS token system; `npm run build` → `dist/styles/scrying-pool.css` ✅; VisibilityBadge `:root` exception documented +- ✅ Task 8: 49 unit tests across 4 contract test files; all pass; `tests/helpers/foundryAdapterMock.js` canonical mock established +- ✅ Task 9: `.gitea/workflows/ci.yml` — lint + typecheck + test + build on every push +- ✅ Task 10: `lang/en.json` — valid `{}` skeleton +- ✅ Task 11: 5 Handlebars template stubs created +- ✅ Task 12: Full pipeline — `lint` ✅ `typecheck` ✅ `test` ✅ `build` ✅ `release` ✅ + +**Deviations from story spec:** +- `eslint.config.js` (flat config) used instead of `.eslintrc.js` (ESLint 9 dropped `.eslintrc.*` support) +- `moduleResolution: "bundler"` instead of `"node16"` (node16 requires module: Node16) +- `foundry-vtt-types` not installed — `Hooks` declared via minimal `src/types/foundry-globals.d.ts` instead + +### File List + +- `package.json` +- `package-lock.json` +- `tsconfig.json` +- `eslint.config.js` +- `vitest.config.js` +- `.gitignore` +- `module.json` +- `module.js` +- `scripts/package.mjs` +- `src/types/foundry-globals.d.ts` +- `src/utils/uuid.js` +- `src/contracts/visibility-matrix.js` +- `src/contracts/socket-message.js` +- `src/contracts/pending-op.js` +- `src/contracts/scene-preset.js` +- `styles/scrying-pool.less` +- `styles/tokens/_base.less` +- `styles/tokens/_states.less` +- `styles/tokens/_motion.less` +- `styles/tokens/_focus.less` +- `styles/components/_participant-card.less` +- `styles/components/_roster-strip.less` +- `styles/components/_directors-board.less` +- `styles/components/_scene-preset-panel.less` +- `styles/components/_notification.less` +- `styles/components/_player-badge.less` +- `styles/components/_player-panel.less` +- `lang/en.json` +- `templates/directors-board.hbs` +- `templates/participant-card.hbs` +- `templates/roster-strip.hbs` +- `templates/scene-preset-panel.hbs` +- `templates/player-panel.hbs` +- `.gitea/workflows/ci.yml` +- `tests/helpers/foundryAdapterMock.js` +- `tests/fixtures/socket-payloads.js` +- `tests/fixtures/visibility-states.js` +- `tests/fixtures/state-store-snapshots.js` +- `tests/fixtures/scene-preset.js` +- `tests/fixtures/pending-op.js` +- `tests/fixtures/foundry-adapter.js` +- `tests/unit/contracts/visibility-matrix.test.js` +- `tests/unit/contracts/socket-message.test.js` +- `tests/unit/contracts/pending-op.test.js` +- `tests/unit/contracts/scene-preset.test.js` + +### Change Log + +- 2025-05-21: Story 1.1 implementation complete — full scaffold, CI, LESS token system, contracts, test infrastructure (49 tests, all pass; full pipeline lint/typecheck/test/build/release exits 0)--- DIFF TO REVIEW --- +diff --git a/.gitignore b/.gitignore +new file mode 100644 +index 0000000..c2ccb26 +--- /dev/null ++++ b/.gitignore +@@ -0,0 +1,4 @@ ++dist/ ++node_modules/ ++*.zip ++*.lock +diff --git a/eslint.config.js b/eslint.config.js +new file mode 100644 +index 0000000..4de7904 +--- /dev/null ++++ b/eslint.config.js +@@ -0,0 +1,124 @@ ++import js from "@eslint/js"; ++import jsdoc from "eslint-plugin-jsdoc"; ++import importPlugin from "eslint-plugin-import"; ++import globals from "globals"; ++import { fileURLToPath } from "url"; ++import { dirname } from "path"; ++ ++const __dirname = dirname(fileURLToPath(import.meta.url)); ++ ++export default [ ++ js.configs.recommended, ++ { ++ plugins: { ++ jsdoc, ++ import: importPlugin, ++ }, ++ languageOptions: { ++ globals: { ++ // Browser built-ins (console, setTimeout, etc.) ++ ...globals.browser, ++ // FoundryVTT globals injected at runtime ++ Hooks: "readonly", ++ game: "readonly", ++ ui: "readonly", ++ canvas: "readonly", ++ foundry: "readonly", ++ CONFIG: "readonly", ++ CONST: "readonly", ++ }, ++ }, ++ rules: { ++ // Require JSDoc on all exported symbols ++ "jsdoc/require-jsdoc": [ ++ "error", ++ { ++ publicOnly: true, ++ require: { ++ FunctionDeclaration: true, ++ MethodDefinition: true, ++ ClassDeclaration: true, ++ ArrowFunctionExpression: false, ++ FunctionExpression: false, ++ }, ++ contexts: ["ExportNamedDeclaration > FunctionDeclaration"], ++ }, ++ ], ++ "jsdoc/require-param": "warn", ++ "jsdoc/require-returns": "warn", ++ ++ // Import boundary enforcement ++ "import/no-restricted-paths": [ ++ "error", ++ { ++ zones: [ ++ // src/core/ → may import src/contracts/ and src/utils/ ONLY ++ { ++ target: "./src/core", ++ from: "./src/foundry", ++ message: "src/core/ must not import from src/foundry/", ++ }, ++ { ++ target: "./src/core", ++ from: "./src/ui", ++ message: "src/core/ must not import from src/ui/", ++ }, ++ { ++ target: "./src/core", ++ from: "./src/notifications", ++ message: "src/core/ must not import from src/notifications/", ++ }, ++ { ++ target: "./src/core", ++ from: "./src/presets", ++ message: "src/core/ must not import from src/presets/", ++ }, ++ // src/foundry/ → may import src/contracts/ and src/utils/ ONLY ++ { ++ target: "./src/foundry", ++ from: "./src/core", ++ message: "src/foundry/ must not import from src/core/", ++ }, ++ { ++ target: "./src/foundry", ++ from: "./src/ui", ++ message: "src/foundry/ must not import from src/ui/", ++ }, ++ { ++ target: "./src/foundry", ++ from: "./src/notifications", ++ message: "src/foundry/ must not import from src/notifications/", ++ }, ++ { ++ target: "./src/foundry", ++ from: "./src/presets", ++ message: "src/foundry/ must not import from src/presets/", ++ }, ++ // src/contracts/ → no internal imports ++ { ++ target: "./src/contracts", ++ from: "./src", ++ message: "src/contracts/ must not import from other src/ modules", ++ }, ++ // src/utils/ → no internal imports ++ { ++ target: "./src/utils", ++ from: "./src", ++ message: "src/utils/ must not import from other src/ modules", ++ }, ++ ], ++ }, ++ ], ++ }, ++ }, ++ { ++ files: ["tests/**/*.js"], ++ rules: { ++ // Relax JSDoc requirement for test files ++ "jsdoc/require-jsdoc": "off", ++ }, ++ }, ++ { ++ ignores: ["dist/", "node_modules/", "*.zip"], ++ }, ++]; +diff --git a/module.js b/module.js +new file mode 100644 +index 0000000..a3ad2d7 +--- /dev/null ++++ b/module.js +@@ -0,0 +1,24 @@ ++/** ++ * module.js — Entry point and wiring diagram for Video View Manager (Scrying Pool). ++ * ++ * This file is the wiring diagram ONLY. It imports all modules, constructs them ++ * with injected dependencies, and holds NO business logic. ++ * ++ * Initialisation order: ++ * Hooks.once('init') → register world settings → construct FoundryAdapter ++ * → StateStore → SocketHandler (queue+drain) ++ * Hooks.once('ready') → VisibilityManager → SocketHandler.setReady() ++ * → NotificationBus → RoleRenderer → RosterStrip ++ * → DirectorsBoard (lazy, GM only) ++ */ ++ ++Hooks.once("init", () => { ++ console.log("[ScryingPool] init — module loading"); ++ // Story 1.3+: register world settings, construct FoundryAdapter, StateStore, SocketHandler ++}); ++ ++Hooks.once("ready", () => { ++ console.log("[ScryingPool] ready — module active"); ++ // Story 1.3+: construct VisibilityManager, NotificationBus, RoleRenderer, RosterStrip ++ // Story 1.5+: register DirectorsBoard (lazy, GM only) ++}); +diff --git a/module.json b/module.json +new file mode 100644 +index 0000000..e1cffdc +--- /dev/null ++++ b/module.json +@@ -0,0 +1,29 @@ ++{ ++ "id": "video-view-manager", ++ "title": "Video View Manager (Scrying Pool)", ++ "version": "0.1.0", ++ "description": "GM camera visibility control for FoundryVTT v14 — hide, show, and manage participant feeds in real time.", ++ "authors": [ ++ { ++ "name": "Morr" ++ } ++ ], ++ "compatibility": { ++ "minimum": "14", ++ "verified": "14" ++ }, ++ "esmodules": [ ++ "module.js" ++ ], ++ "styles": [ ++ "dist/styles/scrying-pool.css" ++ ], ++ "languages": [ ++ { ++ "lang": "en", ++ "name": "English", ++ "path": "lang/en.json" ++ } ++ ], ++ "flags": {} ++} +diff --git a/package.json b/package.json +new file mode 100644 +index 0000000..fab7015 +--- /dev/null ++++ b/package.json +@@ -0,0 +1,26 @@ ++{ ++ "name": "video-view-manager", ++ "version": "0.1.0", ++ "description": "FoundryVTT v14 module — Scrying Pool camera visibility control", ++ "type": "module", ++ "scripts": { ++ "build": "lessc styles/scrying-pool.less dist/styles/scrying-pool.css", ++ "watch": "chokidar 'styles/**/*.less' -c 'lessc styles/scrying-pool.less dist/styles/scrying-pool.css'", ++ "typecheck": "tsc --noEmit", ++ "lint": "eslint src/ module.js", ++ "test": "vitest run", ++ "test:watch": "vitest", ++ "release": "node scripts/package.mjs" ++ }, ++ "devDependencies": { ++ "@types/node": "^22.0.0", ++ "chokidar": "5.0.0", ++ "eslint": "^9.0.0", ++ "eslint-plugin-import": "^2.31.0", ++ "eslint-plugin-jsdoc": "^50.0.0", ++ "happy-dom": "^20.0.0", ++ "less": "4.6.4", ++ "typescript": "5.9.3", ++ "vitest": "2.1.8" ++ } ++} +diff --git a/scripts/package.mjs b/scripts/package.mjs +new file mode 100644 +index 0000000..9a7300e +--- /dev/null ++++ b/scripts/package.mjs +@@ -0,0 +1,60 @@ ++/** ++ * Release script — produces module.zip. ++ * ++ * Single version source of truth: reads version from package.json, ++ * writes it into module.json, then zips all release artefacts. ++ * ++ * Usage: node scripts/package.mjs ++ */ ++ ++import { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; ++import { resolve, dirname } from "path"; ++import { fileURLToPath } from "url"; ++import { createGzip } from "zlib"; ++import { exec } from "child_process"; ++import { promisify } from "util"; ++ ++const execAsync = promisify(exec); ++const __dirname = dirname(fileURLToPath(import.meta.url)); ++const ROOT = resolve(__dirname, ".."); ++ ++// Read version from package.json (single source of truth) ++const pkg = JSON.parse(readFileSync(resolve(ROOT, "package.json"), "utf8")); ++const { version } = pkg; ++ ++// Write version into module.json ++const moduleJsonPath = resolve(ROOT, "module.json"); ++const moduleJson = JSON.parse(readFileSync(moduleJsonPath, "utf8")); ++moduleJson.version = version; ++writeFileSync(moduleJsonPath, JSON.stringify(moduleJson, null, 2) + "\n", "utf8"); ++console.log(`[ScryingPool] module.json version set to ${version}`); ++ ++// Ensure dist/ exists (build should have run first) ++if (!existsSync(resolve(ROOT, "dist"))) { ++ console.error("[ScryingPool] dist/ not found — run npm run build first"); ++ process.exit(1); ++} ++ ++// Files and directories to include in module.zip ++const INCLUDE = [ ++ "module.json", ++ "module.js", ++ "lang/", ++ "templates/", ++ "dist/", ++ "src/", ++]; ++ ++// Build zip using system zip command ++const targets = INCLUDE.filter((f) => existsSync(resolve(ROOT, f))); ++const zipArgs = targets.map((t) => `"${t}"`).join(" "); ++const zipCmd = `cd "${ROOT}" && zip -r module.zip ${zipArgs}`; ++ ++console.log("[ScryingPool] Creating module.zip..."); ++try { ++ await execAsync(zipCmd); ++ console.log(`[ScryingPool] module.zip created (v${version})`); ++} catch (err) { ++ console.error("[ScryingPool] zip failed:", err instanceof Error ? err.message : String(err)); ++ process.exit(1); ++} +diff --git a/tsconfig.json b/tsconfig.json +new file mode 100644 +index 0000000..7d64af8 +--- /dev/null ++++ b/tsconfig.json +@@ -0,0 +1,14 @@ ++{ ++ "compilerOptions": { ++ "checkJs": true, ++ "strict": true, ++ "noEmit": true, ++ "target": "ESNext", ++ "module": "ESNext", ++ "moduleResolution": "bundler", ++ "allowJs": true, ++ "lib": ["ESNext", "DOM"] ++ }, ++ "include": ["src/**/*.js", "src/**/*.d.ts", "module.js", "scripts/**/*.mjs", "tests/**/*.js"], ++ "exclude": ["node_modules", "dist"] ++} +diff --git a/vitest.config.js b/vitest.config.js +new file mode 100644 +index 0000000..c18efc0 +--- /dev/null ++++ b/vitest.config.js +@@ -0,0 +1,23 @@ ++import { defineConfig } from "vitest/config"; ++ ++export default defineConfig({ ++ test: { ++ environment: "happy-dom", ++ globals: false, ++ include: ["tests/**/*.test.js"], ++ coverage: { ++ provider: "v8", ++ reporter: ["text", "lcov"], ++ include: ["src/**/*.js"], ++ exclude: ["src/contracts/**"], ++ }, ++ }, ++ resolve: { ++ alias: { ++ "@src": "/src", ++ "@contracts": "/src/contracts", ++ "@utils": "/src/utils", ++ "@tests": "/tests", ++ }, ++ }, ++}); diff --git a/_bmad-output/implementation-artifacts/bmad-review-blind-hunter-prompt.md b/_bmad-output/implementation-artifacts/bmad-review-blind-hunter-prompt.md new file mode 100644 index 0000000..6936298 --- /dev/null +++ b/_bmad-output/implementation-artifacts/bmad-review-blind-hunter-prompt.md @@ -0,0 +1,377 @@ +# Blind Hunter Review Layer + +## ROLE +You are **Blind Hunter** — an adversarial code reviewer. You have NO access to the project, spec, or any context. You only see the diff below. + +## MISSION +Find problems. Be ruthless. Assume nothing is intentional. Look for: +- **Security vulnerabilities** (injection, XSS, path traversal, hardcoded secrets) +- **Bugs** (logical errors, race conditions, null dereferences) +- **Performance issues** (N+1 queries, unnecessary computations, memory leaks) +- **Anti-patterns** (god objects, circular dependencies, mutable globals) +- **Code smells** (duplicate code, long methods, magic numbers) +- **Best practice violations** (error handling, input validation, coding standards) +- **Anything suspicious** (unusual patterns, odd dependencies, weird configurations) + +## OUTPUT FORMAT +Output ONLY a Markdown list of findings. No preamble, no summary. Each finding: +```markdown +- **[SEVERITY]** Short title — file:line — evidence/quote from diff +``` +Severity: CRITICAL, HIGH, MEDIUM, LOW, INFO + +## DIFF TO REVIEW + +```diff +diff --git a/.gitignore b/.gitignore +new file mode 100644 +index 0000000..c2ccb26 +--- /dev/null ++++ b/.gitignore +@@ -0,0 +1,4 @@ ++dist/ ++node_modules/ ++*.zip ++*.lock +diff --git a/eslint.config.js b/eslint.config.js +new file mode 100644 +index 0000000..4de7904 +--- /dev/null ++++ b/eslint.config.js +@@ -0,0 +1,124 @@ ++import js from "@eslint/js"; ++import jsdoc from "eslint-plugin-jsdoc"; ++import importPlugin from "eslint-plugin-import"; ++import globals from "globals"; ++import { fileURLToPath } from "url"; ++import { dirname } from "path"; ++ ++const __dirname = dirname(fileURLToPath(import.meta.url)); ++ ++export default [ ++ js.configs.recommended, ++ { ++ plugins: { ++ jsdoc, ++ import: importPlugin, ++ }, ++ languageOptions: { ++ globals: { ++ // Browser built-ins (console, setTimeout, etc.) ++ ...globals.browser, ++ // FoundryVTT globals injected at runtime ++ Hooks: "readonly", ++ game: "readonly", ++ ui: "readonly", ++ canvas: "readonly", ++ foundry: "readonly", ++ CONFIG: "readonly", ++ CONST: "readonly", ++ }, ++ }, ++ rules: { ++ // Require JSDoc on all exported symbols ++ "jsdoc/require-jsdoc": [ ++ "error", ++ { ++ publicOnly: true, ++ require: { ++ FunctionDeclaration: true, ++ MethodDefinition: true, ++ ClassDeclaration: true, ++ ArrowFunctionExpression: false, ++ FunctionExpression: false, ++ }, ++ contexts: ["ExportNamedDeclaration > FunctionDeclaration"], ++ }, ++ ], ++ "jsdoc/require-param": "warn", ++ "jsdoc/require-returns": "warn", ++ ++ // Import boundary enforcement ++ "import/no-restricted-paths": [ ++ "error", ++ { ++ zones: [ ++ // src/core/ → may import src/contracts/ and src/utils/ ONLY ++ { ++ target: "./src/core", ++ from: "./src/foundry", ++ message: "src/core/ must not import from src/foundry/", ++ }, ++ { ++ target: "./src/core", ++ from: "./src/ui", ++ message: "src/core/ must not import from src/ui/", ++ }, ++ { ++ target: "./src/core", ++ from: "./src/notifications", ++ message: "src/core/ must not import from src/notifications/", ++ }, ++ { ++ target: "./src/core", ++ from: "./src/presets", ++ message: "src/core/ must not import from src/presets/", ++ }, ++ // src/foundry/ → may import src/contracts/ and src/utils/ ONLY ++ { ++ target: "./src/foundry", ++ from: "./src/core", ++ message: "src/foundry/ must not import from src/core/", ++ }, ++ { ++ target: "./src/foundry", ++ from: "./src/ui", ++ message: "src/foundry/ must not import from src/ui/", ++ }, ++ { ++ target: "./src/foundry", ++ from: "./src/notifications", ++ message: "src/foundry/ must not import from src/notifications/", ++ }, ++ { ++ target: "./src/foundry", ++ from: "./src/presets", ++ message: "src/foundry/ must not import from src/presets/", ++ }, ++ // src/contracts/ → no internal imports ++ { ++ target: "./src/contracts", ++ from: "./src", ++ message: "src/contracts/ must not import from other src/ modules", ++ }, ++ // src/utils/ → no internal imports ++ { ++ target: "./src/utils", ++ from: "./src", ++ message: "src/utils/ must not import from other src/ modules", ++ }, ++ ], ++ }, ++ ], ++ }, ++ }, ++ { ++ files: ["tests/**/*.js"], ++ rules: { ++ // Relax JSDoc requirement for test files ++ "jsdoc/require-jsdoc": "off", ++ }, ++ }, ++ { ++ ignores: ["dist/", "node_modules/", "*.zip"], ++ }, ++]; +diff --git a/module.js b/module.js +new file mode 100644 +index 0000000..a3ad2d7 +--- /dev/null ++++ b/module.js +@@ -0,0 +1,24 @@ ++/** ++ * module.js — Entry point and wiring diagram for Video View Manager (Scrying Pool). ++ * ++ * This file is the wiring diagram ONLY. It imports all modules, constructs them ++ * with injected dependencies, and holds NO business logic. ++ * ++ * Initialisation order: ++ * Hooks.once('init') → register world settings → construct FoundryAdapter ++ * → StateStore → SocketHandler (queue+drain) ++ * Hooks.once('ready') → VisibilityManager → SocketHandler.setReady() ++ * → NotificationBus → RoleRenderer → RosterStrip ++ * → DirectorsBoard (lazy, GM only) ++ */ ++ ++Hooks.once("init", () => { ++ console.log("[ScryingPool] init — module loading"); ++ // Story 1.3+: register world settings, construct FoundryAdapter, StateStore, SocketHandler ++}); ++ ++Hooks.once("ready", () => { ++ console.log("[ScryingPool] ready — module active"); ++ // Story 1.3+: construct VisibilityManager, NotificationBus, RoleRenderer, RosterStrip ++ // Story 1.5+: register DirectorsBoard (lazy, GM only) ++}); +diff --git a/module.json b/module.json +new file mode 100644 +index 0000000..e1cffdc +--- /dev/null ++++ b/module.json +@@ -0,0 +1,29 @@ ++{ ++ "id": "video-view-manager", ++ "title": "Video View Manager (Scrying Pool)", ++ "version": "0.1.0", ++ "description": "GM camera visibility control for FoundryVTT v14 — hide, show, and manage participant feeds in real time.", ++ "authors": [ ++ { ++ "name": "Morr" ++ } ++ ], ++ "compatibility": { ++ "minimum": "14", ++ "verified": "14" ++ }, ++ "esmodules": [ ++ "module.js" ++ ], ++ "styles": [ ++ "dist/styles/scrying-pool.css" ++ ], ++ "languages": [ ++ { ++ "lang": "en", ++ "name": "English", ++ "path": "lang/en.json" ++ } ++ ], ++ "flags": {} ++} +diff --git a/package.json b/package.json +new file mode 100644 +index 0000000..fab7015 +--- /dev/null ++++ b/package.json +@@ -0,0 +1,26 @@ ++{ ++ "name": "video-view-manager", ++ "version": "0.1.0", ++ "description": "FoundryVTT v14 module — Scrying Pool camera visibility control", ++ "type": "module", ++ "scripts": { ++ "build": "lessc styles/scrying-pool.less dist/styles/scrying-pool.css", ++ "watch": "chokidar 'styles/**/*.less' -c 'lessc styles/scrying-pool.less dist/styles/scrying-pool.css'", ++ "typecheck": "tsc --noEmit", ++ "lint": "eslint src/ module.js", ++ "test": "vitest run", ++ "test:watch": "vitest", ++ "release": "node scripts/package.mjs" ++ }, ++ "devDependencies": { ++ "@types/node": "^22.0.0", ++ "chokidar": "5.0.0", ++ "eslint": "^9.0.0", ++ "eslint-plugin-import": "^2.31.0", ++ "eslint-plugin-jsdoc": "^50.0.0", ++ "happy-dom": "^20.0.0", ++ "less": "4.6.4", ++ "typescript": "5.9.3", ++ "vitest": "2.1.8" ++ } ++} +diff --git a/scripts/package.mjs b/scripts/package.mjs +new file mode 100644 +index 0000000..9a7300e +--- /dev/null ++++ b/scripts/package.mjs +@@ -0,0 +1,60 @@ ++/** ++ * Release script — produces module.zip. ++ * ++ * Single version source of truth: reads version from package.json, ++ * writes it into module.json, then zips all release artefacts. ++ * ++ * Usage: node scripts/package.mjs ++ */ ++ ++import { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; ++import { resolve, dirname } from "path"; ++import { fileURLToPath } from "url"; ++import { createGzip } from "zlib"; ++import { exec } from "child_process"; ++import { promisify } from "util"; ++ ++const execAsync = promisify(exec); ++const __dirname = dirname(fileURLToPath(import.meta.url)); ++const ROOT = resolve(__dirname, ".."); ++ ++// Read version from package.json (single source of truth) ++const pkg = JSON.parse(readFileSync(resolve(ROOT, "package.json"), "utf8")); ++const { version } = pkg; ++ ++// Write version into module.json ++const moduleJsonPath = resolve(ROOT, "module.json"); ++const moduleJson = JSON.parse(readFileSync(moduleJsonPath, "utf8")); ++moduleJson.version = version; ++writeFileSync(moduleJsonPath, JSON.stringify(moduleJson, null, 2) + "\n", "utf8"); ++console.log(`[ScryingPool] module.json version set to ${version}`); ++ ++// Ensure dist/ exists (build should have run first) ++if (!existsSync(resolve(ROOT, "dist"))) { ++ console.error("[ScryingPool] dist/ not found — run npm run build first"); ++ process.exit(1); ++} ++ ++// Files and directories to include in module.zip ++const INCLUDE = [ ++ "module.json", ++ "module.js", ++ "lang/", ++ "templates/", ++ "dist/", ++ "src/", ++]; ++ ++// Build zip using system zip command ++const targets = INCLUDE.filter((f) => existsSync(resolve(ROOT, f))); ++const zipArgs = targets.map((t) => `"${t}"`).join(" "); ++const zipCmd = `cd "${ROOT}" && zip -r module.zip ${zipArgs}`; ++ ++console.log("[ScryingPool] Creating module.zip..."); ++try { ++ await execAsync(zipCmd); ++ console.log(`[ScryingPool] module.zip created (v${version})`); ++} catch (err) { ++ console.error("[ScryingPool] zip failed:", err instanceof Error ? err.message : String(err)); ++ process.exit(1); ++} +diff --git a/tsconfig.json b/tsconfig.json +new file mode 100644 +index 0000000..7d64af8 +--- /dev/null ++++ b/tsconfig.json +@@ -0,0 +1,14 @@ ++{ ++ "compilerOptions": { ++ "checkJs": true, ++ "strict": true, ++ "noEmit": true, ++ "target": "ESNext", ++ "module": "ESNext", ++ "moduleResolution": "bundler", ++ "allowJs": true, ++ "lib": ["ESNext", "DOM"] ++ }, ++ "include": ["src/**/*.js", "src/**/*.d.ts", "module.js", "scripts/**/*.mjs", "tests/**/*.js"], ++ "exclude": ["node_modules", "dist"] ++} +diff --git a/vitest.config.js b/vitest.config.js +new file mode 100644 +index 0000000..c18efc0 +--- /dev/null ++++ b/vitest.config.js +@@ -0,0 +1,23 @@ ++import { defineConfig } from "vitest/config"; ++ ++export default defineConfig({ ++ test: { ++ environment: "happy-dom", ++ globals: false, ++ include: ["tests/**/*.test.js"], ++ coverage: { ++ provider: "v8", ++ reporter: ["text", "lcov"], ++ include: ["src/**/*.js"], ++ exclude: ["src/contracts/**"], ++ }, ++ }, ++ resolve: { ++ alias: { ++ "@src": "/src", ++ "@contracts": "/src/contracts", ++ "@utils": "/src/utils", ++ "@tests": "/tests", ++ }, ++ }, ++}); diff --git a/_bmad-output/implementation-artifacts/bmad-review-edge-case-hunter-prompt.md b/_bmad-output/implementation-artifacts/bmad-review-edge-case-hunter-prompt.md new file mode 100644 index 0000000..a594eaa --- /dev/null +++ b/_bmad-output/implementation-artifacts/bmad-review-edge-case-hunter-prompt.md @@ -0,0 +1,381 @@ +# Edge Case Hunter Review Layer + +## ROLE +You are **Edge Case Hunter** — a meticulous reviewer focused on boundary conditions and unusual scenarios. You have read access to the project files but ONLY for understanding context. Your primary input is the diff below. + +## MISSION +Walk every branching path and boundary condition. Look for: +- **Unchecked assumptions** (what if this is null/undefined/empty/zero?) +- **Off-by-one errors** (loop boundaries, array indices, string slicing) +- **Type coercion issues** (== vs ===, truthy/falsy confusion) +- **Concurrency problems** (race conditions, async/await mishandling) +- **Edge input values** (empty strings, very long strings, special characters, unicode) +- **State transitions** (what happens after error? after retry? after timeout?) +- **Error handling gaps** (unhandled exceptions, missing error cases) +- **API contract violations** (return types, parameter validation, side effects) + +## OUTPUT FORMAT +Output ONLY a Markdown list of findings. No preamble, no summary. Each finding: +```markdown +- **[SEVERITY]** Short title — file:line — edge case description + evidence +``` +Severity: CRITICAL, HIGH, MEDIUM, LOW, INFO + +## PROJECT ROOT +/home/morr/work/foundryvtt/video-view-manager + +## DIFF TO REVIEW + +```diff +diff --git a/.gitignore b/.gitignore +new file mode 100644 +index 0000000..c2ccb26 +--- /dev/null ++++ b/.gitignore +@@ -0,0 +1,4 @@ ++dist/ ++node_modules/ ++*.zip ++*.lock +diff --git a/eslint.config.js b/eslint.config.js +new file mode 100644 +index 0000000..4de7904 +--- /dev/null ++++ b/eslint.config.js +@@ -0,0 +1,124 @@ ++import js from "@eslint/js"; ++import jsdoc from "eslint-plugin-jsdoc"; ++import importPlugin from "eslint-plugin-import"; ++import globals from "globals"; ++import { fileURLToPath } from "url"; ++import { dirname } from "path"; ++ ++const __dirname = dirname(fileURLToPath(import.meta.url)); ++ ++export default [ ++ js.configs.recommended, ++ { ++ plugins: { ++ jsdoc, ++ import: importPlugin, ++ }, ++ languageOptions: { ++ globals: { ++ // Browser built-ins (console, setTimeout, etc.) ++ ...globals.browser, ++ // FoundryVTT globals injected at runtime ++ Hooks: "readonly", ++ game: "readonly", ++ ui: "readonly", ++ canvas: "readonly", ++ foundry: "readonly", ++ CONFIG: "readonly", ++ CONST: "readonly", ++ }, ++ }, ++ rules: { ++ // Require JSDoc on all exported symbols ++ "jsdoc/require-jsdoc": [ ++ "error", ++ { ++ publicOnly: true, ++ require: { ++ FunctionDeclaration: true, ++ MethodDefinition: true, ++ ClassDeclaration: true, ++ ArrowFunctionExpression: false, ++ FunctionExpression: false, ++ }, ++ contexts: ["ExportNamedDeclaration > FunctionDeclaration"], ++ }, ++ ], ++ "jsdoc/require-param": "warn", ++ "jsdoc/require-returns": "warn", ++ ++ // Import boundary enforcement ++ "import/no-restricted-paths": [ ++ "error", ++ { ++ zones: [ ++ // src/core/ → may import src/contracts/ and src/utils/ ONLY ++ { ++ target: "./src/core", ++ from: "./src/foundry", ++ message: "src/core/ must not import from src/foundry/", ++ }, ++ { ++ target: "./src/core", ++ from: "./src/ui", ++ message: "src/core/ must not import from src/ui/", ++ }, ++ { ++ target: "./src/core", ++ from: "./src/notifications", ++ message: "src/core/ must not import from src/notifications/", ++ }, ++ { ++ target: "./src/core", ++ from: "./src/presets", ++ message: "src/core/ must not import from src/presets/", ++ }, ++ // src/foundry/ → may import src/contracts/ and src/utils/ ONLY ++ { ++ target: "./src/foundry", ++ from: "./src/core", ++ message: "src/foundry/ must not import from src/core/", ++ }, ++ { ++ target: "./src/foundry", ++ from: "./src/ui", ++ message: "src/foundry/ must not import from src/ui/", ++ }, ++ { ++ target: "./src/foundry", ++ from: "./src/notifications", ++ message: "src/foundry/ must not import from src/notifications/", ++ }, ++ { ++ target: "./src/foundry", ++ from: "./src/presets", ++ message: "src/foundry/ must not import from src/presets/", ++ }, ++ // src/contracts/ → no internal imports ++ { ++ target: "./src/contracts", ++ from: "./src", ++ message: "src/contracts/ must not import from other src/ modules", ++ }, ++ // src/utils/ → no internal imports ++ { ++ target: "./src/utils", ++ from: "./src", ++ message: "src/utils/ must not import from other src/ modules", ++ }, ++ ], ++ }, ++ ], ++ }, ++ }, ++ { ++ files: ["tests/**/*.js"], ++ rules: { ++ // Relax JSDoc requirement for test files ++ "jsdoc/require-jsdoc": "off", ++ }, ++ }, ++ { ++ ignores: ["dist/", "node_modules/", "*.zip"], ++ }, ++]; +diff --git a/module.js b/module.js +new file mode 100644 +index 0000000..a3ad2d7 +--- /dev/null ++++ b/module.js +@@ -0,0 +1,24 @@ ++/** ++ * module.js — Entry point and wiring diagram for Video View Manager (Scrying Pool). ++ * ++ * This file is the wiring diagram ONLY. It imports all modules, constructs them ++ * with injected dependencies, and holds NO business logic. ++ * ++ * Initialisation order: ++ * Hooks.once('init') → register world settings → construct FoundryAdapter ++ * → StateStore → SocketHandler (queue+drain) ++ * Hooks.once('ready') → VisibilityManager → SocketHandler.setReady() ++ * → NotificationBus → RoleRenderer → RosterStrip ++ * → DirectorsBoard (lazy, GM only) ++ */ ++ ++Hooks.once("init", () => { ++ console.log("[ScryingPool] init — module loading"); ++ // Story 1.3+: register world settings, construct FoundryAdapter, StateStore, SocketHandler ++}); ++ ++Hooks.once("ready", () => { ++ console.log("[ScryingPool] ready — module active"); ++ // Story 1.3+: construct VisibilityManager, NotificationBus, RoleRenderer, RosterStrip ++ // Story 1.5+: register DirectorsBoard (lazy, GM only) ++}); +diff --git a/module.json b/module.json +new file mode 100644 +index 0000000..e1cffdc +--- /dev/null ++++ b/module.json +@@ -0,0 +1,29 @@ ++{ ++ "id": "video-view-manager", ++ "title": "Video View Manager (Scrying Pool)", ++ "version": "0.1.0", ++ "description": "GM camera visibility control for FoundryVTT v14 — hide, show, and manage participant feeds in real time.", ++ "authors": [ ++ { ++ "name": "Morr" ++ } ++ ], ++ "compatibility": { ++ "minimum": "14", ++ "verified": "14" ++ }, ++ "esmodules": [ ++ "module.js" ++ ], ++ "styles": [ ++ "dist/styles/scrying-pool.css" ++ ], ++ "languages": [ ++ { ++ "lang": "en", ++ "name": "English", ++ "path": "lang/en.json" ++ } ++ ], ++ "flags": {} ++} +diff --git a/package.json b/package.json +new file mode 100644 +index 0000000..fab7015 +--- /dev/null ++++ b/package.json +@@ -0,0 +1,26 @@ ++{ ++ "name": "video-view-manager", ++ "version": "0.1.0", ++ "description": "FoundryVTT v14 module — Scrying Pool camera visibility control", ++ "type": "module", ++ "scripts": { ++ "build": "lessc styles/scrying-pool.less dist/styles/scrying-pool.css", ++ "watch": "chokidar 'styles/**/*.less' -c 'lessc styles/scrying-pool.less dist/styles/scrying-pool.css'", ++ "typecheck": "tsc --noEmit", ++ "lint": "eslint src/ module.js", ++ "test": "vitest run", ++ "test:watch": "vitest", ++ "release": "node scripts/package.mjs" ++ }, ++ "devDependencies": { ++ "@types/node": "^22.0.0", ++ "chokidar": "5.0.0", ++ "eslint": "^9.0.0", ++ "eslint-plugin-import": "^2.31.0", ++ "eslint-plugin-jsdoc": "^50.0.0", ++ "happy-dom": "^20.0.0", ++ "less": "4.6.4", ++ "typescript": "5.9.3", ++ "vitest": "2.1.8" ++ } ++} +diff --git a/scripts/package.mjs b/scripts/package.mjs +new file mode 100644 +index 0000000..9a7300e +--- /dev/null ++++ b/scripts/package.mjs +@@ -0,0 +1,60 @@ ++/** ++ * Release script — produces module.zip. ++ * ++ * Single version source of truth: reads version from package.json, ++ * writes it into module.json, then zips all release artefacts. ++ * ++ * Usage: node scripts/package.mjs ++ */ ++ ++import { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; ++import { resolve, dirname } from "path"; ++import { fileURLToPath } from "url"; ++import { createGzip } from "zlib"; ++import { exec } from "child_process"; ++import { promisify } from "util"; ++ ++const execAsync = promisify(exec); ++const __dirname = dirname(fileURLToPath(import.meta.url)); ++const ROOT = resolve(__dirname, ".."); ++ ++// Read version from package.json (single source of truth) ++const pkg = JSON.parse(readFileSync(resolve(ROOT, "package.json"), "utf8")); ++const { version } = pkg; ++ ++// Write version into module.json ++const moduleJsonPath = resolve(ROOT, "module.json"); ++const moduleJson = JSON.parse(readFileSync(moduleJsonPath, "utf8")); ++moduleJson.version = version; ++writeFileSync(moduleJsonPath, JSON.stringify(moduleJson, null, 2) + "\n", "utf8"); ++console.log(`[ScryingPool] module.json version set to ${version}`); ++ ++// Ensure dist/ exists (build should have run first) ++if (!existsSync(resolve(ROOT, "dist"))) { ++ console.error("[ScryingPool] dist/ not found — run npm run build first"); ++ process.exit(1); ++} ++ ++// Files and directories to include in module.zip ++const INCLUDE = [ ++ "module.json", ++ "module.js", ++ "lang/", ++ "templates/", ++ "dist/", ++ "src/", ++]; ++ ++// Build zip using system zip command ++const targets = INCLUDE.filter((f) => existsSync(resolve(ROOT, f))); ++const zipArgs = targets.map((t) => `"${t}"`).join(" "); ++const zipCmd = `cd "${ROOT}" && zip -r module.zip ${zipArgs}`; ++ ++console.log("[ScryingPool] Creating module.zip..."); ++try { ++ await execAsync(zipCmd); ++ console.log(`[ScryingPool] module.zip created (v${version})`); ++} catch (err) { ++ console.error("[ScryingPool] zip failed:", err instanceof Error ? err.message : String(err)); ++ process.exit(1); ++} +diff --git a/tsconfig.json b/tsconfig.json +new file mode 100644 +index 0000000..7d64af8 +--- /dev/null ++++ b/tsconfig.json +@@ -0,0 +1,14 @@ ++{ ++ "compilerOptions": { ++ "checkJs": true, ++ "strict": true, ++ "noEmit": true, ++ "target": "ESNext", ++ "module": "ESNext", ++ "moduleResolution": "bundler", ++ "allowJs": true, ++ "lib": ["ESNext", "DOM"] ++ }, ++ "include": ["src/**/*.js", "src/**/*.d.ts", "module.js", "scripts/**/*.mjs", "tests/**/*.js"], ++ "exclude": ["node_modules", "dist"] ++} +diff --git a/vitest.config.js b/vitest.config.js +new file mode 100644 +index 0000000..c18efc0 +--- /dev/null ++++ b/vitest.config.js +@@ -0,0 +1,23 @@ ++import { defineConfig } from "vitest/config"; ++ ++export default defineConfig({ ++ test: { ++ environment: "happy-dom", ++ globals: false, ++ include: ["tests/**/*.test.js"], ++ coverage: { ++ provider: "v8", ++ reporter: ["text", "lcov"], ++ include: ["src/**/*.js"], ++ exclude: ["src/contracts/**"], ++ }, ++ }, ++ resolve: { ++ alias: { ++ "@src": "/src", ++ "@contracts": "/src/contracts", ++ "@utils": "/src/utils", ++ "@tests": "/tests", ++ }, ++ }, ++}); diff --git a/_bmad-output/implementation-artifacts/deferred-work.md b/_bmad-output/implementation-artifacts/deferred-work.md new file mode 100644 index 0000000..f5952dd --- /dev/null +++ b/_bmad-output/implementation-artifacts/deferred-work.md @@ -0,0 +1,23 @@ +# Deferred Work + +## Deferred from: code review of 1-1-module-scaffold-cicd-pipeline-and-design-token-system (2026-05-21) + +- [ ] Vulnerable dependency CVE-2023-43645 — flat-cache-4.0.1 (needs verification) +- [ ] Version conflicts — Multiple versions: make-dir (5.7.2, 6.3.1, 7.8.0), debug (4.4.3, 3.2.7) (npm resolution, pre-existing) + +## Deferred from: Group 6 (Tests) - 2026-05-21 + +- [ ] MAX_PAYLOAD_BYTES size boundary test — Story 1.2+ SocketHandler responsibility (EC-001) +- [ ] Multi-track stream test — Story 1.2+ WebRTC surface (EC-010) +- [ ] Invalid snapshot fixtures (wrong version, null version) — Story 1.3+ StateStore (EC-012) +- [ ] Circular reference in gameWebrtc handling — Story 1.2+ WebRTC probe (EC-020) + +## Deferred from: Group 7 (CI/CD & i18n) - 2026-05-21 + +- [ ] Missing failure notifications — Story 1.2+ (EC-002) +- [ ] Missing build artifact upload — Story 1.2+ (EC-006) +- [ ] No Node.js matrix testing — Story 1.2+ (EC-004) +- [ ] Missing concurrency control — Story 1.2+ (EC-008) +- [ ] Missing i18n schema validation — Story 1.2+ (EC-I18N-003) +- [ ] ubuntu-latest not pinned — Story 1.2+ (EC-003) +- [ ] Missing coverage upload — Story 1.2+ (EC-005) diff --git a/_bmad-output/implementation-artifacts/sprint-status.yaml b/_bmad-output/implementation-artifacts/sprint-status.yaml new file mode 100644 index 0000000..8cf624a --- /dev/null +++ b/_bmad-output/implementation-artifacts/sprint-status.yaml @@ -0,0 +1,73 @@ +# generated: 2026-05-21T01:00:00+02:00 +# last_updated: 2026-05-21T01:00:00+02:00 +# project: video-view-manager +# project_key: NOKEY +# tracking_system: file-system +# story_location: _bmad-output/implementation-artifacts + +# STATUS DEFINITIONS: +# ================== +# Epic Status: +# - backlog: Epic not yet started +# - in-progress: Epic actively being worked on +# - done: All stories in epic completed +# +# Epic Status Transitions: +# - backlog → in-progress: Automatically when first story is created (via create-story) +# - in-progress → done: Manually when all stories reach 'done' status +# +# Story Status: +# - backlog: Story only exists in epic file +# - ready-for-dev: Story file created in stories folder +# - in-progress: Developer actively working on implementation +# - review: Ready for code review (via Dev's code-review workflow) +# - done: Story completed +# +# Retrospective Status: +# - optional: Can be completed but not required +# - done: Retrospective has been completed +# +# WORKFLOW NOTES: +# =============== +# - Epic transitions to 'in-progress' automatically when first story is created +# - Stories can be worked in parallel if team capacity allows +# - Developer typically creates next story after previous one is 'done' to incorporate learnings +# - Dev moves story to 'review', then runs code-review (fresh context, different LLM recommended) + +generated: "2026-05-21T01:00:00+02:00" +last_updated: "2026-05-22T00:00:00+02:00" +project: video-view-manager +project_key: NOKEY +tracking_system: file-system +story_location: _bmad-output/implementation-artifacts + +development_status: + # Epic 1: Core Camera Visibility Control + epic-1: in-progress + 1-1-module-scaffold-cicd-pipeline-and-design-token-system: done + 1-2-webrtc-spike-track-disabling-api-validation: done + 1-3-data-layer-foundryadapter-statestore-and-socket-infrastructure: backlog + 1-4-core-logic-scryingpoolcontroller-and-visibilitymanager: backlog + 1-5-gm-control-ui-scryingpoolstrip-actionpopover-and-av-tile-integration: backlog + 1-6-player-camera-status-badge: backlog + epic-1-retrospective: optional + + # Epic 2: Player Notifications & Director's Board + epic-2: backlog + 2-1-notificationbus-and-notification-verbosity: backlog + 2-2-directors-board-core-layout-and-participant-toggle: backlog + 2-3-directors-board-bulk-actions-spotlight-and-keyboard-shortcuts: backlog + epic-2-retrospective: optional + + # Epic 3: Scene-Aware Camera Automation (Scene Presets) + epic-3: backlog + 3-1-save-and-load-scene-presets: backlog + 3-2-scene-auto-apply-and-confirmationbar: backlog + 3-3-preset-import-and-export: backlog + epic-3-retrospective: optional + + # Epic 4: Player Privacy Panel + epic-4: backlog + 4-1-player-privacy-panel-and-automation-opt-ins: backlog + 4-2-custom-portrait-fallback: backlog + epic-4-retrospective: optional diff --git a/_bmad-output/planning-artifacts/architecture.md b/_bmad-output/planning-artifacts/architecture.md new file mode 100644 index 0000000..bec4615 --- /dev/null +++ b/_bmad-output/planning-artifacts/architecture.md @@ -0,0 +1,1093 @@ +--- +stepsCompleted: [1, 2, 3, 4, 5, 6, 7, 8] +stepsReviewed: { "6": "party-mode" } +workflowType: 'architecture' +lastStep: 8 +status: 'complete' +completedAt: '2026-05-20' +inputDocuments: + - _bmad-output/planning-artifacts/briefs/brief-video-view-manager-2026-05-19/brief.md + - _bmad-output/planning-artifacts/prds/prd-video-view-manager-2026-05-19/prd.md + - _bmad-output/planning-artifacts/ux-design-specification.md +workflowType: 'architecture' +project_name: 'video-view-manager' +user_name: 'Morr' +date: '2026-05-20' +--- + +# Architecture Decision Document: Video View Manager (Scrying Pool) + +_This document builds collaboratively through step-by-step discovery. Sections are appended as we work through each architectural decision together._ + +--- + +## Project Context Analysis + +### Requirements Overview + +**Functional Requirements:** +26 FRs across 5 feature groups implementing the Progressive Enhancement Architecture: + +- **Level 1 — Core Visibility Toggle (FR-1–8):** Right-click GM toggle on Scrying Pool's own roster strip, socket broadcast, world-level persistence, 8 Participant States (`active`, `hidden`, `self-muted`, `offline`, `cam-lost`, `reconnecting`, `never-connected`, `ghost`), visual state indicators, WebRTC track disabling with CSS fallback, Portrait Fallback. +- **Level 2 — Director's Board (FR-9–14):** `ApplicationV2` floating window, seating-chart layout, per-card toggle, bulk Show/Hide with one-step undo, Spotlight with pre-spotlight snapshot, full keyboard navigation. +- **Level 3 — Scene-Aware Presets (FR-15–19):** Save/load/auto-apply Visibility Matrix snapshots linked to FoundryVTT Scene activation, per-scene and global disable toggles, JSON import/export. _(Scope note: Level 3 is in-scope for v1.0 per PRD §6.1. If delivery risk materialises, FR-1–8 is the minimum shippable increment and Level 3 can shift to v1.1 without breaking the architecture — PRD §Cross-Cutting NFRs.)_ +- **Contextual Notifications (FR-20–22):** 3-tier coalesced notification bus above `ui.notifications`, role-differentiated verbosity, persistent self-status badge on own tile. +- **Player Privacy Panel (FR-23–26):** Per-user opt-in for future automation effects (Reaction Cam, HP-Reactive Styling), custom Portrait Fallback. + +**Non-Functional Requirements:** +- State update latency ≤500ms (local network), all-client including Director's Board reflection +- Director's Board renders interactive within 1s for 12 Participants +- Socket payload ≤4KB per Visibility Matrix update +- No blocking of FoundryVTT main render loop (async state application) +- Graceful degradation when `game.webrtc` is unavailable — no errors, all UI hidden or disabled +- Hook chaining required on all `Hooks.on()` registrations; no naked overrides +- No external data transmission; no analytics or telemetry +- WCAG AA: all interactive elements keyboard-navigable with ARIA labels +- CSS scoped to `.scrying-pool` with SP semantic token alias layer over Foundry tokens + +**Scale & Complexity:** +Medium-high for a FoundryVTT module. No infrastructure layer; FoundryVTT handles transport, persistence backbone, and rendering context. Complexity comes from real-time sync, optimistic UI with reconciliation, role-differentiated rendering, and 3-tier progressive UI. + +- Primary domain: Platform-embedded browser module (FoundryVTT v14 ES module) +- Complexity level: Medium-high +- Estimated core modules: 6–8 (`VisibilityManager`, `SocketHandler`, `StateStore`, `DirectorsBoard`, `NotificationBus`, `ScenePresetManager`, `PlayerPrivacyPanel`, `AudienceView/RoleRenderer`) + +--- + +### Technical Constraints & Dependencies + +- **FoundryVTT v14 API only** — `ApplicationV2` for floating windows, `game.settings` for world/client persistence, native socket API for broadcast, `Hooks` system for lifecycle integration, `game.webrtc` / `AVMaster` for optional track disabling (OQ-1 unresolved) +- **No external libraries** — Font Awesome 6 (Foundry-bundled) + Foundry CSS custom properties only; no UI frameworks, no socketlib +- **ES module architecture** — standard FoundryVTT v14 module pattern +- **CSS scoping discipline** — all selectors under `.scrying-pool`; `--sp-*` semantic tokens mapping to Foundry tokens with hardcoded fallbacks; `--color-*` / `--font-*` / `--border-*` Foundry tokens forbidden directly inside `.scrying-pool` +- **Right-click surface** — must target Scrying Pool's own roster strip, NOT core AV tile DOM (WebRTC DOM is not a stable extension surface; `contextmenu` on `