# 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: ```markdown - **[SEVERITY]** Short title — file:line — evidence/quote from diff ``` Severity: CRITICAL, HIGH, MEDIUM, LOW, INFO ## 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", + }, + }, +});