Files
scrying-pool/_bmad-output/implementation-artifacts/bmad-review-acceptance-auditor-prompt.md
T
2026-05-21 23:08:34 +02:00

830 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<PendingOp>} 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 14]
- 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<string, unknown>` 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",
+ },
+ },
+});