# 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)