35 KiB
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
- Diff (below)
- Spec file:
_bmad-output/implementation-artifacts/1-1-module-scaffold-cicd-pipeline-and-design-token-system.md - Context docs: Any documents referenced in the spec's frontmatter
contextfield (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
-
Given the repository is checked out fresh When
npm install && npm run lint && npm run typecheck && npm run testare executed Then all commands exit 0 Andnpm run buildproducesmodule.zipcontainingmodule.json,scripts/,styles/,templates/,lang/ -
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 -
Given a developer writes an exported function without a JSDoc comment When
npm run lintruns Theneslintreports ajsdoc/require-jsdocviolation -
Given a source file imports from a restricted layer When
npm run lintruns Thenimport/no-restricted-pathsreports a boundary violation -
Given a Gitea push is made When the CI workflow runs Then lint, typecheck, and test all run; a failing test fails the workflow
-
Given a developer writes module CSS using a Foundry
--color-*/--font-*/--border-*token directly inside.scrying-poolCSS When the linting convention is enforced Then a violation is reported — all Foundry tokens must be aliased through--sp-* -
Given a developer renders any participant state When they look up the token system in
styles/scrying-pool.lessThen 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:rootexception is documented: badge tokens declared on:rootbecause badge mounts outside.scrying-poolroot - And all animated token usages gated under
@media (prefers-reduced-motion: no-preference)
- Layer 1: SP semantic aliases (
-
Given the 4 contract files exist When a story imports
src/contracts/visibility-matrix.jsThen it exports a canonical shape constant, a factory function (createVisibilityMatrix()), and a guard/validator function (isValidVisibilityMatrix(data)) And the same factory + validator pattern applies tosocket-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 -yand configurepackage.jsonwith exact scripts block (see Dev Notes) - 1.2 Install all devDependencies with pinned versions (see Dev Notes)
- 1.3 Verify
npm installexits 0 with lock file generated
- 1.1 Run
-
Task 2: Create root config files (AC: #1, #3, #4)
- 2.1 Create
tsconfig.jsonwithcheckJs,strict,noEmit,ESNexttarget,module: ESNext,moduleResolution: node16,allowJs: true - 2.2 Create
.eslintrc.jswithjsdoc/require-jsdocon all exported symbols andimport/no-restricted-pathszones for all 6 boundary rules (see Dev Notes) - 2.3 Create
vitest.config.jswith happy-dom environment, path aliases, coverage config - 2.4 Create
.gitignoreexcludingdist/,node_modules/,*.zip
- 2.1 Create
-
Task 3: Create
module.jsonv14 manifest (AC: #2)- 3.1 Set
id: "video-view-manager", title, version frompackage.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" }]
- 3.1 Set
-
Task 4: Create
scripts/package.mjsrelease script (AC: #1)- 4.1 Read version from
package.json; write intomodule.jsonat release time - 4.2 Produce
module.zipcontaining:module.json,module.js,dist/,lang/,templates/,src/ - 4.3 Single version source of truth —
package.jsononly; never manually editmodule.jsonversion field
- 4.1 Read version from
-
Task 5: Create
module.jsentry point stub (AC: #2)- 5.1 Empty orchestrator that registers
Hooks.once('init', () => {})andHooks.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)
- 5.1 Empty orchestrator that registers
-
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
TypeErrorwith field name on violation; timestamps as finite non-negative integers; id fields non-empty strings; nullable fields explicitnull
- 6.1
-
Task 7: Create design token LESS system (AC: #6, #7)
- 7.1 Create
styles/scrying-pool.lessentry point with@importreferences 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
:rootblock forVisibilityBadgeexception 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 buildproducesdist/styles/scrying-pool.css
- 7.1 Create
-
Task 8: Create test infrastructure (AC: #1)
- 8.1 Create
tests/helpers/foundryAdapterMock.js—createFoundryAdapterMock(overrides={})canonical mock; coverssettings,socket,users,scenes,notifications,webrtc: null,hooks - 8.2 Create
tests/fixtures/socket-payloads.js—Object.freeze'd stub with valid + malformed shapes (missingopId, 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— allObject.freeze'd - 8.4 Create contract test files in
tests/unit/contracts/for all 4 contracts — test factory happy path, validator rejections
- 8.1 Create
-
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)
- 9.1 Create
-
Task 10: Create
lang/en.jsoni18n 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.jsonaslanguagesarray entry
- 10.1 Create with empty-but-valid JSON
-
Task 11: Create
templates/stubs (AC: #1)- 11.1 Create minimal stub
.hbsfiles:directors-board.hbs,participant-card.hbs,roster-strip.hbs,scene-preset-panel.hbs,player-panel.hbs
- 11.1 Create minimal stub
-
Task 12: Verify full pipeline (AC: #1, #3, #4)
- 12.1
npm run lintexits 0 on clean code; reports violation on missing JSDoc export - 12.2
npm run typecheckexits 0 - 12.3
npm run testexits 0 (contract tests pass) - 12.4
npm run buildproducesdist/styles/scrying-pool.css - 12.5
npm run releaseproducesmodule.zip
- 12.1
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-typesMUST be pinned to a specific commit SHA, not#main. Document the SHA and the Foundry v14 version it targets in a comment intsconfig.jsonorpackage.json.chokidaris for LESS watch only —less --watchdoes 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(neverundefined)
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-directorMUST 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 {}, neverexport default - World settings prefix:
scrying-pool.— nevervideo-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
nullnotundefinedfor "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" }]
}
⚠️
versionfield inmodule.jsonis managed byscripts/package.mjsat 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.jsas 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"requiresmodule: "Node16"— changed tomoduleResolution: "bundler"(TS 5+ only, compatible with ESNext module).- Contract validators: destructuring
unknownrequires intermediateRecord<string, unknown>cast for TypeScript strict mode. scripts/package.mjs:catch(err)needserr instanceof Error ? err.message : String(err)under strict TS.- Test fixture/helper files with untyped stub functions use
// @ts-nocheckpragma — deliberate, test infrastructure only. globalspackage available transitively via ESLint — used forglobals.browserin flat config.
Completion Notes List
- ✅ Task 1:
package.jsoninitialized,npm installexits 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.jsonv14 manifest with all required fields - ✅ Task 4:
scripts/package.mjsrelease script; producesmodule.zip(14 KB) at v0.1.0 - ✅ Task 5:
module.jsentry stub withHooks.once('init')andHooks.once('ready') - ✅ Task 6: All 4 contract files fully implemented with factory + validator; all validators reject unknown keys, throw
TypeErrorwith field name - ✅ Task 7: Full LESS token system;
npm run build→dist/styles/scrying-pool.css✅; VisibilityBadge:rootexception documented - ✅ Task 8: 49 unit tests across 4 contract test files; all pass;
tests/helpers/foundryAdapterMock.jscanonical 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-typesnot installed —Hooksdeclared via minimalsrc/types/foundry-globals.d.tsinstead
File List
package.jsonpackage-lock.jsontsconfig.jsoneslint.config.jsvitest.config.js.gitignoremodule.jsonmodule.jsscripts/package.mjssrc/types/foundry-globals.d.tssrc/utils/uuid.jssrc/contracts/visibility-matrix.jssrc/contracts/socket-message.jssrc/contracts/pending-op.jssrc/contracts/scene-preset.jsstyles/scrying-pool.lessstyles/tokens/_base.lessstyles/tokens/_states.lessstyles/tokens/_motion.lessstyles/tokens/_focus.lessstyles/components/_participant-card.lessstyles/components/_roster-strip.lessstyles/components/_directors-board.lessstyles/components/_scene-preset-panel.lessstyles/components/_notification.lessstyles/components/_player-badge.lessstyles/components/_player-panel.lesslang/en.jsontemplates/directors-board.hbstemplates/participant-card.hbstemplates/roster-strip.hbstemplates/scene-preset-panel.hbstemplates/player-panel.hbs.gitea/workflows/ci.ymltests/helpers/foundryAdapterMock.jstests/fixtures/socket-payloads.jstests/fixtures/visibility-states.jstests/fixtures/state-store-snapshots.jstests/fixtures/scene-preset.jstests/fixtures/pending-op.jstests/fixtures/foundry-adapter.jstests/unit/contracts/visibility-matrix.test.jstests/unit/contracts/socket-message.test.jstests/unit/contracts/pending-op.test.jstests/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", - },
- }, +});