fix(loop-engine): S1 次序回归测试改 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 两条隔离规则一条都不沾。真正躲不掉的手抄只有六笔配方常量本身(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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
xcs
2026-08-29 15:15:18 +08:00
co-authored by Claude Opus 5
parent defa048270
commit 02e0d1206d
4 changed files with 98 additions and 150 deletions
@@ -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<Record<string, unknown>>, 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<Record<string, readonly unknown[]>>, declared: readonly Declared[]): Record<string, number> {
const out: Record<string, number> = {};
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<Record<string, readonly unknown[]>>, declared: readonly Declared[]): Record<string, string | number> {
const out: Record<string, string | number> = {};
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<string, unknown>, 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<string, unknown>;
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<string, string> = {};
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<Record<string, readonly unknown[]>>, declared: readonly Declared[]): Record<string, number> {
const out: Record<string, number> = {};
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<string, unknown>;
return String(one[field.name] ?? '').trim() === value;
}).length;
}
}
}
return out;
}
/** 抄自 `engines.ts` 的 `distinctReadings`(逐字一致) */
function distinctReadings(mine: Readonly<Record<string, readonly unknown[]>>, declared: readonly Declared[]): Record<string, number> {
const out: Record<string, number> = {};
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<string>();
for (const row of rows) {
const one = (typeof row === 'object' && row !== null ? row : {}) as Record<string, unknown>;
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<string, unknown>) {
const drafts = new Map<string, { domain: string; object: string; key: Record<string, string>; patch: Record<string, unknown> }>();
@@ -237,7 +152,7 @@ function harness(seed: Record<string, unknown>) {
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<string, string>; patch: Record<string, unknown>; skipUnchanged?: boolean; skipExisting?: boolean }) => {
@@ -300,14 +215,14 @@ function seedScenario(): Record<string, unknown> {
/* 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',
+31
View File
@@ -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,这份镜像能不能测得出来"。
+1
View File
@@ -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/*"]
+1
View File
@@ -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'),
},
},