feat: 第一季九章完整剧本与引擎(chapters 4-9、相处线索报告替代七维评分、采样覆盖驱动支线插入、consent v2、报告页隐私区块完善)+ 跨 agent 断点修复
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 剧本评审包生成:把评审简报 + 完整性快照 + 三份母文档合成一个文件,
|
||||
* 剧本评审包生成:把评审简报、框架、支线与九章同源全文合成一个文件,
|
||||
* 供上传给 ChatGPT / 其他 AI 做外部评审。
|
||||
*
|
||||
* 运行:pnpm script:review
|
||||
@@ -8,6 +8,7 @@
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import './build-season-script';
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
@@ -20,16 +21,17 @@ const today = new Date().toISOString().slice(0, 10);
|
||||
const HEADER = `# 《苍梧疑冢》剧本评审包
|
||||
|
||||
> 生成时间:${today}(由 pnpm script:review 从仓库母文档自动合成)
|
||||
> 本文档供外部 AI 评审使用,包含:评审简报 → 当前完整性快照 → 三份母文档全文。
|
||||
> 本文档供外部 AI 评审使用,包含:评审简报、框架、支线、整季设计及九章同源全文。
|
||||
|
||||
---
|
||||
|
||||
## 一、这是什么
|
||||
|
||||
一款互动叙事("影游")交友产品的第一季剧本。核心机制:用户以"时空档案馆采集员"身份
|
||||
玩考古探险剧情,**剧情中的选择和语音讲述被用来实测人格**(7 维度:依恋类型、冲突处理
|
||||
风格、价值观、情绪稳定性、风险偏好、文化底蕴、择偶偏好),后续用于伴侣匹配。
|
||||
因此剧本同时承担两个职责:① 作为互动小说要好看;② 作为心理测量工具要有效。
|
||||
玩考古探险剧情,**在知情同意后,从剧情选择和自愿讲述中整理待验证的相处线索**。
|
||||
原型保留七维试验接口:依恋、冲突处理、价值观、情绪调节、风险偏好、阅读求知与择偶偏好,
|
||||
不将输出当作人格诊断,也不声称已经完成真人匹配或验证恋爱预测力。
|
||||
因此剧本同时承担两个职责:① 作为互动小说要好看;② 让日常取舍自然显现,为未来交友提供可核对的线索。
|
||||
|
||||
## 二、请评审什么(按优先级)
|
||||
|
||||
@@ -48,14 +50,10 @@ const HEADER = `# 《苍梧疑冢》剧本评审包
|
||||
|
||||
## 三、当前完整性快照(${today})
|
||||
|
||||
- **Act 1(第 1-3 章)**:剧本完成并已灌装进引擎数据(18 个选择点、4 个语音讲述点、
|
||||
全部预写场景变体),本包内含全文。
|
||||
- **自适应支线**(Jev 置信度低时动态插入的 5 条支线 × 2 变体):剧本完成,本包内含全文;
|
||||
引擎的条件插入逻辑未实现。
|
||||
- **Act 2(第 4-6 章)/ Act 3(第 7-9 章)**:只有弧线大纲(见故事框架第 2 节),
|
||||
剧本未写。
|
||||
- **已知留白**:解谜点的"三档提示"机制只有设计没有完整实现剧本;C2-06 的二级选项
|
||||
未建模(剧本已注明叙事化处理)。
|
||||
- **九章主线**:49 个交互点、122 条按钮出口,已接入应用。含全部分支后续、谜题提示与预写变体。
|
||||
- **自适应支线**:5 条支线 × 2 变体,已有引擎条件插入及回归校验。
|
||||
- **结局**:三种见证方式、三种档案馆治理与三种个人尾声可组合;不是27条完全独立长剧情。
|
||||
- **验证边界**:自动测试验证路径与数据约束,文学性、选项等值及匹配预测力仍需真人验证。
|
||||
|
||||
---
|
||||
---
|
||||
@@ -67,16 +65,19 @@ ${readDoc('docs/story-framework.md')}
|
||||
---
|
||||
---
|
||||
|
||||
# 母文档 2:Act 1 正式剧本(第 1-3 章全文)
|
||||
|
||||
${readDoc('docs/script/act1.md')}
|
||||
|
||||
---
|
||||
---
|
||||
|
||||
# 母文档 3:自适应支线剧本
|
||||
# 自适应支线剧本
|
||||
|
||||
${readDoc('docs/script/adaptive-branches.md')}
|
||||
|
||||
---
|
||||
|
||||
# 当前整季设计与伏笔核销
|
||||
|
||||
${readDoc('docs/script/season-design.md')}
|
||||
|
||||
# 当前九章可玩剧本全文(运行台词权威版本)
|
||||
|
||||
${readDoc('docs/script/season1-full.md')}
|
||||
`;
|
||||
|
||||
const outPath = join(ROOT, 'docs/script/review-bundle.md');
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import { CHAPTERS } from '../src/engine/chapters';
|
||||
import type { StoryNode } from '../src/engine/types';
|
||||
|
||||
function render(node: StoryNode): string {
|
||||
const heading = `### ${node.id}${'pointId' in node ? ` / ${node.pointId}` : ''}`;
|
||||
switch (node.type) {
|
||||
case 'dialogue': return `${heading}\n\n**${node.speaker}**:${node.text}\n\n→ ${node.next}`;
|
||||
case 'cutscene': return `${heading}\n\n${node.caption}\n\n→ ${node.next}`;
|
||||
case 'chapterEnd': return `${heading}\n\n${node.nextChapter ? `→ ${node.nextChapter}` : '第一季终 → 相处线索报告'}`;
|
||||
case 'identity': return `${heading}\n\n${node.prompt}\n\n身份字段由玩家确认,不作人格证据。\n\n→ ${node.next}`;
|
||||
case 'choice': return `${heading}\n\n${node.prompt}\n\n${node.options.map((o) => `- **${o.id}**:${o.label}\n → ${o.next}`).join('\n')}\n\n${node.puzzle ? `**谜面与校验**\n\n${node.puzzle.questions.map((q) => `${q.prompt}\n${q.options.map((o, i) => `- ${i + 1}. ${o}${i === q.answer ? '(答案)' : ''}`).join('\n')}`).join('\n\n')}\n\n**三档提示**\n\n${node.puzzle.hints.map((h, i) => `${i + 1}. ${h}`).join('\n')}\n\n` : ''}候选证据:${node.dimensions.join(' / ') || '不参与画像'}。`;
|
||||
case 'narration': return `${heading}\n\n**${node.mechanism}**\n\n${node.prompt}\n\n候选证据:${node.dimensions.join(' / ') || '不参与画像'}。\n\n**全部预写变体**\n\n${node.variants.map((v) => `- ${v.signature}:${v.label}`).join('\n')}\n\n${Object.entries(node.followUps).map(([d, text]) => `- 追问 ${d}:${text}`).join('\n')}\n\n→ ${node.next}`;
|
||||
}
|
||||
}
|
||||
|
||||
const points = CHAPTERS.flatMap((c) => c.nodes.filter((n) => 'pointId' in n));
|
||||
const body = `# 《苍梧疑冢》第一季 · 完整可玩剧本\n\n> 从 content/chapters/chapter1.json 至 chapter9.json 自动生成,手改源 JSON 后运行 pnpm script:season。\n> 含九章全部对白、选项、后续、谜底、提示、讲述变体与跳转;不止一条默认路线。\n> ${points.length} 个主线交互点。条件标记由引擎解释,不向玩家显示。第一幕的十条可选支线全文另见 adaptive-branches.md。\n> 文学与测量设计依据、全季时间线及伏笔核销见 season-design.md;既有第一幕详细创作说明见 act1.md。运行文案以本册同源的 JSON 为准。\n\n${CHAPTERS.map((c) => `## ${c.title} / 第 ${c.act} 幕\n\n${c.nodes.map(render).join('\n\n---\n\n')}`).join('\n\n---\n\n')}`;
|
||||
writeFileSync(new URL('../docs/script/season1-full.md', import.meta.url), body + '\n');
|
||||
console.log(`完整剧本:${CHAPTERS.length} 章,${points.length} 个主线交互点,${body.length} 字符(包含制作标记)`);
|
||||
+19
-17
@@ -1,5 +1,5 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { ACT1 as chapters } from '../src/engine/chapters';
|
||||
import { BRANCHES } from '../src/engine/branches';
|
||||
import { decideInsertion } from '../src/engine/branching';
|
||||
import { StoryEngine } from '../src/engine/engine';
|
||||
@@ -7,13 +7,12 @@ import { makeEvent } from '../src/engine/eventStore';
|
||||
import type { StoryEvent, EventStore } from '../src/engine/eventStore';
|
||||
import { choiceFlags, deriveFlags, resolveConditionalText } from '../src/engine/flags';
|
||||
import { MemoryProgressStore } from '../src/engine/progress';
|
||||
import { buildState, scoreProfile, PROFILE_DIMENSIONS } from '../src/engine/scoring';
|
||||
import { buildState, assessRelationships, PROFILE_DIMENSIONS } from '../src/engine/scoring';
|
||||
import type { Confidences, ProfileDimension } from '../src/engine/scoring';
|
||||
import type { Chapter, StoryNode, ChoiceBehavior } from '../src/engine/types';
|
||||
import { MockJevClient } from '../src/services/jev';
|
||||
import type { JevQuestion } from '../src/services/jev';
|
||||
|
||||
const chapters: Chapter[] = [1, 2, 3].map((i) => JSON.parse(readFileSync(new URL(`../content/chapters/chapter${i}.json`, import.meta.url), 'utf8')));
|
||||
const confidences = (low: Confidences = {}): Confidences => Object.assign(Object.fromEntries(PROFILE_DIMENSIONS.map((d) => [d.id, 0.9])), low);
|
||||
const inserted = (id: string) => {
|
||||
const b = BRANCHES.find((b) => b.id === id)!;
|
||||
@@ -203,9 +202,9 @@ for (const dimension of ['attachment', 'values', 'conflict_style', 'emotional_st
|
||||
const plans = store.events.filter((e) => e.kind === 'branch_inserted');
|
||||
assert.equal(plans.length, 2);
|
||||
assert(plans[0].branchId?.endsWith('-a') && plans[1].branchId?.endsWith('-b'));
|
||||
assert.equal(jev.calls.length, 3);
|
||||
assert(store.events.some((e) => e.kind === 'evidence_insufficient' && e.dimensions.includes(dimension)));
|
||||
assert(String(jev.calls[2]).includes('CB-'));
|
||||
// 章末插入置信度 = samplingCoverage(事件采样统计),不再调 Jev——锁新语义
|
||||
assert.equal(jev.calls.length, 0);
|
||||
// 支线本身贡献采样:走完 A→B 后该维度覆盖达标,不再标记证据不足(原断言基于旧的 Jev 置信度语义)
|
||||
const count = store.events.length;
|
||||
e.replay('chapter1');
|
||||
assert.equal(e.insertions.length, 0);
|
||||
@@ -214,7 +213,6 @@ for (const dimension of ['attachment', 'values', 'conflict_style', 'emotional_st
|
||||
await walkTo(e, 'c1-end');
|
||||
await e.advance();
|
||||
assert.equal(e.insertions[0].id, plans[0].branchId);
|
||||
assert(!String(jev.calls.at(-1)).includes(plans[0].sessionId));
|
||||
}
|
||||
console.log('✓ 五维各跑通三章、A→B换皮、末章证据不足、本局隔离、replay 清计划');
|
||||
|
||||
@@ -226,28 +224,30 @@ console.log('✓ 五维各跑通三章、A→B换皮、末章证据不足、本
|
||||
await walkTo(e, 'c1-end');
|
||||
const oldSnapshot = progress.load()!;
|
||||
await Promise.all([e.advance(), e.advance(), e.advance()]);
|
||||
assert.equal(jev.calls.length, 1);
|
||||
assert.equal(jev.calls.length, 0); // 新语义:章末决策用采样统计,无 Jev 调用;幂等性看 branch_inserted 事件数
|
||||
assert.equal(e.state.chapterId, 'chapter2');
|
||||
assert.equal(store.events.filter((ev) => ev.kind === 'branch_inserted').length, 1);
|
||||
const restored = new StoryEngine(chapters, store, jev, undefined, progress, BRANCHES);
|
||||
await restored.restore(oldSnapshot);
|
||||
await restored.advance();
|
||||
assert.equal(jev.calls.length, 1);
|
||||
assert.equal(jev.calls.length, 0);
|
||||
assert.equal(restored.insertions.length, 1);
|
||||
assert.equal(store.events.filter((ev) => ev.kind === 'branch_inserted').length, 1);
|
||||
}
|
||||
for (const jev of [
|
||||
{ ask: async () => { throw new Error('test offline'); } },
|
||||
{ ask: async () => ({}) },
|
||||
{ adaptiveScoring: false, ask: async () => { assert.fail('缺 key 不应打分'); } },
|
||||
]) {
|
||||
// 新语义:章末插入决策用 samplingCoverage(不调 Jev),Jev 抛错/空响应不影响插入;
|
||||
// 唯一关闭路径是 adaptiveScoring === false(无 key 声明不打分)
|
||||
for (const [jev, expectInserted] of [
|
||||
[{ ask: async () => { throw new Error('test offline'); } }, true],
|
||||
[{ ask: async () => ({}) }, true],
|
||||
[{ adaptiveScoring: false, ask: async () => { assert.fail('缺 key 不应打分'); } }, false],
|
||||
] as const) {
|
||||
const store = new MemoryStore();
|
||||
const e = new StoryEngine(chapters, store, jev, undefined, undefined, BRANCHES);
|
||||
await e.restore({ sessionId: 'test', chapterId: 'chapter1', nodeId: 'c1-end', flags: {} });
|
||||
await e.advance();
|
||||
assert.equal(e.state.chapterId, 'chapter2');
|
||||
assert.equal(e.insertions.length, 0);
|
||||
assert.equal(store.events.length, 0);
|
||||
assert.equal(e.insertions.length, expectInserted ? 1 : 0);
|
||||
assert.equal(store.events.filter((ev) => ev.kind === 'branch_inserted').length, expectInserted ? 1 : 0);
|
||||
}
|
||||
console.log('✓ 重复点击与刷新幂等、Jev故障/无效响应/无key降级');
|
||||
|
||||
@@ -275,6 +275,8 @@ for (const branchId of ['b-cul-a', 'b-cul-b']) {
|
||||
console.log('✓ 文化双问题分题记录 partial,主动离场独立于提示用尽/答错');
|
||||
|
||||
assert(!buildState([makeEvent({ sessionId: 'private', chapterId: 'chapter1', kind: 'profile', pointId: 'C1-01', dimensions: [], profile: { name: 'SECRET-NAME', city: '', age: '', gender: '', occupation: '', education: '' } })]).includes('SECRET-NAME'));
|
||||
await assert.rejects(() => scoreProfile([], { ask: async () => ({}) }));
|
||||
const threeAttachmentScenes = ['C2-01', 'C2-03', 'C2-06'].map((p) =>
|
||||
makeEvent({ sessionId: 'test', chapterId: 'chapter2', pointId: p, kind: 'choice', dimensions: ['attachment'], optionId: 'a', optionLabel: 'a' }));
|
||||
await assert.rejects(() => assessRelationships(threeAttachmentScenes, { ask: async () => ({}) }));
|
||||
assert.equal(resolveConditionalText('【若 cb_val_a_water = d_redirect:粥棚】', { cb_val_a_water: 'a_give' }), '');
|
||||
console.log('✓ 共享评分排除身份数据、无效响应不冒充低置信、条件文案不串线');
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { CHAPTERS } from '../src/engine/chapters';
|
||||
import { CONSENT_VERSION, StoryEngine } from '../src/engine/engine';
|
||||
import type { EventStore, StoryEvent } from '../src/engine/eventStore';
|
||||
import { deriveFlags } from '../src/engine/flags';
|
||||
import { MemoryProgressStore } from '../src/engine/progress';
|
||||
import { buildState } from '../src/engine/scoring';
|
||||
import { MockJevClient } from '../src/services/jev';
|
||||
import type { StoryNode } from '../src/engine/types';
|
||||
|
||||
class MemoryStore implements EventStore {
|
||||
events: StoryEvent[] = [];
|
||||
async append(e: StoryEvent) { this.events.push(e); }
|
||||
async list(session?: string) { return this.events.filter((e) => !session || e.sessionId === session); }
|
||||
async purge() { this.events = []; }
|
||||
}
|
||||
const pointIds = new Set<string>();
|
||||
{
|
||||
const store = new MemoryStore();
|
||||
store.events.push({ id: 'old', ts: 1, sessionId: 'old', chapterId: 'chapter1', kind: 'consent', pointId: 'CONSENT', dimensions: [], consentVersion: 'act1-consent-v1' });
|
||||
const engine = new StoryEngine(CHAPTERS, store, new MockJevClient());
|
||||
assert.equal(await engine.hasConsent(), false);
|
||||
await engine.grantConsent();
|
||||
assert.equal(await engine.hasConsent(), true);
|
||||
assert.equal(store.events.at(-1)?.consentVersion, CONSENT_VERSION);
|
||||
assert.equal(store.events[0].consentVersion, 'act1-consent-v1');
|
||||
}
|
||||
console.log('✓ 新版在线处理告知需重新确认,旧同意存证保留');
|
||||
let optionCount = 0;
|
||||
for (const [i, chapter] of CHAPTERS.entries()) {
|
||||
const nodes = new Map(chapter.nodes.map((n) => [n.id, n]));
|
||||
assert.equal(nodes.size, chapter.nodes.length, `${chapter.id} 节点唯一`);
|
||||
const seen = new Set<string>();
|
||||
const visiting = new Set<string>();
|
||||
const visit = (id: string) => {
|
||||
assert(!visiting.has(id), `${chapter.id}/${id} 循环`);
|
||||
if (seen.has(id)) return;
|
||||
const n = nodes.get(id);
|
||||
assert(n, `${chapter.id}/${id} 缺失`);
|
||||
visiting.add(id);
|
||||
if (n.type === 'chapterEnd') assert.equal(n.nextChapter, CHAPTERS[i + 1]?.id);
|
||||
else if (n.type === 'choice') {
|
||||
assert(n.options.length >= 2);
|
||||
assert.equal(new Set(n.options.map((o) => o.id)).size, n.options.length);
|
||||
for (const o of n.options) visit(o.next);
|
||||
optionCount += n.options.length;
|
||||
if (n.puzzle) {
|
||||
assert.equal(n.puzzle.hints.length, 3);
|
||||
for (const q of n.puzzle.questions) assert(q.answer >= 0 && q.answer < q.options.length);
|
||||
assert(n.options.some((o) => o.id.endsWith('-EXIT')));
|
||||
}
|
||||
} else visit(n.next);
|
||||
visiting.delete(id);
|
||||
seen.add(id);
|
||||
};
|
||||
visit(chapter.startNodeId);
|
||||
assert.equal(seen.size, nodes.size, `${chapter.id} 全部节点可达且可到章末`);
|
||||
for (const n of chapter.nodes) if ('pointId' in n) {
|
||||
assert(!pointIds.has(n.pointId), n.pointId);
|
||||
pointIds.add(n.pointId);
|
||||
if (n.type === 'narration') assert(n.variants.some((v) => v.signature.includes('沉默')));
|
||||
}
|
||||
}
|
||||
assert.equal(pointIds.size, 49);
|
||||
console.log(`✓ 九章图结构、${pointIds.size} 个交互点、${optionCount} 条选项出口、谜题三档提示及沉默变体`);
|
||||
|
||||
function texts(node: StoryNode): string[] {
|
||||
if (node.type === 'dialogue') return [node.text];
|
||||
if (node.type === 'cutscene') return [node.caption];
|
||||
if (node.type === 'choice') return [node.prompt, ...node.options.map((o) => o.label)];
|
||||
return 'prompt' in node ? [node.prompt] : [];
|
||||
}
|
||||
async function step(e: StoryEngine, mode: number, picks: Record<string, string> = {}) {
|
||||
const n = e.currentNode();
|
||||
for (const text of texts(n)) assert(!e.renderText(text).includes('【若'), `条件泄漏 ${n.id}`);
|
||||
if (n.type === 'identity') await e.confirmIdentity({ name: '测试', age: '', city: '', gender: '', education: '', occupation: '' });
|
||||
else if (n.type === 'narration') {
|
||||
if (mode === 0 || n.pointId === 'C3-02') await e.skipNarration();
|
||||
else {
|
||||
await e.submitNarration('我喜欢回家后先安静喝杯茶,再慢慢讲今天的事。');
|
||||
if (e.state.pendingFollowUp) await e.skipFollowUp();
|
||||
}
|
||||
} else if (n.type === 'choice') {
|
||||
const selected = picks[n.pointId] ?? n.options[mode % n.options.length].id;
|
||||
await e.choose(selected, n.puzzle ? { answers: n.puzzle.questions.map((q) => q.answer), hintsUsed: mode % 4 } : undefined);
|
||||
} else await e.advance();
|
||||
}
|
||||
for (const mode of [0, 1, 2]) {
|
||||
const store = new MemoryStore();
|
||||
const progress = new MemoryProgressStore();
|
||||
let engine = new StoryEngine(CHAPTERS, store, new MockJevClient(), undefined, progress);
|
||||
const visited = new Set<string>();
|
||||
for (let guard = 0; !engine.state.finished && guard < 1000; guard++) {
|
||||
visited.add(engine.state.chapterId);
|
||||
await step(engine, mode);
|
||||
if (engine.state.nodeId === 'c7-opening') {
|
||||
const restored = new StoryEngine(CHAPTERS, store, new MockJevClient(), undefined, progress);
|
||||
await restored.restore(progress.load());
|
||||
assert.deepEqual(restored.state.flags, engine.state.flags);
|
||||
engine = restored;
|
||||
}
|
||||
}
|
||||
assert(engine.state.finished, `路线 ${mode} 必须结束`);
|
||||
assert.equal(visited.size, 9);
|
||||
assert.equal(new Set(store.events.map((e) => e.pointId)).size, 49);
|
||||
assert.deepEqual(deriveFlags(store.events), engine.state.flags);
|
||||
const state = buildState(store.events);
|
||||
for (const privatePoint of ['C3-02', 'C7-02', 'C9-05']) assert(!state.includes(privatePoint));
|
||||
assert(state.includes('情境「'));
|
||||
assert.equal(store.events.filter((e) => /^C[4-9]/.test(e.pointId) && e.kind === 'choice').every((e) => !!e.choicePrompt), true);
|
||||
}
|
||||
console.log('✓ 九章三条端到端路线:全沉默、讲述、谜题退出;跨幕恢复与旗标重建;私密节点隔离');
|
||||
|
||||
// 每个新增选项都实际运行至终点,不以图可达代替引擎验证。
|
||||
for (const chapter of CHAPTERS.slice(3)) for (const node of chapter.nodes) if (node.type === 'choice') {
|
||||
for (const option of node.options) {
|
||||
const store = new MemoryStore();
|
||||
const engine = new StoryEngine(CHAPTERS, store, new MockJevClient(), chapter.id);
|
||||
const seen = new Set<string>();
|
||||
for (let guard = 0; !engine.state.finished && guard < 1000; guard++) {
|
||||
seen.add(engine.state.nodeId);
|
||||
await step(engine, 0, { [node.pointId]: option.id });
|
||||
}
|
||||
assert(engine.state.finished);
|
||||
assert(seen.has(option.next), `${node.pointId}/${option.id} 必须播放专属后续`);
|
||||
assert.equal(store.events.find((e) => e.pointId === node.pointId)?.optionId, option.id);
|
||||
}
|
||||
}
|
||||
console.log('✓ 全部新增选项逐一实走至季终,含三种治理结局与三种个人尾声');
|
||||
|
||||
for (const plan of ['delegate', 'observe', 'relay']) for (const ending of ['shared', 'local', 'pause']) {
|
||||
const store = new MemoryStore();
|
||||
const engine = new StoryEngine(CHAPTERS, store, new MockJevClient(), 'chapter6');
|
||||
let route = '';
|
||||
let outcome = '';
|
||||
for (let guard = 0; !engine.state.finished && guard < 500; guard++) {
|
||||
const n = engine.currentNode();
|
||||
if (n.id === 'c8-route' && n.type === 'dialogue') route = engine.renderText(n.text);
|
||||
if (n.id === 'c9-institution' && n.type === 'dialogue') outcome = engine.renderText(n.text);
|
||||
await step(engine, 0, { 'C6-03': plan, 'C8-05': ending });
|
||||
}
|
||||
assert(route.includes(plan === 'observe' ? '留在南岸' : plan === 'delegate' ? '记录回放' : '转录来源'));
|
||||
assert(outcome.includes(ending === 'shared' ? '共同保管' : ending === 'local' ? '分存回函' : '空白的任务日历'));
|
||||
}
|
||||
console.log('✓ 3×3 见证方式与治理结局交叉验证:亲历/转录不混写,结局正确兑现');
|
||||
+3
-5
@@ -12,7 +12,7 @@
|
||||
* consent 存证删除后回到未同意态、可重新同意并继续走剧情。
|
||||
* 运行:pnpm verify
|
||||
*/
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { ACT1 as chapters } from '../src/engine/chapters';
|
||||
import { CONSENT_VERSION, StoryEngine } from '../src/engine/engine';
|
||||
import { LocalStorageEventStore } from '../src/engine/eventStore';
|
||||
import type { EventStore, StoryEvent } from '../src/engine/eventStore';
|
||||
@@ -20,7 +20,7 @@ import { choiceFlags, deriveFlags, narrationFlags } from '../src/engine/flags';
|
||||
import { buildState } from '../src/engine/scoring';
|
||||
import { MemoryProgressStore } from '../src/engine/progress';
|
||||
import { SqliteEventStore } from '../src/engine/sqliteEventStore';
|
||||
import type { Chapter, ChoiceNode, IdentityProfile, NarrationNode } from '../src/engine/types';
|
||||
import type { ChoiceNode, IdentityProfile, NarrationNode } from '../src/engine/types';
|
||||
import { FOLLOW_UP_DIMENSIONS } from '../src/engine/types';
|
||||
import { MockJevClient } from '../src/services/jev';
|
||||
import type { JevAnswer, JevQuestion } from '../src/services/jev';
|
||||
@@ -60,9 +60,6 @@ class MemoryStore implements EventStore {
|
||||
}
|
||||
}
|
||||
|
||||
const chapters = [1, 2, 3].map(
|
||||
(i) => JSON.parse(readFileSync(new URL(`../content/chapters/chapter${i}.json`, import.meta.url), 'utf8')) as Chapter,
|
||||
);
|
||||
|
||||
let failures = 0;
|
||||
function check(ok: boolean, msg: string) {
|
||||
@@ -587,3 +584,4 @@ if (failures > 0) {
|
||||
}
|
||||
console.log('\n全部校验通过');
|
||||
await import('./verify-adaptive');
|
||||
await import('./verify-ondevice-asr');
|
||||
|
||||
Reference in New Issue
Block a user