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

382 lines
11 KiB
Markdown

# Edge Case Hunter Review Layer
## ROLE
You are **Edge Case Hunter** — a meticulous reviewer focused on boundary conditions and unusual scenarios. You have read access to the project files but ONLY for understanding context. Your primary input is the diff below.
## MISSION
Walk every branching path and boundary condition. Look for:
- **Unchecked assumptions** (what if this is null/undefined/empty/zero?)
- **Off-by-one errors** (loop boundaries, array indices, string slicing)
- **Type coercion issues** (== vs ===, truthy/falsy confusion)
- **Concurrency problems** (race conditions, async/await mishandling)
- **Edge input values** (empty strings, very long strings, special characters, unicode)
- **State transitions** (what happens after error? after retry? after timeout?)
- **Error handling gaps** (unhandled exceptions, missing error cases)
- **API contract violations** (return types, parameter validation, side effects)
## OUTPUT FORMAT
Output ONLY a Markdown list of findings. No preamble, no summary. Each finding:
```markdown
- **[SEVERITY]** Short title — file:line — edge case description + evidence
```
Severity: CRITICAL, HIGH, MEDIUM, LOW, INFO
## PROJECT ROOT
/home/morr/work/foundryvtt/video-view-manager
## DIFF TO REVIEW
```diff
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",
+ },
+ },
+});