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

35 KiB
Raw Blame History

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:

- **[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

  • Task 1: Initialize npm project and install devDependencies (AC: #1)

    • 1.1 Run npm init -y and configure package.json with exact scripts block (see Dev Notes)
    • 1.2 Install all devDependencies with pinned versions (see Dev Notes)
    • 1.3 Verify npm install exits 0 with lock file generated
  • Task 2: Create root config files (AC: #1, #3, #4)

    • 2.1 Create tsconfig.json with checkJs, strict, noEmit, ESNext target, module: ESNext, moduleResolution: node16, allowJs: true
    • 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)
    • 2.3 Create vitest.config.js with happy-dom environment, path aliases, coverage config
    • 2.4 Create .gitignore excluding dist/, node_modules/, *.zip
  • Task 3: Create module.json v14 manifest (AC: #2)

    • 3.1 Set id: "video-view-manager", title, version from package.json, v14 compatibility block
    • 3.2 Register esmodules: ["module.js"], styles: ["dist/styles/scrying-pool.css"], languages: [{ lang: "en", name: "English", path: "lang/en.json" }]
  • Task 4: Create scripts/package.mjs release script (AC: #1)

    • 4.1 Read version from package.json; write into module.json at release time
    • 4.2 Produce module.zip containing: module.json, module.js, dist/, lang/, templates/, src/
    • 4.3 Single version source of truth — package.json only; never manually edit module.json version field
  • Task 5: Create module.js entry point stub (AC: #2)

    • 5.1 Empty orchestrator that registers Hooks.once('init', () => {}) and Hooks.once('ready', () => {})
    • 5.2 No business logic — wiring only; add [ScryingPool] console log to confirm load
    • 5.3 Export nothing (module entry point, not a library)
  • Task 6: Create the 4 contract files in src/contracts/ (AC: #8)

    • 6.1 src/contracts/visibility-matrix.js — typedef + createVisibilityMatrix() + isValidVisibilityMatrix()
    • 6.2 src/contracts/socket-message.js — typedef + createSocketMessage() + isValidSocketMessage()
    • 6.3 src/contracts/pending-op.js — typedef + createPendingOp() + isValidPendingOp()
    • 6.4 src/contracts/scene-preset.js — typedef + createScenePreset() + isValidScenePreset()
    • 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
  • Task 7: Create design token LESS system (AC: #6, #7)

    • 7.1 Create styles/scrying-pool.less entry point with @import references only
    • 7.2 Create styles/tokens/_base.less — Layer 1 SP semantic aliases (6 tokens + hardcoded fallbacks)
    • 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)
    • 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)
    • 7.5 Create styles/tokens/_focus.less — module-wide focus ring; high-contrast outer ring + inner offset
    • 7.6 Add :root block for VisibilityBadge exception with documenting comment
    • 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
    • 7.8 Verify npm run build produces dist/styles/scrying-pool.css
  • Task 8: Create test infrastructure (AC: #1)

    • 8.1 Create tests/helpers/foundryAdapterMock.jscreateFoundryAdapterMock(overrides={}) canonical mock; covers settings, socket, users, scenes, notifications, webrtc: null, hooks
    • 8.2 Create tests/fixtures/socket-payloads.jsObject.freeze'd stub with valid + malformed shapes (missing opId, wrong enum, extra keys)
    • 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
    • 8.4 Create contract test files in tests/unit/contracts/ for all 4 contracts — test factory happy path, validator rejections
  • Task 9: Create Gitea CI workflow (AC: #5)

    • 9.1 Create .gitea/workflows/ci.yml — runs on every push; steps: npm ci, npm run lint, npm run typecheck, npm run test
    • 9.2 Failing test must fail the workflow (non-zero exit propagates)
  • Task 10: Create lang/en.json i18n skeleton (AC: #1)

    • 10.1 Create with empty-but-valid JSON {} — or a top-level "video-view-manager" namespace stub
    • 10.2 Register in module.json as languages array entry
  • Task 11: Create templates/ stubs (AC: #1)

    • 11.1 Create minimal stub .hbs files: directors-board.hbs, participant-card.hbs, roster-strip.hbs, scene-preset-panel.hbs, player-panel.hbs
  • Task 12: Verify full pipeline (AC: #1, #3, #4)

    • 12.1 npm run lint exits 0 on clean code; reports violation on missing JSDoc export
    • 12.2 npm run typecheck exits 0
    • 12.3 npm run test exits 0 (contract tests pass)
    • 12.4 npm run build produces dist/styles/scrying-pool.css
    • 12.5 npm run release produces module.zip

Dev Notes

npm scripts — exact definitions

"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

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)

/** @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

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

: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.jsStateStore.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

// ✅ 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

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

// 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

{
  "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 builddist/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",
    
  • },
  • }, +});