R7 / 决策 D10。任务书 docs/tasks/TASK-cend-surfaces-claude-alignment-20260916.md。 T7.1 常驻条。任务书 E9 说区间只在盘面标题行,实际读代码:对话区顶部 已有常驻条 RectificationTimeline 且已显示区间与宽度,窄屏 .is-compact 下 也可见。真正只存在于 .rectification-board__clock 的是代表分钟——窄屏盘面 是默认关闭的 overlay,代表分钟就此看不见。所以没有另造第二条(会撞固定条 高这条正确性约束),而是在既有条上补齐缺的两项: 05:07–05:09 · 3 分钟 · 代表分钟 05:08 · 已答 6 题 代表分钟与盘面标题行是同一次 workingRectificationTime(result) 调用,一个 值两处用,不是两条推导路径。已答题数逐字读服务端的 inference_state.answered_probe_count,投影没给就整项不显示,不补「已答 0 题」。 条上不写确认,边界句仍只在交付/采用旁白里出现一次。读数只增宽不增高:条高 在滚动容器外,变高会让贴底读者的内容无声滑出视野。 收窄进度未做,触发让步顺序第 5 条。没有这个字段(服务端只把这句话拼进旁白 正文),且前端自己减会算错——candidate_range 是当前窗口且会放宽(BUG-572), 拿它当「最初」相减会把一次放宽报成一次收窄。缺口与解除条件写进 BLOCKED.md。 T7.2 盘面折叠。实际有 8 个 details(任务书记的五处漏了交界节奏、错标了两块), 按内容归成「候选分钟」(换升时刻 + 次级候选)与「参数与口径」(宫位表、确认门、 本轮技法、两段本命上升、经历与大运对照含双轨一致性、交界节奏)。每块保留原有 渲染条件,engineMeaningToDisplayCopy() 仍是 5 处,逐分钟行的 is-changed / 当前时间 一字未改。共享组件 technique-audit-disclosure.tsx 未动。 顺带修掉一个静默失效的断言:rectification-agentic-entry 用 board.slice(indexOf(A), indexOf(B)),B 改名后 indexOf 返回 -1,slice(i, -1) 会悄悄退化成几乎整份文件,底下四条 doesNotMatch 照样绿。已换成带越界断言的 sliceBetween()。两处改动的断言都在测试里写了原值/新值/原因。 tsc 0 错;lint 0 error / 118 warning(同基线);npm test 3350 → 3360,失败 仍是 31 条无 Docker 用例且与基线逐条一致;next build exit 0,/ 仍 ○ Static; 产物 CSS gzip 39,017 → 38,973 B(−0.11%)。 窄屏 .is-compact 下常驻条的实际可见性无 Chrome 无法验证,已按任务书写成环境 缺口,未标通过;CSS 上已确认无任何规则在该状态下隐藏该条,并写成测试断言。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
177 lines
6.7 KiB
TypeScript
177 lines
6.7 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { createElement } from "react";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
|
|
import {
|
|
changedHouseNumbers,
|
|
diffRectificationBoard,
|
|
groupWindowTransitions,
|
|
houseTableToNorthIndianChart,
|
|
rectificationBoardPeekCopy,
|
|
} from "../src/lib/rectification-board-model.ts";
|
|
import { parseRectificationCandidateResult } from "../src/lib/rectification-candidate-result.ts";
|
|
import type { WindowScanTransition } from "../src/lib/rectification-agentic/v9/refinement-packet.ts";
|
|
import { RectificationBoard } from "../src/components/rectification-board.tsx";
|
|
|
|
const CANDIDATE_ID = "88888888-8888-4888-8888-888888888881";
|
|
const SECOND_CANDIDATE_ID = "88888888-8888-4888-8888-888888888882";
|
|
const THIRD_CANDIDATE_ID = "88888888-8888-4888-8888-888888888883";
|
|
|
|
function transition(layer: WindowScanTransition["layer"], at: string): WindowScanTransition {
|
|
return { layer, at, user_meaning: `${layer} 在 ${at} 发生变化` };
|
|
}
|
|
|
|
function houses(lagna: string, seventh = lagna) {
|
|
return Array.from({ length: 12 }, (_, index) => ({
|
|
house: index + 1,
|
|
sign: index === 6 ? seventh : lagna,
|
|
occupants: index === 0 ? ["太阳"] : [],
|
|
}));
|
|
}
|
|
|
|
function snapshot(input: Readonly<{
|
|
time: string;
|
|
lagna: string;
|
|
seventh?: string;
|
|
transitions?: readonly WindowScanTransition[];
|
|
}>) {
|
|
return parseRectificationCandidateResult({
|
|
resultId: "11111111-1111-4111-8111-111111111111",
|
|
candidates: [
|
|
{ candidateId: CANDIDATE_ID, rank: 1, time: input.time, relativeSupport: 34, tiedMinuteCount: 1 },
|
|
{ candidateId: SECOND_CANDIDATE_ID, rank: 2, time: "05:08", relativeSupport: 33, tiedMinuteCount: 1 },
|
|
{ candidateId: THIRD_CANDIDATE_ID, rank: 3, time: "05:09", relativeSupport: 33, tiedMinuteCount: 1 },
|
|
],
|
|
overallConfidence: "low",
|
|
selectionAllowed: true,
|
|
confirmationAllowed: false,
|
|
representativeTime: input.time,
|
|
selectedTime: null,
|
|
selectionKind: null,
|
|
decisionReceipt: {
|
|
house_table: {
|
|
time: input.time,
|
|
lagna: input.lagna,
|
|
houses: houses(input.lagna, input.seventh ?? input.lagna),
|
|
},
|
|
window_scan: {
|
|
scanned: true,
|
|
d9_lagna_count: 2,
|
|
d10_lagna_count: 1,
|
|
transitions: input.transitions ?? [],
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
test("groups 换升时刻 by clock minute and splits scoring layers from display layers", () => {
|
|
const grouped = groupWindowTransitions([
|
|
transition("d11", "04:47"),
|
|
transition("hora", "04:47"),
|
|
transition("d24", "04:47"),
|
|
transition("d10", "04:48"),
|
|
transition("pranapada", "05:14"),
|
|
transition("d9", "04:57"),
|
|
]);
|
|
|
|
assert.deepEqual(grouped.map((row) => row.at), ["04:47", "04:48", "04:57", "05:14"]);
|
|
assert.deepEqual(grouped[0], {
|
|
at: "04:47",
|
|
scoringLayers: ["d24", "d11"],
|
|
displayLayers: ["hora"],
|
|
});
|
|
assert.deepEqual(grouped[1]?.scoringLayers, ["d10"]);
|
|
assert.deepEqual(grouped[3], {
|
|
at: "05:14",
|
|
scoringLayers: [],
|
|
displayLayers: ["pranapada"],
|
|
});
|
|
});
|
|
|
|
test("marks only houses whose sign or occupants actually changed", () => {
|
|
const before = snapshot({ time: "04:45", lagna: "金牛座" })?.houseTable ?? null;
|
|
const after = snapshot({ time: "04:48", lagna: "金牛座", seventh: "天秤座" })?.houseTable ?? null;
|
|
assert.deepEqual(changedHouseNumbers(before, after), [7]);
|
|
assert.deepEqual(changedHouseNumbers(null, after), []);
|
|
});
|
|
|
|
test("announces the first board fill and later house or 换升 changes without dumping the table", () => {
|
|
const first = snapshot({
|
|
time: "04:45",
|
|
lagna: "金牛座",
|
|
transitions: [transition("d9", "04:57")],
|
|
});
|
|
const second = snapshot({
|
|
time: "04:48",
|
|
lagna: "金牛座",
|
|
seventh: "天秤座",
|
|
transitions: [transition("d9", "04:57"), transition("d10", "04:48")],
|
|
});
|
|
assert.ok(first && second);
|
|
|
|
const opened = diffRectificationBoard(null, first);
|
|
assert.equal(opened.liveMessage, "当前本命宫位已按 04:45 排出,上升金牛座。");
|
|
assert.deepEqual(opened.houseNumbers, []);
|
|
assert.equal(opened.timeChanged, false);
|
|
|
|
const updated = diffRectificationBoard(first, second);
|
|
assert.equal(updated.timeChanged, true);
|
|
assert.deepEqual(updated.houseNumbers, [7]);
|
|
assert.deepEqual(updated.transitionMinutes, ["04:48"]);
|
|
assert.match(updated.liveMessage, /本命宫位已按 04:48 重算(上升 金牛座)。/);
|
|
assert.match(updated.liveMessage, /第7宫已更新/);
|
|
assert.match(updated.liveMessage, /换升时刻已更新/);
|
|
assert.doesNotMatch(updated.liveMessage, /太阳|第1宫|第2宫|下面是本轮/);
|
|
});
|
|
|
|
test("peek copy stays a control label, not a second dump of the house table", () => {
|
|
const empty = rectificationBoardPeekCopy(null);
|
|
assert.equal(empty.title, "当前盘面");
|
|
assert.equal(empty.detail, "补充经历后会在这里更新");
|
|
assert.equal(rectificationBoardPeekCopy(null, "05:10").detail, "大概 05:10");
|
|
assert.equal(rectificationBoardPeekCopy(null, "05:10", "hospital_record").detail, "出生记录 05:10");
|
|
|
|
const filled = rectificationBoardPeekCopy(snapshot({ time: "04:48", lagna: "金牛座" }));
|
|
assert.equal(filled.title, "当前盘面");
|
|
assert.equal(filled.detail, "04:48 · 上升金牛座");
|
|
});
|
|
|
|
test("board shows a north-indian chart and folds 换升时刻 by default", () => {
|
|
const result = snapshot({
|
|
time: "04:45",
|
|
lagna: "金牛座",
|
|
transitions: [transition("d9", "04:57"), transition("hora", "04:47")],
|
|
});
|
|
assert.ok(result);
|
|
const chart = houseTableToNorthIndianChart(result.houseTable!);
|
|
assert.equal(chart.houses.length, 12);
|
|
assert.equal(chart.houses[0]?.houseNumber, 1);
|
|
assert.equal(chart.houses[0]?.sign, "金牛座");
|
|
const html = renderToStaticMarkup(createElement(RectificationBoard, {
|
|
result,
|
|
declaredTime: "04:45",
|
|
savedStatus: null,
|
|
diff: diffRectificationBoard(null, result),
|
|
compact: false,
|
|
open: true,
|
|
boardId: "board",
|
|
titleId: "board-title",
|
|
onClose: () => undefined,
|
|
}));
|
|
assert.match(html, /role="img"/);
|
|
assert.match(html, /当前本命盘,上升金牛座/);
|
|
// 原值: assert.match(html, /换升时刻 · 2 个/)
|
|
// 新值: 「候选分钟」段的 summary 带 `2 个时刻`,`换升时刻` 变成段内组标题。
|
|
// 原因: D10 五段收敛成两段,换升时刻与次级候选并入「候选分钟」。计数仍是
|
|
// grouped.length,仍然默认折叠——只是标签换了归属,内容一条不丢。
|
|
assert.match(html, /候选分钟/);
|
|
assert.match(html, /2 个时刻/);
|
|
assert.match(html, /换升时刻/);
|
|
assert.match(html, /<details/);
|
|
assert.doesNotMatch(html, /<details[^>]*\sopen/);
|
|
assert.match(html, /本命宫位表/);
|
|
assert.match(html, /参数与口径/);
|
|
assert.doesNotMatch(html, /<section[^>]*aria-label="换升时刻"/);
|
|
});
|