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

11 KiB

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:

- **[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 --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",
+    },
+  },
+});