feat: 第一季九章完整剧本与引擎(chapters 4-9、相处线索报告替代七维评分、采样覆盖驱动支线插入、consent v2、报告页隐私区块完善)+ 跨 agent 断点修复
This commit is contained in:
+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('✓ 共享评分排除身份数据、无效响应不冒充低置信、条件文案不串线');
|
||||
|
||||
Reference in New Issue
Block a user