diff --git a/web/package.json b/web/package.json
index 7371884..54ac2a4 100644
--- a/web/package.json
+++ b/web/package.json
@@ -6,7 +6,8 @@
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
- "preview": "vite preview"
+ "preview": "vite preview",
+ "smoke": "node scripts/smoke-admin.mjs"
},
"dependencies": {
"pinia": "^4.0.3",
diff --git a/web/scripts/check-declaration-order.mjs b/web/scripts/check-declaration-order.mjs
new file mode 100644
index 0000000..5ae40be
--- /dev/null
+++ b/web/scripts/check-declaration-order.mjs
@@ -0,0 +1,167 @@
+/**
+ * 声明顺序守卫:抓「setup 期就会求值的表达式(含它的间接依赖)引用了后面才声明的 const」这一类 TDZ。
+ *
+ * 背景:AthletesSection.vue 里 `filteredIds = computed(() => filtered.value...)` 引用了
+ * 199 行之后才声明的 `filtered`,而 `watch(filteredIds, ...)` 在注册时会**同步求值一次源** ——
+ * setup 阶段就访问到 TDZ 中的 `filtered`,抛
+ * `ReferenceError: Cannot access 'filtered' before initialization`,整个分区渲染失败、
+ * 页面只剩标签栏。这个缺陷躲过了 vue-tsc 与全部单元测试(没有 jsdom,组件从不真正挂载),
+ * 在 dev_0.1.6 / 0.1.7 / 0.1.8 三个版本里都没被发现,所以单独做一个静态检查。
+ *
+ * 规则(保守,只报确定会炸的):
+ * 1. `watch(...)` / `watchEffect(...)` 是**注册时同步求值**的(Vue 要取初始值);
+ * 2. 从它的第一个实参出发,沿「const 初始化式里引用了哪些 const」这条边做**传递闭包**;
+ * 3. 闭包里任何 const 的声明行落在 watch 调用行**之后**,就是一次必然的 TDZ。
+ * 直接依赖与间接依赖都要查 —— 只查直接依赖会漏掉本次事故(引用的 filteredIds 声明在前面)。
+ *
+ * 只扫 ``
+ const violations = findOrderViolations('Broken.vue', broken)
+ assert.ok(violations.length > 0, '应当报出 watch 求值到后声明的 filtered')
+ assert.equal(violations[0].name, 'filtered')
+ assert.ok(violations[0].declaredAt > violations[0].line, '声明行必须晚于求值行')
+})
+
+test('声明顺序正确时不报', () => {
+ const fixed = ``
+ assert.deepEqual(findOrderViolations('Fixed.vue', fixed), [])
+})
+
+test('函数局部的 const 不算违规(只看脚本块顶层的声明)', () => {
+ const localConst = ``
+ assert.deepEqual(findOrderViolations('Local.vue', localConst), [])
+})
+
+test('整个 web/src 目录当前没有这类写法', () => {
+ const root = new URL('../../', import.meta.url).pathname
+ const violations = scanWebSources(root)
+ assert.deepEqual(
+ violations,
+ [],
+ violations.map((v) => `${v.file}:${v.line} 会求值到 ${v.name}(声明于第 ${v.declaredAt} 行)`).join('\n'),
+ )
+})