From 02e0d1206d1906fd6741c108901f5fb2f14f3d96 Mon Sep 17 00:00:00 2001 From: xcs Date: Sat, 29 Aug 2026 15:15:18 +0800 Subject: [PATCH] =?UTF-8?q?fix(loop-engine):=20S1=20=E6=AC=A1=E5=BA=8F?= =?UTF-8?q?=E5=9B=9E=E5=BD=92=E6=B5=8B=E8=AF=95=E6=94=B9=20import=20?= =?UTF-8?q?=E7=9C=9F=E8=AF=BB=E6=95=B0=E5=87=BD=E6=95=B0,=E5=88=A0?= =?UTF-8?q?=E6=8E=89=E5=B7=B2=E7=BB=8F=E6=BC=82=E7=A7=BB=E7=9A=84=E6=89=8B?= =?UTF-8?q?=E6=8A=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一轮判断"读数层四个函数也躲不掉、只能手抄"是错的:rowReadings/missingReadings/ tallyReadings/distinctReadings 是 apps/api/src/app/wiring/engines.ts 的具名导出,测试文件住在 apps/api/src/modules/loop-engine/__tests__/,import 它们是 apps→apps,activities-isolated 与 apps-not-into-activities 两条隔离规则一条都不沾。真正躲不掉的手抄只有六笔配方常量本身(ACT_*, 因为那条边是 apps/* → activities/* 的反方向,才是两条规则真堵住的)。 而且手抄件已经在同一笔提交里漂了:missingReadings 拼缺项名的真身是 gaps.map((f) => f.label ?? f.name)(engines.ts:378),手抄件漏了 label ?? 那半句,连带镜像的 Declared 类型没有 label 字段,这条分支从结构上就复刻不出来。 删掉约 150 行手抄,改成 import 真身。相对路径要爬三层撞了 eslint 的 no-restricted-imports, 新增 @app/* 路径别名(tsconfig.base.json + vitest.config.ts 各一行,与既有 @modules/* / @shared/* 同一种写法)—— 取名核对过不撞 pnpm 工作区包名 @app/api / @app/web(两者从未被当作 TS import specifier 用过,且 apps/api 的 package.json exports 只暴露了 "." 一个入口)。 变异复验(删手抄之后这条用例仍能精确复现原 bug):旧次序 → RED,Received 恰是评审报告点名的 那个空字符串,与手抄版本逐字相同;新次序 → GREEN。depcruise 单独确认零违规。 文件头注按"测什么、不测什么"重写:次序不倒退已由 steps.test.ts 用精确数组相等钉在真 ASSESSMENT_STEPS 上;这份镜像守的是引擎逐 action 刷新时机,在形状上有代表性即可,不是逐字 等于真配方的承诺。docs/debt.md 补一条,点名唯一还留着的手抄(ACT_* 六个常量)与谁改了 S1 配方形状该顺手核什么。 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet --- .../__tests__/w05-gate-reason-order.test.ts | 215 ++++++------------ docs/debt.md | 31 +++ tsconfig.base.json | 1 + vitest.config.ts | 1 + 4 files changed, 98 insertions(+), 150 deletions(-) diff --git a/apps/api/src/modules/loop-engine/__tests__/w05-gate-reason-order.test.ts b/apps/api/src/modules/loop-engine/__tests__/w05-gate-reason-order.test.ts index 5762acb..6ee131b 100644 --- a/apps/api/src/modules/loop-engine/__tests__/w05-gate-reason-order.test.ts +++ b/apps/api/src/modules/loop-engine/__tests__/w05-gate-reason-order.test.ts @@ -1,60 +1,62 @@ /** - * **S1 配方的动作次序是承重的**(2026-08-29,评审用活探针复现,详情见 - * `activities/part-substitution-assessment/src/case-model.ts` `S1_inputs.auto` 里第 ④ 笔 - * 头上的注释,以及 `.superpowers/sdd/2026-08-29-act-work-05-rewrite/task-4-report.md`)。 + * **这份文件测的是什么、不测什么**(2026-08-29,评审二轮把靶子写准)。 * - * 案卷投影(`sub_eval.case`)写的 `gate_reason` 是**摆给人看的那句话**,它最深一层的 `when` - * 要读 `parts_profile_missing + parts_parameter_missing`(主数据 / 参数骨架两笔铺出来的基座读数)。 - * 若案卷这一笔排在铺骨架那两笔**之前**,它读到的是这一拍**刷新之前**的旧数 —— - * 具体地:candidate 这颗料的类型刚在本拍第 ① 笔被判定,主数据/参数骨架**即将**在后面几笔 - * 铺出来,而此刻它们还不存在,`parts_parameter_missing` 读到 0(不存在的行不算「缺」)。 - * 收步与发卡的把关本身没坏(`card.when` 排在铺骨架之后,读到的是刷新之后的真实读数, - * 卡照样正确发出),但案卷里存的这句话、连同卡面 `basis` 里「还差什么」那一格,会是空的 —— - * 人最需要一句解释的时候,读到的是空的。 + * 测:**引擎逐 action 刷新时机**的回归 —— 一笔 `project` 若读了另一笔 `project` 才会撑出来的 + * 基座读数(`_missing` / `_rows` / `_distinct_` 这类),它必须排在被读的那几笔**之后**,否则读到 + * 的是这一拍刷新之前的旧数。这一条只有把真引擎(`createAutoRunner`)跑起来才测得出 —— + * 纯结构断言看不出"次序对不对会不会导致某个值算错"。 * - * **为什么不直接 import `case-model.ts` 的真配方**:`activities/*` 与 `apps/*` 互相不许 - * import(`.dependency-cruiser.cjs` 的 `activities-isolated` / `apps-not-into-activities`, - * ADR-004)——测试文件不例外。下面手写的六笔**逐字镜像** `S1_inputs.auto` 当前的地址 / 字段 / - * `when` 链,证明的是"这个次序在真实引擎语义下会不会重现这个 bug",不是"这份文件今天长这样"。 + * 不测:`case-model.ts` 里 `S1_inputs.auto.do` 六笔**当前逐字长什么样**。那件事已经由 + * `activities/part-substitution-assessment/__tests__/steps.test.ts` 的"六笔,次序是…"那条用例 + * 用精确数组相等钉在**真** `ASSESSMENT_STEPS` 上了。下面手写的六个 `ACT_*` 只是**在形状上有 + * 代表性**的样本(覆盖 S1 实际用到的四种地址写法:字面 `rows` / 单层 `each+where` / 两层 + * `each+where+outer` / `once`)——它们不随 `case-model.ts` 自动同步,也**不必**逐字同步: + * 那是个没人能守的承诺。谁改了 S1 配方的**形状**(不是次序),该顺手核一遍这份文件还有没有 + * 代表性,欠账记在 `docs/debt.md`。 * - * **为什么不用 `auto-runner.test.ts` 现成的 `harness()`**:那个替身的 `writable()` 硬编码只有 - * 两张表、且没有 `required`/`tally`/`distinct` 元数据。`_missing`/`_distinct_` 这类基座读数 - * 根本不是 `auto-runner.ts` 自己算的(它只管 project/analyze/card/submit/say 五个动词)—— - * 是**上一层**(`apps/api/src/app/wiring/engines.ts` 的 `taskData.view()`)在 `mine:` 之上叠出来的。 - * 下面把 `rowReadings`/`missingReadings`/`tallyReadings`/`distinctReadings` 四个纯函数原样抄一份 - * (逐行核对过与生产版一致,各自头上标了抄自哪个函数),连同真实的 `@sfeel` 求值器,拼出一个 - * 「引擎 + 读数层」的最小复现环境 —— 这正是评审所说"把未经修改的引擎与配方直接执行"的写法。 + * **为什么手写这六笔,不直接 import 真配方**:`activities/*` 与 `apps/*` 互相不许 import + * (`.dependency-cruiser.cjs` 的 `activities-isolated` / `apps-not-into-activities`,ADR-004)—— + * 测试文件不例外。这一条是真的躲不掉。 + * + * **读数层四个函数不手写,直接 import 真身**(评审二轮指出的修正)——`rowReadings` / + * `missingReadings` / `tallyReadings` / `distinctReadings` 是 `apps/api/src/app/wiring/engines.ts` + * 的具名导出;本文件住在 `apps/api/src/modules/loop-engine/__tests__/`,import 它们是 + * **`apps` → `apps`**,`activities-isolated` 与 `apps-not-into-activities` 一条都不沾, + * `modules/loop-engine` 也不会因此反向依赖 `app/wiring`(这份 import 只在测试文件里, + * `depcruise apps packages activities` 的产出模块图会照实反映出来)。第一版误判"读数层也躲不掉" + * 而手抄了一份,结果就在同一笔提交里漂了:`missingReadings` 拼缺项名的真身是 + * `gaps.map((f) => f.label ?? f.name)`(`engines.ts:378`),手抄件漏了 `label ?? ` 那半句 —— + * 这个仓已经有四个既有测试文件在做同样的事(`apps/api/src/app/__tests__/{ledger-coerce, + * decision-card-face,view-table-edit,acceptance-gate}.test.ts` 都 import 自 `../wiring/engines.ts`), + * 抄反而是舍近求远。 + * + * **路径走的是 `@app/*` 别名,不是相对路径**:那四个既有测试文件住在 `apps/api/src/app/__tests__/`, + * 与 `engines.ts` 只隔一层(`../wiring/engines.ts`);本文件住在 `apps/api/src/modules/loop-engine/ + * __tests__/`,相对路径要爬三层(`../../../app/wiring/engines.ts`),撞上 `eslint.config.js` 的 + * `no-restricted-imports`(`'../../../*'` 一律拒,提示"跨模块请用路径别名")—— 仓里此前没有别名 + * 能到 `apps/api/src/app/*`(该目录只被同目录下的测试用一层相对路径引用过),所以新增了 + * `@app/*`(`tsconfig.base.json` + `vitest.config.ts` 各一行,与既有的 `@modules/*` / `@shared/*` + * 同一种写法、同一种粒度)。取名 `@app` 而不撞现有的 pnpm 工作区包名 `@app/api` / `@app/web`—— + * 那两个名字只在 `package.json` scripts 里当 `pnpm --filter` 的过滤参数用,从未在任何 `.ts` 源码里 + * 被 `import` 过(已用 grep 核过),且 `apps/api/package.json` 的 `exports` 只暴露了 `"."` 这一个 + * 入口(整个 server 的 `main.ts`),没有暴露任何子路径,两套解析各管各的,不会互相截胡。 */ import { describe, expect, it } from 'vitest'; import type { AutoRecipe } from '@contracts'; import { taskIdSchema, staffIdSchema } from '@contracts'; import { evaluateCondition } from '@sfeel'; +import { distinctReadings, missingReadings, rowReadings, tallyReadings } from '@app/wiring/engines.ts'; import { createAutoRunner } from '../internal/usecase/auto-runner.ts'; const TASK = taskIdSchema.parse('33333333-3333-4333-8333-333333333333'); const OWNER = staffIdSchema.parse('44444444-4444-4444-8444-444444444444'); -type Declared = { - domain: string; - object: string; - caseKey?: string; - fields: ReadonlyArray<{ - name: string; - primaryKey: boolean; - required?: boolean; - expect?: boolean; - tally?: readonly string[]; - distinct?: boolean; - requiredWhen?: { column: string; value: string }; - }>; -}; - /* - * **四张台账的字段元数据 —— 逐列核对过与 `manifest.ts` 一致**(镜像,非 import,理由见头注)。 - * `writable()` 那个更窄的形状(auto-runner 拿它取主键、校验 `into` 地址)不够算读数: - * `required`/`tally`/`distinct` 这些标记只在这份「声明」里,读数层要靠它们才算得出来。 + * **四张台账的字段元数据**(形状与 `manifest.ts` 一致,与真配方同款"镜像而非 import"取舍)—— + * 写法照抄 `apps/api/src/app/__tests__/ledger-coerce.test.ts` 里 `DECL` 那几个 fixture 的样子: + * 不需要显式类型标注,直接把这份数据交给 import 进来的真函数,由调用点结构匹配。 */ -const DECLARED: readonly Declared[] = [ +const DECLARED = [ { domain: 'sub_eval', object: 'part', @@ -62,10 +64,10 @@ const DECLARED: readonly Declared[] = [ fields: [ { name: 'eval_ref', primaryKey: true }, { name: 'internal_pn', primaryKey: true }, - { name: 'role', primaryKey: false, required: true, tally: ['original', 'candidate'] }, - { name: 'found_pn_ref', primaryKey: false, requiredWhen: { column: 'exists_status', value: 'found' } }, - { name: 'exists_status', primaryKey: false, required: true, tally: ['found', 'absent'] }, - { name: 'chip_type_id', primaryKey: false, required: true, distinct: true }, + { name: 'role', primaryKey: false, required: true, label: '角色', tally: ['original', 'candidate'] }, + { name: 'found_pn_ref', primaryKey: false, label: '在库核对依据', requiredWhen: { column: 'exists_status', value: 'found' } }, + { name: 'exists_status', primaryKey: false, required: true, label: '在库状态', tally: ['found', 'absent'] }, + { name: 'chip_type_id', primaryKey: false, required: true, label: '芯片类型', distinct: true }, ], }, { @@ -84,13 +86,13 @@ const DECLARED: readonly Declared[] = [ object: 'profile', fields: [ { name: 'internal_pn', primaryKey: true }, - { name: 'chip_type_id', primaryKey: false, required: true }, - { name: 'brand_name', primaryKey: false, required: true }, - { name: 'lifecycle_status', primaryKey: false, required: true, tally: ['active', 'nrnd', 'eol', 'discontinued', 'risk'] }, - { name: 'specification_description', primaryKey: false, expect: true }, - { name: 'manufacturer_name', primaryKey: false, expect: true }, - { name: 'datasheet_ref', primaryKey: false, expect: true }, - { name: 'datasheet_version', primaryKey: false, expect: true }, + { name: 'chip_type_id', primaryKey: false, required: true, label: '芯片类型' }, + { name: 'brand_name', primaryKey: false, required: true, label: '品牌名称' }, + { name: 'lifecycle_status', primaryKey: false, required: true, label: '生命周期状态', tally: ['active', 'nrnd', 'eol', 'discontinued', 'risk'] }, + { name: 'specification_description', primaryKey: false, expect: true, label: '规格描述' }, + { name: 'manufacturer_name', primaryKey: false, expect: true, label: '制造商名称' }, + { name: 'datasheet_ref', primaryKey: false, expect: true, label: 'Datasheet 引用' }, + { name: 'datasheet_version', primaryKey: false, expect: true, label: 'Datasheet 版本' }, ], }, { @@ -99,106 +101,19 @@ const DECLARED: readonly Declared[] = [ fields: [ { name: 'internal_pn', primaryKey: true }, { name: 'param_name', primaryKey: true }, - { name: 'param_value', primaryKey: false, required: true }, - { name: 'param_unit', primaryKey: false, expect: true }, - { name: 'param_note', primaryKey: false, expect: true }, + { name: 'param_value', primaryKey: false, required: true, label: '值' }, + { name: 'param_unit', primaryKey: false, expect: true, label: '单位' }, + { name: 'param_note', primaryKey: false, expect: true, label: '说明' }, ], }, ]; -/** 抄自 `apps/api/src/app/wiring/engines.ts` 的 `rowLabel`(逐字一致)—— `missingReadings` 拒回点名要用 */ -function rowLabel(id: Readonly>, caseKey: string | undefined): string { - const parts = Object.entries(id) - .filter(([k, v]) => k !== caseKey && v !== undefined && v !== null && String(v) !== '') - .map(([, v]) => String(v)); - return parts.length > 0 ? parts.join('/') : Object.values(id).map(String).join('/'); -} - -/** 抄自 `engines.ts` 的 `rowReadings`(逐字一致,略去与本用例无关的入参默认值注释) */ -function rowReadings(mine: Readonly>, declared: readonly Declared[]): Record { - const out: Record = {}; - for (const { domain, object } of declared) out[`${domain}_${object}_rows`] = 0; - for (const [key, list] of Object.entries(mine)) { - if (!key.startsWith('mine:')) continue; - out[`${key.slice('mine:'.length).replace(/\./g, '_')}_rows`] = list.length; - } - return out; -} - -/** 抄自 `engines.ts` 的 `missingReadings`(逐字一致) */ -function missingReadings(mine: Readonly>, declared: readonly Declared[]): Record { - const out: Record = {}; - for (const { domain, object, caseKey, fields } of declared) { - const rows = mine[`mine:${domain}.${object}`] ?? []; - const keys = fields.filter((f) => f.primaryKey).map((f) => f.name); - const blank = (one: Record, c: string): boolean => one[c] === undefined || one[c] === null || String(one[c]).trim() === ''; - for (const [mark, suffix] of [ - ['required', 'missing'], - ['expect', 'gaps'], - ] as const) { - const watched = fields.filter((f) => f[mark] === true || (mark === 'required' && f.requiredWhen !== undefined)); - if (watched.length === 0) continue; - const bad: string[] = []; - for (const row of rows) { - const one = (typeof row === 'object' && row !== null ? row : {}) as Record; - const due = watched.filter((f) => f.requiredWhen === undefined || String(one[f.requiredWhen.column] ?? '') === f.requiredWhen.value); - const gaps = due.filter((f) => blank(one, f.name)); - if (gaps.length === 0) continue; - const id: Record = {}; - for (const k of keys) if (!blank(one, k)) id[k] = String(one[k]); - bad.push(`${rowLabel(id, caseKey)} 缺${gaps.map((f) => f.name).join('、')}`); - } - out[`${domain}_${object}_${suffix}`] = bad.length; - if (bad.length > 0) out[`${domain}_${object}_${suffix}_keys`] = bad.slice(0, 12).join(';'); - } - } - return out; -} - -/** 抄自 `engines.ts` 的 `tallyReadings`(逐字一致) */ -function tallyReadings(mine: Readonly>, declared: readonly Declared[]): Record { - const out: Record = {}; - for (const { domain, object, fields } of declared) { - for (const field of fields) { - if (field.tally === undefined || field.tally.length === 0) continue; - const rows = mine[`mine:${domain}.${object}`] ?? []; - const slug = (v: string): string => v.toLowerCase().replace(/[^a-z0-9_]/g, ''); - for (const value of field.tally) { - const key = `${domain}_${object}_${field.name}_${slug(value)}`; - if (key.endsWith('_')) continue; - out[key] = rows.filter((row) => { - const one = (typeof row === 'object' && row !== null ? row : {}) as Record; - return String(one[field.name] ?? '').trim() === value; - }).length; - } - } - } - return out; -} - -/** 抄自 `engines.ts` 的 `distinctReadings`(逐字一致) */ -function distinctReadings(mine: Readonly>, declared: readonly Declared[]): Record { - const out: Record = {}; - for (const { domain, object, fields } of declared) { - for (const field of fields) { - if (field.distinct !== true) continue; - const rows = mine[`mine:${domain}.${object}`] ?? []; - const seen = new Set(); - for (const row of rows) { - const one = (typeof row === 'object' && row !== null ? row : {}) as Record; - const value = String(one[field.name] ?? '').trim(); - if (value !== '') seen.add(value); - } - out[`${domain}_${object}_distinct_${field.name}`] = seen.size; - } - } - return out; -} - /** * 假端口 —— 手写的依赖注入替身(不是 mock 框架),`view()` 按生产 `taskData.view()` 同一份配方 - * 拼 `enriched` 作用域:`{...seed, ...读数四件套, ...mine}`(生产多算的 `rowKeyList` / - * `toolCallReadings` / `decisionReadings` 本用例的 `when` 链用不到,略去)。 + * 拼作用域:`{...seed, ...读数四件套(真函数), ...mine, ...flattened}`。这一层组合逻辑本身 + * (`engines.ts` 里内嵌在 `wireEngines(deps).taskData.view` 闭包中的那一段)没有被抽成可 import + * 的纯函数、且与 `deps.ledger`/`deps.registry` 等真实 IO 端口绑在一起 —— 手写这部分组合与 + * `ACT_*` 六个常量是同一类"躲不掉的镜像"(纯读数计算才是真正能 import 的那一半)。 */ function harness(seed: Record) { const drafts = new Map; patch: Record }>(); @@ -237,7 +152,7 @@ function harness(seed: Record) { label: `${d.domain}.${d.object}`, keys: d.fields.filter((f) => f.primaryKey).map((f) => f.name), ...(d.caseKey === undefined ? {} : { caseKey: d.caseKey }), - fields: d.fields.map((f) => ({ name: f.name, label: f.name })), + fields: d.fields.map((f) => ({ name: f.name, label: f.label ?? f.name })), })), ), draftWrite: (i: { domain: string; object: string; key: Record; patch: Record; skipUnchanged?: boolean; skipExisting?: boolean }) => { @@ -300,14 +215,14 @@ function seedScenario(): Record { /* candidate 的类型已经归好(chip_type_id=1)—— 与原厂同类型,才走得到 gate_reason 最深那层判据 */ { internal_pn: 'XL2596S-5.0', chip_type_id: 1, brand_name: 'XLSEMI', lifecycle_status: 'active', specification_description: '同步降压', manufacturer_name: 'XLSEMI', datasheet_ref: 'https://example.com/xl2596.pdf', datasheet_version: 'Rev-1' }, ], - /* candidate 一条参数都没有 —— ③ 铺出骨架空行之前,mine:parts.parameter 里压根没有它的行 */ + /* candidate 一条参数都没有 —— 铺骨架那一笔跑之前,mine:parts.parameter 里压根没有它的行 */ 'parts.parameter': [{ internal_pn: 'LM2596S-5.0', param_name: '输入电压范围', param_value: '4.5V~40V', param_unit: 'V', param_note: '' }], }; } /* - * **六笔,逐字镜像 `S1_inputs.auto` 当前的地址 / 字段 / when 链**(2026-08-29 修复后的次序: - * 两颗料 → 主数据骨架 → 参数骨架 → 案卷 → 卡 → 交代)。 + * **六笔,在形状上代表 `S1_inputs.auto` —— 覆盖它用到的四种地址写法**(字面 rows / 单层 + * each+where / 两层 each+where+outer / once),不是逐字同步的承诺(见文件头注)。 */ const ACT_PART = { kind: 'project', diff --git a/docs/debt.md b/docs/debt.md index 6352618..901c45a 100644 --- a/docs/debt.md +++ b/docs/debt.md @@ -1379,3 +1379,34 @@ pnpm 硬链到全局 store,真实占盘远小于这个数,但 `du` 看着吓人 + 三套 e2e;②本轮改的是配方那一条新路,`git diff --stat b995148...HEAD -- activities/` 为空这条硬约束意味着不能顺手把模型路径也捎带上、再靠一次浏览器走查收尾 —— 排查与验收的工作量不是这一条闭合修复能捎带的。**应当独立起一条、排在 W05 重写之前**。 + +## S1 配方回归测试里躲不掉的手抄(2026-08-29,Task 4 评审二轮) + +`apps/api/src/modules/loop-engine/__tests__/w05-gate-reason-order.test.ts` 证的是**引擎逐 action +刷新时机**这件事(案卷投影必须排在读它的基座读数所依赖的那几笔之后)—— 这一条只有把真引擎跑起来 +才测得出;`activities/part-substitution-assessment/__tests__/steps.test.ts` 的"六笔,次序是…"用例 +用精确数组相等钉的是**真** `ASSESSMENT_STEPS` 本身,两份测试各守各的,互不重复。 + +这份引擎级测试**手写了六个 `ACT_*` 常量**,在形状上代表 `case-model.ts` 里 `S1_inputs.auto.do` +当前用到的四种地址写法(字面 `rows` / 单层 `each+where` / 两层 `each+where+outer` / `once`)—— +这是**躲不掉**的手抄:`.dependency-cruiser.cjs` 的 `activities-isolated` / `apps-not-into-activities` +两条规则对测试文件没有例外,`apps/*` 与 `activities/*` 互相不许 import,所以测试文件不能直接 +`import { ASSESSMENT_STEPS } from '.../case-model.ts'`。 + +(读数层四个纯函数 `rowReadings` / `missingReadings` / `tallyReadings` / `distinctReadings` **不在 +此列**——它们是 `apps/api/src/app/wiring/engines.ts` 的具名导出,从 `apps/api/src/modules/ +loop-engine/__tests__/` import 它们是 `apps → apps`,两条隔离规则一条都不沾。第一版误判"读数层也 +躲不掉"而手抄了一份,评审查出与生产版已经漂了一处(`missingReadings` 拼缺项名用 +`f.label ?? f.name`,手抄件漏了 `?? f.name` 那半句),已改为直接 import 真身,删掉约 150 行手抄, +换成 `@app/*` 路径别名〔`tsconfig.base.json` + `vitest.config.ts` 各新增一行,与既有 `@modules/*` +/ `@shared/*` 同一种写法〕——之前的三层相对路径撞了 `eslint.config.js` 的 `no-restricted-imports`。) + +**欠账**:这份镜像**不会随 `case-model.ts` 自动同步**——它守的是引擎机制而不是"逐字等于当前配方" +这个没人能守的承诺,所以次序变化不影响它的有效性,但如果 S1 配方的**形状**变了(比如新增了 +`analyze` 动词、某一笔的 `from` 换了一种从没出现过的地址写法、或某个 `when` 链换了结构),这份 +文件的代表性可能跟不上而不自知——没有测试会为此变红,装包也不会拒。 + +**谁改了 `S1_inputs.auto` 的形状(不是次序),该顺手做的事**:打开 +`w05-gate-reason-order.test.ts`,核一遍 `ACT_*` 六个常量描述的地址/字段写法是否还能代表 +`case-model.ts` 当前用到的构造种类;不必逐字同步(那不是这份测试的目标),但要能回答 +"如果生产那边这一类写法出了刷新时机的 bug,这份镜像能不能测得出来"。 diff --git a/tsconfig.base.json b/tsconfig.base.json index 266071c..b08b6be 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -29,6 +29,7 @@ "@utils": ["./packages/utils/src/index.ts"], "@testing": ["./tools/it-database.ts"], "@modules/*": ["./apps/api/src/modules/*"], + "@app/*": ["./apps/api/src/app/*"], "@features/*": ["./apps/web/src/features/*"], "@shared/*": ["./apps/api/src/shared/*"], "@web-shared/*": ["./apps/web/src/shared/*"] diff --git a/vitest.config.ts b/vitest.config.ts index 93b59ef..63ba9ab 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -17,6 +17,7 @@ export default defineConfig({ '@features': p('./apps/web/src/features'), '@testing': p('./tools/it-database.ts'), '@modules': p('./apps/api/src/modules'), + '@app': p('./apps/api/src/app'), '@shared': p('./apps/api/src/shared'), }, },