Skip to content

Commit 3d49342

Browse files
committed
feat(views): 三轨转写行接上低置信墨度标记
1 parent 279fb76 commit 3d49342

2 files changed

Lines changed: 64 additions & 1 deletion

File tree

‎app/src/utils/lowConfidence.test.ts‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
* 类名**字面量**的镜像一致性另由 `motion/env.test.ts` 的 V6 对拍守住(两者分工不重叠)。
1111
* @ai-context 副作用:无(纯函数用例,不读 store、不发请求、不写盘)。
1212
*/
13+
import { dirname, join } from "node:path";
14+
import { fileURLToPath } from "node:url";
1315
import { describe, expect, it } from "vitest";
16+
import { walkSources, relOf, readLines } from "../ui/primitives/sliceScan";
1417
import { lowConfidenceClass } from "./lowConfidence";
1518

1619
describe("lowConfidenceClass", () => {
@@ -24,3 +27,60 @@ describe("lowConfidenceClass", () => {
2427
expect(lowConfidenceClass(undefined)).toBe("");
2528
});
2629
});
30+
31+
/**
32+
* C4.2 的**加宽判据**:`lowConfidenceClass` 的**生产调用点 ≥2 个文件**(批 7 T20 落地第 2 点)。
33+
*
34+
* @ai-context Why 这条判据必须存在:C4.2 逐字「留 + **加宽到 ≥2 生产调用点**」是本批对 R12.4
35+
* 环境层第 ③ 件的**唯一可机器核对的口径**;若只数「有没有人 import」而不数**调用形态**,
36+
* 把调用点删掉只留 import 也能绿(真空判据)。故本判据**两形态并列**(import 形态 ∧ 调用形态),
37+
* 并要求两形态命中的**文件集逐字相等** —— 只删一边必红。
38+
* @ai-context 域口径:`app/src/**` 的 `.ts`/`.tsx` **减** `*.test.*` **减**定义模块自身
39+
* (`utils/lowConfidence.ts`)—— 定义不是调用点;`ui/primitives/**` **不排除**(它是原语层,
40+
* 若真在那里调用,那也是一个真实调用点)。
41+
* @ai-context 读法:经 `sliceScan.readLines` 的 `stripped`(**先剥注释、抹为等长空白**)⇒
42+
* 文件头注释里提到本函数名不产生幻影调用点(B9 第 9 条:整文件扫描器最常被注释误伤)。
43+
* @ai-context 正控 / 负控:正控 = `session-detail/SessionRawView.tsx`(批 6 T28 的**第 1 点**,
44+
* 它掉了说明扫描器坏了,而不是"第 2 点没做");负控 = 一个不存在于全仓的标识符(必须 0 命中)
45+
* ⇒ 两个方向都能红,判据非空转。
46+
*/
47+
describe("C4.2 · lowConfidenceClass 的生产调用点 ≥2(多形态并列 + 正控/负控)", () => {
48+
const SRC = join(dirname(fileURLToPath(import.meta.url)), "..");
49+
const SELF = "utils/lowConfidence.ts";
50+
const DEF_FILE = "utils/lowConfidence.ts";
51+
/** 两形态:① import 说明符形态(`from ".../lowConfidence"`)② 调用形态 `lowConfidenceClass(` */
52+
const IMPORT_FORM = /from\s+["'][^"']*\/lowConfidence["']/g;
53+
const CALL_FORM = /\blowConfidenceClass\s*\(/g;
54+
55+
/** 命中文件集(相对 `app/src` 的正斜杠路径;**先剥注释**) */
56+
const filesMatching = (re: RegExp): string[] => {
57+
const out: string[] = [];
58+
for (const abs of walkSources(SRC)) {
59+
const rel = relOf(SRC, abs);
60+
if (/\.test\.tsx?$/.test(rel) || rel === DEF_FILE || rel === SELF) continue;
61+
const text = readLines(abs).stripped.join("\n");
62+
if (new RegExp(re.source, "g").test(text)) out.push(rel);
63+
}
64+
return out.sort();
65+
};
66+
67+
it("① 调用形态命中 ≥2 个生产文件,且 import 形态命中的文件集与它逐字相等", () => {
68+
const byCall = filesMatching(CALL_FORM);
69+
const byImport = filesMatching(IMPORT_FORM);
70+
expect(
71+
byCall.length,
72+
`C4.2「低置信 ≥2 生产调用点」未达成:调用形态只命中 ${byCall.length} 个文件(${byCall.join(" · ")})`,
73+
).toBeGreaterThanOrEqual(2);
74+
expect(byImport, "只删 import 或只删调用都会在这里红(两形态的文件集必须逐字相等)").toEqual(byCall);
75+
});
76+
77+
it("② 正控(第 1 点仍在)· 负控(不存在的标识符 0 命中)· 定义模块与测试不计入", () => {
78+
const byCall = filesMatching(CALL_FORM);
79+
expect(byCall, "批 6 T28 的第 1 个调用点消失了 ⇒ 先查扫描器,再查代码").toContain(
80+
"components/session-detail/SessionRawView.tsx",
81+
);
82+
expect(byCall, "批 7 T20 的第 2 个调用点不在命中集里").toContain("views/session/SessionTriTrackView.tsx");
83+
expect(filesMatching(/\bzzzNoSuchSymbolZzz\s*\(/), "负控 0 命中 ⇒ 扫描器不是恒真").toEqual([]);
84+
expect(byCall, "定义模块自身不算调用点").not.toContain(DEF_FILE);
85+
});
86+
});

‎app/src/views/session/SessionTriTrackView.tsx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
import type { CSSProperties, ReactElement } from "react";
6464
import { Button, EmptyState, Text } from "../../ui/primitives";
6565
import { fmtMs } from "../../utils/fmt";
66+
import { lowConfidenceClass } from "../../utils/lowConfidence"; // C4.2:低置信第 2 个生产调用点(第 1 点 = SessionRawView)
6667
import type { SessionViewSlot } from "../registry";
6768
import TimeRail from "./TimeRail";
6869
import "./TriTrackAlign.css";
@@ -123,6 +124,7 @@ interface AlignedItem {
123124
readonly meta?: string;
124125
/** 区间尾码(仅画面轨:`– ${fmtMs(last_seen_ms)}`) */
125126
readonly range?: string;
127+
readonly confidence?: number | null; // 转写段置信度(R11.3/C4.2:<0.5 ⇒ 低置信墨度;画面/OCR 轨无此字段)
126128
}
127129

128130
/** 一条轨:`track` 决定 DOM 锚点,`items` 已按 ms 升序(渲染层不再排序) */
@@ -145,6 +147,7 @@ function segmentItem(seg: Detail["segments"][number], index: number): AlignedIte
145147
time: timeOf(seg.start_ms),
146148
text: seg.text,
147149
meta: SOURCE_LABEL[seg.source] ?? seg.source,
150+
confidence: seg.confidence,
148151
};
149152
}
150153

@@ -229,7 +232,7 @@ function TriTrackItem({ item, onSeek, coaligned }: { readonly item: AlignedItem;
229232
{item.range}
230233
</Text>
231234
)}
232-
<Text as="span" size={4} tone="ink-2">
235+
<Text as="span" size={4} tone="ink-2" className={lowConfidenceClass(item.confidence)}>
233236
{item.text}
234237
</Text>
235238
</div>

0 commit comments

Comments
 (0)