From a2ccf410692bf87a5ac72db7ca984f122c12fe9b Mon Sep 17 00:00:00 2001 From: monitorian <83217380+monitorian@users.noreply.github.com> Date: Sat, 9 May 2026 22:10:34 +0900 Subject: [PATCH] Add safe CI checks and hardware safety pattern detection --- .github/workflows/ci.yml | 42 ++++++++++++++++++++++++ package.json | 6 +++- scripts/safety-checks.js | 71 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 scripts/safety-checks.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e26a012 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: CI + +on: + push: + branches: ["main"] + pull_request: + +permissions: + contents: read + +jobs: + checks: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Typecheck + run: npm run typecheck + + - name: Build check + run: npm run build + + - name: Test + run: npm test + + - name: Safety pattern checks + run: npm run safety:check diff --git a/package.json b/package.json index aef0857..1314403 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,11 @@ "description": "Node.js CLI/library for V2H devices over ECHONET Lite", "main": "index.js", "scripts": { - "test": "node --check index.js && node --check cli.js" + "test": "node --check index.js && node --check cli.js", + "lint": "node --check index.js && node --check cli.js && node --check lib/common.js && node --check lib/discover.js && node --check lib/status.js && node --check lib/format.js", + "typecheck": "node --check index.js && node --check cli.js && node --check lib/common.js && node --check lib/discover.js && node --check lib/status.js && node --check lib/format.js && node --check scripts/safety-checks.js", + "build": "npm pack --dry-run", + "safety:check": "node scripts/safety-checks.js" }, "bin": { "v2h": "cli.js" diff --git a/scripts/safety-checks.js b/scripts/safety-checks.js new file mode 100644 index 0000000..9fb3c5f --- /dev/null +++ b/scripts/safety-checks.js @@ -0,0 +1,71 @@ +#!/usr/bin/env node +const fs = require('fs'); +const path = require('path'); + +const ROOT = process.cwd(); +const SCAN_EXT = new Set(['.js', '.cjs', '.mjs']); +const EXCLUDE_DIRS = new Set(['.git', 'node_modules']); + +const ALLOWED_ECHONET_FILES = new Set([ + path.normalize('lib/common.js'), + path.normalize('lib/discover.js'), + path.normalize('lib/status.js'), + path.normalize('cli.js'), +]); + +function walk(dir, files = []) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (EXCLUDE_DIRS.has(entry.name)) continue; + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + walk(full, files); + } else if (entry.isFile() && SCAN_EXT.has(path.extname(entry.name))) { + files.push(full); + } + } + return files; +} + +function lineOf(text, index) { + return text.slice(0, index).split('\n').length; +} + +const findings = []; +for (const file of walk(ROOT)) { + if (path.normalize(path.relative(ROOT, file)) === path.normalize('scripts/safety-checks.js')) continue; + const rel = path.normalize(path.relative(ROOT, file)); + const text = fs.readFileSync(file, 'utf8'); + + const whileTrue = /while\s*\(\s*true\s*\)/g; + const forEver = /for\s*\(\s*;\s*;\s*\)/g; + const lowSetInterval = /setInterval\s*\((?:.|\n|\r)*?,\s*(\d+)\s*\)/g; + const directEchonet = /(require\(['"]echonet-lite['"]\)|from\s+['"]echonet-lite['"]|\bechonet\.sendOPC1\s*\(|\bechonet\.initialize\s*\()/g; + + let m; + while ((m = whileTrue.exec(text))) { + findings.push(`${rel}:${lineOf(text, m.index)} infinite loop pattern: while(true)`); + } + while ((m = forEver.exec(text))) { + findings.push(`${rel}:${lineOf(text, m.index)} infinite loop pattern: for(;;)`); + } + while ((m = lowSetInterval.exec(text))) { + const ms = Number(m[1]); + if (Number.isFinite(ms) && ms < 1000) { + findings.push(`${rel}:${lineOf(text, m.index)} setInterval below 1000ms: ${ms}`); + } + } + + if (!ALLOWED_ECHONET_FILES.has(rel)) { + while ((m = directEchonet.exec(text))) { + findings.push(`${rel}:${lineOf(text, m.index)} direct hardware API pattern outside approved wrappers`); + } + } +} + +if (findings.length) { + console.error('Safety pattern checks failed:\n'); + findings.forEach((f) => console.error(`- ${f}`)); + process.exit(1); +} + +console.log('Safety pattern checks passed.');