From 87470ad5486089b381663b9256c21671f50b97c7 Mon Sep 17 00:00:00 2001 From: mateaix <7333791@qq.com> Date: Tue, 15 Sep 2026 10:36:52 +0800 Subject: [PATCH] fix(ui): make snowflake precision check portable --- mateclaw-ui/package.json | 6 +- .../scripts/check-snowflake-precision.mjs | 58 +++++++++ .../scripts/check-snowflake-precision.sh | 110 +----------------- 3 files changed, 63 insertions(+), 111 deletions(-) create mode 100644 mateclaw-ui/scripts/check-snowflake-precision.mjs diff --git a/mateclaw-ui/package.json b/mateclaw-ui/package.json index 49fae294..5446c844 100644 --- a/mateclaw-ui/package.json +++ b/mateclaw-ui/package.json @@ -6,10 +6,10 @@ "description": "MateClaw - Personal AI Assistant Web Console", "scripts": { "dev": "vite", - "build": "bash scripts/check-snowflake-precision.sh && node --max-old-space-size=6144 ./node_modules/vue-tsc/bin/vue-tsc.js --noEmit && node --max-old-space-size=6144 ./node_modules/vite/bin/vite.js build", + "build": "node scripts/check-snowflake-precision.mjs && node --max-old-space-size=6144 ./node_modules/vue-tsc/bin/vue-tsc.js --noEmit && node --max-old-space-size=6144 ./node_modules/vite/bin/vite.js build", "preview": "vite preview", - "lint": "eslint src --fix && bash scripts/check-snowflake-precision.sh", - "lint:precision": "bash scripts/check-snowflake-precision.sh", + "lint": "eslint src --fix && node scripts/check-snowflake-precision.mjs", + "lint:precision": "node scripts/check-snowflake-precision.mjs", "test": "vitest run", "test:watch": "vitest" }, diff --git a/mateclaw-ui/scripts/check-snowflake-precision.mjs b/mateclaw-ui/scripts/check-snowflake-precision.mjs new file mode 100644 index 00000000..5b14a09e --- /dev/null +++ b/mateclaw-ui/scripts/check-snowflake-precision.mjs @@ -0,0 +1,58 @@ +#!/usr/bin/env node + +import { readdirSync, readFileSync, statSync } from 'node:fs' +import { dirname, join, relative } from 'node:path' +import { fileURLToPath } from 'node:url' + +const uiRoot = dirname(dirname(fileURLToPath(import.meta.url))) +const sourceRoot = join(uiRoot, 'src') +const allowlist = 'snowflake-precision-ok' + +const checks = [ + ['v-model.number bound to an *Id field', /v-model\.number=".*[Ii]d"?/], + ['Number()/parseInt() on an *Id value', /(Number|parseInt)\(\s*\w*[Ii]d\b/], + ['Number()/parseInt() on an *Id member-access / lookup', /(Number|parseInt)\([^)]*(?:[a-z]Id\b|[Ii]d['"]|\.[Ii]d\b)/], + ['input[type="number"] bound to an *Id field', /]*\btype="number")(?=[^>]*\bv-model(?:\.[^=\s]+)?="[^"]*[Ii]d")/], + ["typeof === 'number' silently drops string IDs", /typeof\s+\S*[Ii]d\b\s*===\s*'number'/], +] + +function filesUnder(directory) { + const files = [] + for (const entry of readdirSync(directory).sort()) { + const path = join(directory, entry) + const stat = statSync(path) + if (stat.isDirectory()) files.push(...filesUnder(path)) + else if (stat.isFile()) files.push(path) + } + return files +} + +let failed = false +for (const [label, pattern] of checks) { + const hits = [] + for (const path of filesUnder(sourceRoot)) { + const content = readFileSync(path) + if (content.includes(0)) continue + const lines = content.toString('utf8').split(/\r?\n/) + lines.forEach((line, index) => { + if (!line.includes(allowlist) && pattern.test(line)) { + hits.push(`${relative(uiRoot, path)}:${index + 1}:${line}`) + } + }) + } + if (hits.length > 0) { + failed = true + console.error(`\x1b[31m✘ ${label}\x1b[0m`) + console.error(hits.join('\n')) + console.error() + } +} + +if (failed) { + console.error('\x1b[31mSnowflake ID precision violations found.\x1b[0m') + console.error('\x1b[33mKeep backend-issued Snowflake IDs as strings throughout the UI.\x1b[0m') + console.error('\x1b[33mFor a bounded non-ID value, append `// snowflake-precision-ok: ` on the same line.\x1b[0m') + process.exit(1) +} + +console.log('\x1b[32m✓ Snowflake ID precision check: clean\x1b[0m') diff --git a/mateclaw-ui/scripts/check-snowflake-precision.sh b/mateclaw-ui/scripts/check-snowflake-precision.sh index c2032ad4..e4aa4a0e 100755 --- a/mateclaw-ui/scripts/check-snowflake-precision.sh +++ b/mateclaw-ui/scripts/check-snowflake-precision.sh @@ -1,111 +1,5 @@ #!/usr/bin/env bash set -euo pipefail -# Snowflake ID precision checker for mateclaw-ui. -# -# Scans the Vue / TypeScript sources for patterns that round-trip a -# backend-issued Snowflake ID through JS Number, silently truncating the -# last digits whenever the value exceeds Number.MAX_SAFE_INTEGER (2^53-1). -# -# Backend-issued Snowflake IDs must remain strings throughout the UI because -# JavaScript Number cannot exactly represent every 64-bit integer. -# -# Run locally from mateclaw-ui: -# bash scripts/check-snowflake-precision.sh -# -# Run in CI: invoked automatically from this package's `lint` and `build` -# scripts. Keeping the checker inside mateclaw-ui also makes those commands -# self-contained in the public repository, where private root scripts are not -# published. -# -# Allowlisting a real exception: -# Append a trailing comment `// snowflake-precision-ok: ` -# on the SAME line as the match. Only do this when the value is provably -# a small bounded integer (e.g. a per-stream sequence counter, a port -# number, a retry budget) — never as a shortcut to silence a real bug. -# -# Exit codes: -# 0 — all checks clean -# 1 — one or more violations found - -UI_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -UI_SRC="${UI_ROOT}/src" - -red() { printf '\033[31m%s\033[0m\n' "$*"; } -green() { printf '\033[32m%s\033[0m\n' "$*"; } -yellow() { printf '\033[33m%s\033[0m\n' "$*"; } - -if [[ ! -d "$UI_SRC" ]]; then - red "Missing UI src directory: $UI_SRC" - exit 1 -fi - -if ! command -v rg >/dev/null 2>&1; then - red "ripgrep (rg) is required. Install with: brew install ripgrep" - exit 1 -fi - -exit_code=0 - -# Run one grep check. Hits carrying the allowlist annotation are skipped. -# -# Args: -# $1 — human-readable label printed when violations are found -# $2 — rg flags ("-n" for fixed-string-ish, "-nP" for PCRE) -# $3 — pattern -run_check() { - local label="$1" flags="$2" pattern="$3" - local hits - # `|| true` so a "no match" exit-1 from rg doesn't trip `set -e`. - hits=$(rg $flags "$pattern" "$UI_SRC" 2>/dev/null | grep -vF 'snowflake-precision-ok' || true) - if [[ -n "$hits" ]]; then - red "✘ $label" - echo "$hits" - echo - exit_code=1 - fi -} - -# (1) v-model.number on a field whose name ends in Id — Vue's .number -# modifier runs looseToNumber on the bound value before write-back. -run_check 'v-model.number bound to an *Id field' \ - '-n' 'v-model\.number=".*[Ii]d"?' - -# (2) Number() / parseInt() / +id explicit coercion of an *Id-named value. -run_check 'Number()/parseInt() on an *Id value' \ - '-nP' '(Number|parseInt)\(\s*\w*[Ii]d\b' - -# (2b) Number()/parseInt() wrapping a member-access or call expression that -# ends in an id — e.g. Number(localStorage.getItem('mc-workspace-id')), -# Number(route.params.agentId), Number(store.currentKB.id). Check (2) only -# catches a bare *Id identifier; these compound forms slip past it but -# truncate just the same. The `\.[Ii]d\b` alternative covers a plain `.id` -# property access (e.g. `.currentKB.id)`) that the camelCase `[a-z]Id` -# and quoted `[Ii]d['"]` alternatives miss. -run_check 'Number()/parseInt() on an *Id member-access / lookup' \ - '-nP' '(Number|parseInt)\([^)]*(?:[a-z]Id\b|[Ii]d['"'"'"]|\.[Ii]d\b)' - -# (3) v-modeled to an *Id field. Vue's vModelText -# runtime auto-applies looseToNumber whenever el.type === 'number', -# even WITHOUT the `.number` modifier, so the input type itself is -# the bug source. -run_check 'input[type="number"] bound to an *Id field' \ - '-nP' 'type="number"[^>]*v-model[^"]*[Ii]d"' - -# (4) `typeof xxxId === 'number'` presence checks. These silently drop the -# value when the backend hands us the string form (ToStringSerializer -# output), masking the real problem as "field unset". -run_check "typeof === 'number' silently drops string IDs" \ - '-nP' "typeof\s+\S*[Ii]d\b\s*===\s*'number'" - -if [[ $exit_code -eq 0 ]]; then - green '✓ Snowflake ID precision check: clean' -else - echo - red 'Snowflake ID precision violations found.' - yellow 'Keep backend-issued Snowflake IDs as strings throughout the UI.' - yellow 'If a hit is a confirmed small-integer exception, append' - yellow '`// snowflake-precision-ok: ` on the same line.' -fi - -exit $exit_code +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec node "${SCRIPT_DIR}/check-snowflake-precision.mjs" "$@"