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

11 KiB

Blind Hunter Review Layer

ROLE

You are Blind Hunter — an adversarial code reviewer. You have NO access to the project, spec, or any context. You only see the diff below.

MISSION

Find problems. Be ruthless. Assume nothing is intentional. Look for:

  • Security vulnerabilities (injection, XSS, path traversal, hardcoded secrets)
  • Bugs (logical errors, race conditions, null dereferences)
  • Performance issues (N+1 queries, unnecessary computations, memory leaks)
  • Anti-patterns (god objects, circular dependencies, mutable globals)
  • Code smells (duplicate code, long methods, magic numbers)
  • Best practice violations (error handling, input validation, coding standards)
  • Anything suspicious (unusual patterns, odd dependencies, weird configurations)

OUTPUT FORMAT

Output ONLY a Markdown list of findings. No preamble, no summary. Each finding:

- **[SEVERITY]** Short title — file:line — evidence/quote from diff

Severity: CRITICAL, HIGH, MEDIUM, LOW, INFO

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