把正文组成收成数据契约,程序投影与旁观维护不再靠模型临场猜。

有变量真值时固定开旁观填表,开场与游玩只填模型生成区。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-07 23:47:58 +08:00
co-authored by Cursor
parent bb4f934b3d
commit d0a48f9431
21 changed files with 3094 additions and 224 deletions
+77
View File
@@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import { formatTableValue, parseTableDraft } from "../web/play-aux.js";
import { Blackboard } from "../src/blackboard/blackboard.js";
import { stringifyTableDoc, createTableFromValues } from "../src/blackboard/table-cells.js";
import { buildPlayAuxView } from "../src/skills/play-aux.js";
@@ -72,6 +73,8 @@ describe("play-aux view", () => {
expect(view.openings).toHaveLength(1);
expect(view.openings[0]?.text).toBe("@玩家站在债契前,堂屋很静。");
expect(view.openings[0]?.text).not.toContain("present.v1");
expect(view.openings[0]?.document).toContain("present.v1");
expect(view.openings[0]?.document).toContain("@玩家站在债契前,堂屋很静。");
});
it("lists dictate 设计.开场白#slot openings with same-slot 主角设定", () => {
@@ -109,6 +112,45 @@ describe("play-aux view", () => {
expect(gate?.persona?.name).toBe("阿泽");
});
it("prefers slotted 设计.开场白#场景短码 over the bare card and the legacy bundle", () => {
const board = new Blackboard();
board.write({
tag: "设计.开场白",
content: JSON.stringify({
schema: "present.v1",
shell: "prose",
blocks: { body: "这张没有短码。" },
}),
source: "agent",
});
board.write({
tag: "设计.开场白#opening-cycle2",
content: JSON.stringify({
schema: "present.v1",
shell: "status_card",
blocks: { status_card: "天数|阶段" },
}),
source: "agent",
});
board.write({
tag: "设计.开场白与开场变量",
content: JSON.stringify({
schema: "context-fragment.v1",
技能: "开场白与开场变量",
正文: { 开场白全文: "@玩家站在债契前。" },
}),
source: "agent",
});
const view = buildPlayAuxView(board);
expect(view.openings).toHaveLength(1);
expect(view.openings[0]?.tag).toBe("设计.开场白#opening-cycle2");
expect(view.openings[0]?.slot).toBe("opening-cycle2");
expect(view.openings[0]?.document).toContain("status_card");
expect(view.openings[0]?.document).toContain("天数|阶段");
// 无 body 时预览回退整包;选用仍走 document,不会压丢 status_card
expect(view.openings[0]?.document).toBe(view.openings[0]?.text);
});
it("lists proliferated zero-layer openings for play swipe", () => {
const board = new Blackboard();
board.write({
@@ -126,3 +168,38 @@ describe("play-aux view", () => {
expect(view.selectedOpeningIndex).toBe(1);
});
});
describe("play table drafts", () => {
it("keeps numbers, booleans, and JSON values in their original types", () => {
expect(parseTableDraft(1, "12")).toEqual({ ok: true, value: 12 });
expect(parseTableDraft(1, "nope").ok).toBe(false);
expect(parseTableDraft(false, "是")).toEqual({ ok: true, value: true });
expect(parseTableDraft(["基础侦察术"], '["基础侦察术","追踪"]')).toEqual({
ok: true,
value: ["基础侦察术", "追踪"],
});
expect(parseTableDraft(["基础侦察术"], "[")).toEqual({ ok: false, error: "JSON 不合法" });
expect(parseTableDraft("空", " 还在 ")).toEqual({ ok: true, value: "还在" });
});
it("pretty-prints long structured values and keeps short ones on one line", () => {
expect(formatTableValue(["基础侦察术"])).toBe('["基础侦察术"]');
const long = { a: "x".repeat(40), b: [1, 2, 3] };
expect(formatTableValue(long)).toContain("\n");
expect(formatTableValue(10)).toBe("10");
});
it("parses JSON strings so nested matrices are editable as objects", () => {
const matrix =
'{"塞琳娜":{"认知度":"有所耳闻","亲密度":-10},"崔岚":{"认知度":"有所耳闻","亲密度":-10}}';
expect(formatTableValue(matrix)).toContain("\n");
expect(formatTableValue(matrix)).toContain("塞琳娜");
expect(parseTableDraft(matrix, formatTableValue(matrix))).toEqual({
ok: true,
value: {
塞琳娜: { 认知度: "有所耳闻", 亲密度: -10 },
崔岚: { 认知度: "有所耳闻", 亲密度: -10 },
},
});
});
});