830 lines
35 KiB
Markdown
830 lines
35 KiB
Markdown
# 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 1–4]
|
||
- UX spec — state colour values: [Source: _bmad-output/planning-artifacts/ux-design-specification.md#state colour table]
|
||
- UX spec — CSS scoping discipline: [Source: _bmad-output/planning-artifacts/ux-design-specification.md#Linting convention]
|
||
- Epics — Story 1.1 ACs + deliverables: [Source: _bmad-output/planning-artifacts/epics.md#Story 1.1]
|
||
|
||
## Dev Agent Record
|
||
|
||
### Agent Model Used
|
||
|
||
Claude Sonnet 4.6 (claude-sonnet-4.6) via GitHub Copilot CLI
|
||
|
||
### Debug Log References
|
||
|
||
- ESLint 9.x uses flat config (`eslint.config.js`), not `.eslintrc.js` as story spec states. Used flat config with `@eslint/js`, browser globals, and FoundryVTT globals declared explicitly.
|
||
- LESS `*/` in block comments causes ParseError — switched all LESS `/** */` comments to `//` line comments.
|
||
- `tsconfig.json`: `moduleResolution: "node16"` requires `module: "Node16"` — changed to `moduleResolution: "bundler"` (TS 5+ only, compatible with ESNext module).
|
||
- Contract validators: destructuring `unknown` requires intermediate `Record<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",
|
||
+ },
|
||
+ },
|
||
+});
|