382 lines
11 KiB
Markdown
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",
|
|
+ },
|
|
+ },
|
|
+});
|