0. Normative Reading Rules

本文档是实现规格,不是头脑风暴记录。

以下词语具有规范含义:

MUST       必须满足的强制要求
MUST NOT   禁止行为
SHOULD     强烈建议,除非有明确理由
MAY        可选行为

当不同章节发生冲突时,以以下优先级为准:

Section 4 Frozen Implementation Decisions 和 Section 16 Slice 0 Entry Checklist
  > Section 15 Development Slices
  > API/interface contracts
  > architecture explanation
  > examples

Slice 0 阶段,所有仓库路径均以仓库根目录为基准:

11_looptrain/

Slice 0 阶段,仓库根目录下的 src/runtime/ 不启用。它只是长期迁移目标。


1. Executive Summary

LoopTrain 必须从当前 standalone SLT 实现,演进为分层 Runtime 架构。

当前可运行游戏位于:

looptrain/standalone/
  server.js
  engine.js
  public/app.js
  llm/
  tests/

可复用游戏材料位于:

looptrain/materials/

迁移必须采用 strangler pattern,不能一次性大重写:

current looptrain/standalone/engine.js

LegacyEngineAdapter

new runtime ports and modules

目标分层 Runtime:

Memory Runtime

CompanionView v1

Deterministic Assistant Runtime

LLM Expression Layer

Cross-loop / Archive / Settlement enhancement

later: Companion Action Runtime

项目核心铁律:

Engine judges.
Memory records.
CompanionView filters.
Assistant guides.
ActionPlanner recommends.
Settlement explains.
LLM phrases.
Validator gates.
Player decides.

第一阶段实现重点不是让许知微自由聊天。

第一阶段实现重点是:

make the runtime prove what 许知微 is allowed to know and say.

即:Runtime 必须能证明许知微当前允许知道什么、允许说什么。


2. Current Project Baseline

2.1 Repository layout

仓库根目录为:

11_looptrain/

关键路径:

11_looptrain/
├── docs/
│   └── runtime-architecture-design.md
├── looptrain/
│   ├── standalone/
│   │   ├── package.json
│   │   ├── server.js
│   │   ├── engine.js
│   │   ├── public/
│   │   │   ├── index.html
│   │   │   ├── app.js
│   │   │   └── style.css
│   │   ├── llm/
│   │   │   ├── providers.js
│   │   │   └── prompt.js
│   │   └── tests/
│   │       └── smoke_test.js
│   ├── materials/
│   │   └── looptrain/
│   └── tests/
└── scripts/
    ├── start_slt.sh
    └── verify_slt.sh

2.2 Current standalone runtime

当前 SLT 运行系统主要由以下文件构成:

looptrain/standalone/server.js        # Express backend, API routes, static serving, LLM bridge route
looptrain/standalone/engine.js        # LoopTrain judge engine: AP/time/clues/dialogue/loop rules
looptrain/standalone/public/app.js    # Vanilla JS browser frontend and UI state
looptrain/standalone/llm/providers.js # DeepSeek provider + mock reply + cleanup
looptrain/standalone/llm/prompt.js    # NPC prompt construction

当前状态模型是一个 flat snapshot,通常包含:

loop
clock
ap_remaining
known_clues
carried_memory
npc_states
flags
dialogue_session

该模型能支撑 v0.5 playable loop,但不是完整 Memory Runtime。它混合了不同生命周期的数据:

  • immediate engine state;
  • confirmed player knowledge;
  • carried-over loop memory;
  • NPC relationship state;
  • temporary dialogue state;
  • internal flags。

新 Runtime 必须拆分这些职责。


3. Non-Negotiable Runtime Laws

以下 Runtime Laws 适用于所有实现。任何代码、prompt、模板、UI 逻辑都不得违反。

Law 1: Engine owns facts

只有 Game Engine / Settlement Runtime 可以创建 authoritative facts。

LLM、Assistant Runtime、Companion Runtime、UI、storage layer 都不能创建或修改事实。

facts 包括:

AP
time
clue unlock state
action success/failure
NPC state
scene state
loop outcome
relationship changes
available interaction targets

Law 2: LLM owns expression only

LLM 只能在安全 envelope 内生成自然语言表达。

LLM 禁止:

write Knowledge
write Belief
trigger actions
create recommended actions
summarize settlement as authoritative fact
reveal hidden truth
modify AP/time/clue state/NPC state/relationship state/loop outcome

Law 3: Assistant reads only CompanionView

Assistant Runtime 不得读取:

raw Memory Runtime state
raw Engine state
IndexedDB stores
hidden truth documents
raw Event Log
full script truth

Assistant Runtime 唯一 state input 是:

CompanionView

Assistant 输出可以展示 recommended actions,但这些 actions 必须来自:

ActionRegistry -> ActionPlanner -> OutputValidator -> ResponseRenderer

LLM 和 templates 都不能发明 action IDs。

Law 5: Settlement only comes from Settlement Runtime

Dialogue settlement 和 loop settlement 必须来自 structured Engine / Settlement output。

Assistant 通过 AssistantSettlementReader 读取 settlement。

LLM 不得总结 raw dialogue history 并把总结当成 authoritative fact。

Law 6: Belief is not Knowledge

Knowledge 是 confirmed fact。Belief 是 inferred,可以错误。

Assistant 可以提到 Belief,但必须使用 uncertainty language。

允许:

“这还不是证据,但它值得验证。”

禁止:

“这就证明他是凶手。”

Law 7: Output is untrusted until validated

所有输出在进入 UI 前都必须通过 Validator。

这包括 deterministic templates 和 LLM output。

Law 8: LLM can be disabled

Runtime 必须在以下配置下仍然可用:

LT_LLM_PROVIDER=disabled

Law 9: Player agency remains final

Assistant 可以推荐候选行动,但不得执行行动,也不得把某个答案表达为唯一正确答案。

v1 中,点击 recommended action 只能填充输入框,不得自动执行。


4. Frozen Implementation Decisions

本节为 Slice 0 冻结实现决策。若与其他章节冲突,以本节为准。

4.1 Runtime source path

Slice 0 Runtime TypeScript 源码必须放在:

looptrain/standalone/src/runtime/

禁止在 Slice 0 使用仓库根目录:

src/runtime/

根目录 src/runtime/ 仅作为长期目标,必须在 package boundary 文档明确后才能启用。

4.2 Compiled output path

Slice 0 编译输出:

looptrain/standalone/dist/runtime/

入口文件:

looptrain/standalone/dist/runtime/index.js

server.js 必须通过 CommonJS 引入:

const runtime = require('./dist/runtime');

不得直接 require TypeScript 源码。

4.3 Module format

Slice 0 模块格式固定为:

CommonJS

不得切换为 ESM。

4.4 TypeScript config

TypeScript 配置文件:

looptrain/standalone/tsconfig.runtime.json

最低配置:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",
    "moduleResolution": "Node",
    "rootDir": "./src/runtime",
    "outDir": "./dist/runtime",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "resolveJsonModule": true
  },
  "include": ["src/runtime/**/*.ts"],
  "exclude": ["node_modules", "dist"]
}

4.5 package.json ownership

TypeScript、build scripts、test scripts、runtime dependencies 均归属:

looptrain/standalone/package.json

必须包含:

{
  "scripts": {
    "build:runtime": "tsc -p tsconfig.runtime.json",
    "test:runtime": "npm run build:runtime && node --test \"dist/runtime/**/*.test.js\"",
    "test:standalone": "node tests/smoke_test.js",
    "test": "npm run test:runtime && npm run test:standalone"
  }
}

4.6 Slice 0 runtime execution location

Slice 0 只实现 server-side Runtime Host 和 deterministic Assistant Runtime skeleton。

Slice 0 server-side:
  LegacyEngineAdapter
  AssistantController
  CompanionViewBuilder
  ActionRegistry
  ActionPlanner
  FallbackTemplateEngine
  OutputValidator
  LLMProvider interface

完整 browser TypeScript MemoryRuntime / IndexedDB 实现不属于 Slice 0。

Slice 0 browser 侧只做:

public/app.js 调用 /api/assistant/ask
展示 AssistantPanel
点击推荐行动填充输入框
维护现有 browser state / legacy snapshot

4.7 MemoryRuntime canonical location

长期设计中,MemoryRuntime canonical location 是:

browser

IndexedDB 只能由 browser-side MemoryRuntime 访问。

但 Slice 0 不实现完整 browser MemoryRuntime。Slice 0 使用 RuntimeClientState 作为 server 与 browser 之间的 request-scoped state contract。

4.8 RuntimeClientState is mandatory

所有 server Runtime API 必须接收:

export interface RuntimeClientState {
  playerId: string;
  runId: string;
  chapterId: string;
  episodeId: string;
  loopId: string;
  sceneId: string;
  snapshotId: string | null;
  lastEventId: string | null;
  eventSeq: number;
  eventsSinceSnapshot: MemoryEvent[];
}

AssistantAskRequest 必须包含 clientState

4.9 Event protocol

事件类型统一使用:

UPPER_SNAKE_CASE

server-side LegacyEngineAdapter 返回 MemoryEventDraft[],不得返回完整 MemoryEvent[]

正式 MemoryEvent 由 browser append 时生成:

eventId
eventSeq
prevEventId
createdAt

4.10 Content path

Legacy content 继续读取:

looptrain/materials/looptrain/

Runtime content 新增路径:

looptrain/materials/runtime/

不得使用不存在的:

looptrain/standalone/materials/

4.11 CompanionViewBuilder module ownership

CompanionViewBuilder 不属于 Assistant 内部模块。

路径必须为:

looptrain/standalone/src/runtime/companion-view/

Assistant Runtime 只能依赖 CompanionView,不能绕过它读 raw state。

4.12 LLM config

新 Runtime 统一使用:

LT_LLM_PROVIDER=disabled | mock | deepseek

兼容 legacy:

LLM_ENABLED=false -> LT_LLM_PROVIDER=disabled
LLM_ENABLED=true  -> LT_LLM_PROVIDER=deepseek, if LT_LLM_PROVIDER not set

默认值:

LT_LLM_PROVIDER=disabled

5. Slice 0 Directory Structure

Slice 0 必须创建以下目录:

looptrain/standalone/src/runtime/
  index.ts

  shared/
    ids.ts
    time.ts
    errors.ts
    result.ts

  ids/
    RuntimeId.ts
    RuntimeIdGenerator.ts

  engine/
    EngineResult.ts
    LegacyEngineAdapter.ts
    MemoryEventDraft.ts

  memory/
    MemoryEvent.ts
    RuntimeClientState.ts
    SerializedMemoryState.ts

  companion-view/
    CompanionView.ts
    CompanionViewBuilder.ts
    CompanionViewPolicy.ts
    CompanionVisibilityFilter.ts
    CompanionSpoilerGuard.ts

  assistant/
    index.ts
    AssistantController.ts
    AssistantTypes.ts
    IntentClassifier.ts
    AssistantPolicyEngine.ts
    ActionRegistry.ts
    ActionPlanner.ts
    AssistantSettlementReader.ts
    PromptBuilder.ts
    LLMProvider.ts
    DeepSeekLLMProvider.ts
    MockLLMProvider.ts
    DisabledLLMProvider.ts
    OutputValidator.ts
    ResponseRenderer.ts
    FallbackTemplateEngine.ts
    AssistantAuditLogger.ts

  content/
    RuntimeContentLoader.ts
    ContentPathPolicy.ts

  policy/
    SpoilerPolicy.ts
    ForbiddenRevealPolicy.ts

  tests/
    *.test.ts

编译后输出到:

looptrain/standalone/dist/runtime/

6. Runtime API Bridge

当前 Express server 为:

looptrain/standalone/server.js

Slice 0 必须扩展 server.js,不得替换 server.js。

6.1 Required routes

Slice 0 至少新增:

POST /api/assistant/ask
GET  /api/assistant/state

可选新增:

POST /api/runtime/action/commit
POST /api/runtime/dialogue/end
POST /api/runtime/loop/fail
POST /api/runtime/loop/next

6.2 POST /api/assistant/ask

请求:

export interface AssistantAskRequest {
  clientState: RuntimeClientState;
  trigger: AssistantTrigger;
  playerText?: string;
  locale: 'zh-CN';
  clientNow: string;
  debug?: boolean;
}

响应:

export interface AssistantAskResult {
  responseId: string;
  mode: AssistantResponseMode;

  assistant: {
    id: 'xu_zhiwei';
    displayName: '许知微';
  };

  visibleText: string;
  recommendedActions: RenderableAssistantAction[];
  clueReferences: RenderableClueReference[];
  beliefReferences: RenderableBeliefReference[];
  settlement?: RenderableSettlement;
  ui: AssistantUIHints;
  audit: AssistantAuditSummary;
}

6.3 GET /api/assistant/state

用于 UI 初始化“询问助手”按钮状态。

响应:

export interface AssistantInitialStateResult {
  buttonVisible: boolean;
  buttonLabel: '询问助手';
  buttonEmphasis: 'high' | 'normal' | 'low' | 'hidden';
  assistantKnownToPlayer: boolean;
  firstContactAvailable: boolean;
}

首轮初次接触前:

buttonVisible = true
buttonLabel = '询问助手'
buttonEmphasis = high
assistantKnownToPlayer = false
firstContactAvailable = true

7. ID Scheme

所有 ID 必须由 RuntimeIdGenerator 统一生成或校验。

路径:

looptrain/standalone/src/runtime/ids/RuntimeIdGenerator.ts

7.1 General rule

所有 ID 必须满足:

^[a-z][a-z0-9_-]*$

禁止使用:

空格
中文
冒号
斜杠
反斜杠
URL
随机自然语言

7.2 playerId

生成时机:玩家首次进入 standalone 页面。

生成位置:browser。

格式:

player_${uuidv4}

持久化:

legacy localStorage / future IndexedDB meta store

7.3 runId

生成时机:New Game / Reset Run / Start Trial。

格式:

run_${uuidv4}

生命周期:一次完整 playthrough。

7.4 chapterId

Slice 0 固定为:

chapter-01

content-authored,不得运行时随机生成。

7.5 episodeId

Slice 0 固定为:

trial-001

legacy trial_001 映射为:

episodeId = trial-001
chapterId = chapter-01

7.6 sceneId

content-authored stable ID。

示例:

scene-carriage-03
scene-carriage-joint-03
scene-dining-car

7.7 loopId

格式:

loop_${loopIndexPadded}_${runShortId}

示例:

loop_0001_550e8400

7.8 eventId

正式 MemoryEvent append 时由 browser 生成。

格式:

evt_${eventSeqPadded}_${random8}

server 不得生成正式 eventId。

7.9 snapshotId

格式:

snap_${snapshotSeqPadded}_${runShortId}

7.10 viewId

CompanionViewBuilder 生成。

规则:

viewId = "view_" + sha256(
  runId + "|" +
  loopId + "|" +
  sceneId + "|" +
  lastEventId + "|" +
  policyVersion + "|" +
  companionViewBuilderVersion
).slice(0, 16)

同输入必须生成同 viewId。


8. Memory Event Protocol

8.1 MemoryEventType

统一事件类型:

export type MemoryEventType =
  | 'LOOP_STARTED'
  | 'ACTION_COMMITTED'
  | 'AP_SPENT'
  | 'TIME_ADVANCED'
  | 'SCENE_CHANGED'
  | 'SCENE_ENTERED'
  | 'DIALOGUE_STARTED'
  | 'DIALOGUE_ENDED'
  | 'DIALOGUE_OUTCOME_RECORDED'
  | 'CLUE_UNLOCKED'
  | 'KNOWLEDGE_CONFIRMED'
  | 'BELIEF_CREATED'
  | 'BELIEF_UPDATED'
  | 'BELIEF_CONTRADICTED'
  | 'BELIEF_CONFIRMED'
  | 'RELATIONSHIP_UPDATED'
  | 'NPC_STATE_UPDATED'
  | 'GOAL_UPDATED'
  | 'ACTION_UNLOCKED'
  | 'LOOP_FAILED'
  | 'LOOP_OUTCOME_RECORDED'
  | 'GOAL_FAILED'
  | 'CARRYOVER_MEMORY_RECORDED'
  | 'FAIL_REASON_RECORDED'
  | 'TIME_RESET'
  | 'AP_RESET'
  | 'CARRYOVER_MEMORY_APPLIED'
  | 'SNAPSHOT_WRITTEN'
  | 'RESET_APPLIED';

8.2 MemoryEventDraft

server 返回 draft:

export interface MemoryEventDraft<TPayload = unknown> {
  type: MemoryEventType;
  runId: string;
  loopId: string;
  chapterId: string;
  episodeId: string;
  sceneId: string;
  payload: TPayload;
}

8.3 MemoryEvent

browser append 后形成正式事件:

export interface MemoryEvent<TPayload = unknown> {
  eventId: string;
  eventSeq: number;
  type: MemoryEventType;
  runId: string;
  loopId: string;
  chapterId: string;
  episodeId: string;
  sceneId: string;
  prevEventId: string | null;
  createdAt: string;
  payload: TPayload;
}

createdAt 必须是 ISO-8601 UTC 字符串。

8.4 Append rule

server 负责判断发生了什么。
browser MemoryRuntime / legacy state bridge 负责生成正式 MemoryEvent 并持久化。

9. LegacyEngineAdapter Contract

路径:

looptrain/standalone/src/runtime/engine/LegacyEngineAdapter.ts

LegacyEngineAdapter 是 server Engine 与 MemoryRuntime 的唯一桥接层。

它不直接写 IndexedDB,也不持久化 MemoryRuntime。

9.1 Interface

export interface LegacyEngineAdapter {
  commitAction(input: CommitActionInput): Promise<CommitActionOutput>;
  endDialogue(input: EndDialogueInput): Promise<EndDialogueOutput>;
  failLoop(input: FailLoopInput): Promise<FailLoopOutput>;
  nextLoop(input: NextLoopInput): Promise<NextLoopOutput>;
}

9.2 commitAction

export interface CommitActionInput {
  clientState: RuntimeClientState;
  actionId: string;
  playerText?: string;
  selectedActionRef?: string;
}

export interface CommitActionOutput {
  engineResult: EngineResult;
  memoryEventDrafts: MemoryEventDraft[];
}

必须始终产生:

ACTION_COMMITTED

可能产生:

AP_SPENT
TIME_ADVANCED
SCENE_CHANGED
DIALOGUE_STARTED
CLUE_UNLOCKED
BELIEF_UPDATED
RELATIONSHIP_UPDATED
NPC_STATE_UPDATED
GOAL_UPDATED
ACTION_UNLOCKED

9.3 endDialogue

export interface EndDialogueInput {
  clientState: RuntimeClientState;
  dialogueId: string;
  npcId: string;
}

export interface EndDialogueOutput {
  engineResult: EngineResult;
  memoryEventDrafts: MemoryEventDraft[];
}

必须始终产生:

DIALOGUE_ENDED

有效对话结束时必须产生:

DIALOGUE_OUTCOME_RECORDED

9.4 failLoop

export interface FailLoopInput {
  clientState: RuntimeClientState;
  failReasonCode: string;
}

export interface FailLoopOutput {
  engineResult: EngineResult;
  memoryEventDrafts: MemoryEventDraft[];
}

必须产生:

LOOP_FAILED
LOOP_OUTCOME_RECORDED

9.5 nextLoop

export interface NextLoopInput {
  clientState: RuntimeClientState;
}

export interface NextLoopOutput {
  engineResult: EngineResult;
  memoryEventDrafts: MemoryEventDraft[];
}

必须产生:

LOOP_STARTED
TIME_RESET
AP_RESET

10. CompanionView v1

10.1 Purpose

CompanionView 是 Assistant Runtime / 许知微唯一可读 state view。

它回答:

What is Xu Zhiwei allowed to know right now?

防剧透不依赖 prompt 约束,而是从源头上不把 hidden truth 放进 CompanionView。

10.2 Data flow

Slice 0:

RuntimeClientState + current legacy engine visible state + runtime content policy

CompanionViewBuilder

CompanionView

AssistantRuntime

长期:

MemoryRuntime + GameEngine

CompanionViewBuilder

CompanionView

AssistantRuntime / CompanionRuntime / 许知微

10.3 Schema

export interface CompanionView {
  viewId: string;
  schemaVersion: 1;

  player: CompanionPlayerView;
  run: CompanionRunView;
  scene: CompanionSceneView;

  knowledge: CompanionKnowledgeView;
  belief: CompanionBeliefView;
  timeline: CompanionTimelineView;
  archive: CompanionArchiveView;
  relationship: CompanionRelationshipView;

  policy: CompanionViewPolicy;
  provenance: CompanionViewProvenance;
}

10.4 CompanionViewPolicy

export interface CompanionViewPolicy {
  assistantPhase:
    | 'pre_contact'
    | 'onboarding'
    | 'guided'
    | 'normal'
    | 'minimal'
    | 'locked';

  guidanceLevel: 0 | 1 | 2 | 3;
  maxSpoilerLevel: 0 | 1 | 2 | 3 | 4 | 5;

  canReferenceBeliefs: boolean;
  canCompareLoops: boolean;
  canReferenceArchive: boolean;
  canRecommendActions: boolean;

  locale: 'zh-CN';
  hiddenTruthAccessible: false;
}

hiddenTruthAccessible 在 v1 必须始终为 false

10.5 Filter rules

CompanionView 必须过滤:

locked clue
hidden identity
future plot
NPC private thoughts
raw relationship scores
hidden branch conditions
full mystery solution

所有 ambiguous visibility 必须 deny by default。

10.6 Acceptance

CompanionView v1 完成标准:

AssistantRuntime 只能用 CompanionView 回答“我们知道什么”。
ActionPlanner 只能用 CompanionView 推荐行动。
PromptBuilder 只能用 CompanionView 构造 prompt。
OutputValidator 只能用 CompanionView 校验 refs。
Hidden truth 不能通过 CompanionView 进入 LLM。
Debug/audit 不泄露 forbidden content。

11. Deterministic Assistant Runtime

11.1 Goal

第一版 Assistant Runtime 必须在 no LLM 模式下工作。

目标是解决:

new players do not know how to start investigating

它通过许知微在剧情内给出引导,但不能变成外部攻略系统。

11.2 Flow

Player clicks “询问助手”

UI sends AssistantAskRequest with RuntimeClientState

AssistantController

CompanionViewBuilder.build()

IntentClassifier

AssistantPolicyEngine

ActionPlanner

AssistantSettlementReader if needed

FallbackTemplateEngine

OutputValidator

ResponseRenderer

UI renders text + action buttons

11.3 Assistant triggers

export type AssistantTrigger =
  | 'ASK_ASSISTANT_BUTTON'
  | 'ASSISTANT_FREE_TEXT'
  | 'PLAYER_STALLED'
  | 'NEW_CLUE_ACQUIRED'
  | 'LOOP_STARTED'
  | 'DIALOGUE_SETTLEMENT'
  | 'LOOP_SETTLEMENT';

11.4 IntentClassifier

export type AssistantIntent =
  | 'ASK_NEXT_ACTION'
  | 'ASK_SCENE_EXPLAIN'
  | 'ASK_CLUE_SUMMARY'
  | 'ASK_LOOP_SUMMARY'
  | 'ASK_IDENTITY'
  | 'ASK_TRUTH'
  | 'ASK_RULE'
  | 'CASUAL_CHAT'
  | 'INVALID_OR_ATTACK';

规则优先级:

ASK_ASSISTANT_BUTTON with no text -> ASK_NEXT_ACTION
prompt injection text -> INVALID_OR_ATTACK
truth / culprit / bomb / full answer -> ASK_TRUTH
“我该怎么办” -> ASK_NEXT_ACTION
“这里是哪” -> ASK_SCENE_EXPLAIN
“现在知道什么” -> ASK_CLUE_SUMMARY
“上一轮为什么失败” -> ASK_LOOP_SUMMARY
“你是谁 / 我是谁” -> ASK_IDENTITY
otherwise -> CASUAL_CHAT

11.5 AssistantPolicyEngine

export interface AssistantPolicy {
  assistantPhase:
    | 'pre_contact'
    | 'onboarding'
    | 'guided'
    | 'normal'
    | 'minimal'
    | 'locked';

  guidanceLevel: 0 | 1 | 2 | 3;
  maxSpoilerLevel: 0 | 1 | 2 | 3 | 4 | 5;

  canRecommendActions: boolean;
  canCompareLoops: boolean;
  canReferenceBeliefs: boolean;
  canReferenceArchive: boolean;

  canTriggerActions: false;
  maxActionCount: number;
  llmProvider: 'disabled' | 'mock' | 'deepseek';
}

默认 progression:

Conditionphaseguidancespoilerbutton
first loop before first contactpre_contact30high
loop 1 after first contactonboarding31high
loop 2guided21normal
loop 3guided22normal
loop 4+normal12low
player masteredminimal02low

Repeated failure adjustment:

same goal failed twice -> guidanceLevel at least 2
same goal failed three times -> maxSpoilerLevel may temporarily become 3
never open spoilerLevel 4/5 in normal gameplay

12. ActionRegistry and ActionPlanner

12.1 ActionRegistry path

ActionRegistry 内容文件路径:

looptrain/materials/runtime/chapters/chapter-01/actions/action-registry.json

Runtime loader:

looptrain/standalone/src/runtime/content/RuntimeContentLoader.ts

12.2 ActionDefinition

export interface ActionDefinition {
  actionId: string;

  type:
    | 'dialogue'
    | 'observe'
    | 'move'
    | 'present_clue'
    | 'review'
    | 'wait';

  targetId?: string;

  label: string;
  inputTemplate: string;

  requiredSceneIds?: string[];
  requiredClueIds?: string[];
  requiredNpcVisibleIds?: string[];
  requiredLocationReachableIds?: string[];

  forbiddenFlags?: string[];

  phaseAllowed: AssistantPhase[];
  spoilerLevel: 0 | 1 | 2 | 3 | 4 | 5;
  riskLevel: 'low' | 'medium' | 'high';
  priorityBase: number;
  tags?: string[];
}

12.3 Filter rules

ActionPlanner 只能从 ActionRegistry 选择 0-3 个行动。

过滤条件:

action.phaseAllowed includes policy.assistantPhase
action.spoilerLevel <= policy.maxSpoilerLevel
requiredSceneIds includes view.scene.sceneId or empty
requiredClueIds all in view.knowledge.confirmedVisible
requiredNpcVisibleIds all in view.scene.visibleNpcIds
requiredLocationReachableIds all in view.scene.reachableLocationIds
forbiddenFlags not active

12.4 Scoring

score = priorityBase
+ current goal match bonus
+ unresolved conflict bonus
+ newly acquired clue bonus
+ onboarding tutorial bonus
+ intent match bonus
- repeated action penalty
- high risk penalty
- near spoiler threshold penalty

Suggested values:

FactorScore
Current goal directly related+40
Resolves unresolved conflict+30
Uses newly acquired clue+20
Teaches current tutorial grammar+25
Intent directly matches action type+20
Repeated in same loop-30
Repeated in previous loop with no new info-15
High risk-20
SpoilerLevel equals max threshold-10

12.5 Rendering rule

按钮 label 来自:

ActionRegistry.label

点击填充文本来自:

ActionRegistry.inputTemplate

LLM 不得生成 action button label 或 inputTemplate。


13. OutputValidator and Renderer

13.1 AssistantResponse

export interface AssistantResponse {
  mode: AssistantResponseMode;

  visibleText: string;

  actionRefs: string[];
  clueRefs: string[];
  beliefRefs: string[];

  settlementRef?: string;

  spoilerLevel: 0 | 1 | 2 | 3 | 4 | 5;
  confidence: 'low' | 'medium' | 'high';

  stateEffects: [];
}

stateEffects 必须永远为空。

13.2 Validators

所有输出必须经过:

SchemaValidator
ActionValidator
ClueValidator
BeliefValidator
SettlementValidator
SpoilerValidator
ToneValidator
StateEffectValidator

13.3 Forbidden tone

禁止语气:

你必须
唯一正确
正确答案是
系统提示
任务已自动完成
真凶就是
直接去抓

允许语气:

可以先
我建议
这还不是证据
也许值得确认
现在能确认的是

13.4 ResponseRenderer

export interface RenderableAssistantAction {
  actionId: string;
  label: string;
  inputTemplate: string;
  type: ActionDefinition['type'];
  riskLevel: ActionDefinition['riskLevel'];
}

v1 点击行为:

fill input box, do not execute

14. LLM Expression Layer

14.1 Goal

LLM integration 只让许知微表达更自然。

LLM 可以改变:

visibleText wording only

LLM 不得改变:

actionRefs
clueRefs
beliefRefs
settlementRef
spoilerLevel
stateEffects
recommendedActions
facts
outcomes

14.2 LLMProvider interface

路径:

looptrain/standalone/src/runtime/assistant/LLMProvider.ts
export interface LLMProvider {
  generate(input: LLMGenerateInput): Promise<LLMGenerateResult>;
}

14.3 LLMGenerateInput

export interface LLMGenerateInput {
  requestId: string;
  provider: 'deepseek' | 'mock' | 'disabled';
  model: string;

  mode:
    | 'awakening_first_contact'
    | 'assistant_advice'
    | 'scene_explain'
    | 'clue_summary'
    | 'dialogue_settlement'
    | 'loop_settlement'
    | 'anti_spoiler'
    | 'casual_chat';

  systemPrompt: string;
  userPrompt: string;

  schemaName: 'AssistantResponse';
  temperature: number;
  maxTokens: number;
  timeoutMs: number;

  metadata: {
    playerId: string;
    runId: string;
    loopId: string;
    chapterId: string;
    episodeId: string;
    sceneId: string;
    viewId: string;
  };
}

14.4 LLMGenerateResult

export interface LLMGenerateResult {
  requestId: string;
  provider: 'deepseek' | 'mock' | 'disabled';
  model: string;

  rawText: string;
  parsedJson: unknown | null;

  finishReason:
    | 'stop'
    | 'length'
    | 'timeout'
    | 'error'
    | 'disabled';

  usage?: {
    promptTokens?: number;
    completionTokens?: number;
    totalTokens?: number;
  };

  latencyMs: number;

  error?: {
    code: string;
    message: string;
  };
}

14.5 DeepSeek provider wrapping rule

现有 DeepSeek 调用必须通过 DeepSeekLLMProvider 包装。

禁止:

AssistantController -> DeepSeek API directly

允许:

AssistantController
  -> PromptBuilder
  -> LLMProvider
  -> DeepSeekLLMProvider

DeepSeekLLMProvider 只负责:

读取 server-side env 中的 API key
调用模型
返回 rawText / parsedJson
记录 latency / usage / error

业务校验只能由 OutputValidator 完成。

14.6 Disabled provider

必须实现 disabled provider。

当:

LT_LLM_PROVIDER=disabled

Assistant Runtime 不调用任何 LLM,直接使用 FallbackTemplateEngine

验收要求:

LT_LLM_PROVIDER=disabled 时,“询问助手”仍然可用。

15. Content Path Policy

15.1 Legacy content

继续读取:

looptrain/materials/looptrain/

Slice 0 不得删除或迁移 legacy content。

15.2 Runtime content

新增:

looptrain/materials/runtime/

标准结构:

looptrain/materials/runtime/
  characters/
    xu-zhiwei.profile.json

  chapters/
    chapter-01/
      chapter.json

      episodes/
        trial-001.json

      scenes/
        scene-carriage-03.json
        scene-carriage-joint-03.json

      actions/
        action-registry.json

      policies/
        assistant-policy.json
        spoiler-policy.json
        forbidden-reveals.json

      settlements/
        dialogue-outcomes.json
        loop-outcomes.json

  templates/
    fallback-templates.zh-CN.json

15.3 许知微 profile

路径:

looptrain/materials/runtime/characters/xu-zhiwei.profile.json

该文件只能包含当前阶段允许公开的人设信息。

不得包含:

许知微真实任务
后续章节真相
主角真实身份
最终谜底

15.4 ActionRegistry

路径:

looptrain/materials/runtime/chapters/chapter-01/actions/action-registry.json

ActionPlanner 只能推荐此文件中注册且通过条件校验的 action。


16. UI Contract

16.1 AskAssistantButton

按钮 label:

询问助手

行为:

first loop: high emphasis
later loops: normal / low depending on policy
must not block main input

16.2 AssistantPanel

展示:

许知微 dialogue
recommended action buttons
clue references
belief references
settlement info

不得展示:

raw LLM output
raw prompt
raw MemoryState
raw EngineState
hidden truth
debug internals in production

16.3 RecommendedActionList

v1 点击行为:

fill input box, do not execute

原因:

Preserve player agency.
Teach valid input grammar.
Avoid assistant playing for the player.
Reduce misclick cost.
Keep Engine execution path unchanged.

17. Test Framework Decision

Slice 0 测试框架固定为:

node:test
assert

不引入:

vitest
jest
mocha
ts-node
tsx

测试源码放在:

looptrain/standalone/src/runtime/tests/

TypeScript 测试先编译,再执行:

node --test "dist/runtime/**/*.test.js"

npm test 必须覆盖:

Runtime tests
standalone smoke test

18. Required Tests

18.1 Runtime host tests

tsconfig.runtime.json exists
build:runtime succeeds
dist/runtime/index.js exists
server.js can require ./dist/runtime

18.2 CompanionView tests

必须验证:

locked clue not in view
hidden identity not in view
future plot not in view
NPC private thoughts not in view
promoted belief not in belief view
raw relationship score not in view
debug=true does not leak hidden truth
forbidden clue dropped
spoilerLevel limit applied
view limits applied

18.3 Assistant Runtime tests

必须包含:

IntentClassifier.test.ts
AssistantPolicyEngine.test.ts
ActionRegistry.test.ts
ActionPlanner.test.ts
FallbackTemplateEngine.test.ts
OutputValidator.test.ts
ResponseRenderer.test.ts
AssistantController.no-llm.test.ts

Golden cases:

first_contact_empty_memory
loop1_after_xiaoning_sound
loop1_after_failed_questioning
loop2_with_archive
ask_truth_attack
dialogue_settlement_with_new_clue
loop_settlement_failed

18.4 LLM validation tests

注入错误输出:

new action_id
locked clue reference
hidden identity reveal
stateEffects non-empty
command tone
non-JSON output

期望:

reject and fallback

18.5 Gameplay acceptance

必须满足:

New player understands “询问助手”.
First assistant click enables at least one valid action.
Player learns asking / observing / following up.
Loop failure recap gives a next-loop direction.
Assistant does not feel like it solves the case.
No LLM required for core assistant use.

19. Development Slices

Slice 0: TypeScript Host + Deterministic Assistant Runtime Skeleton

Deliver:

looptrain/standalone/tsconfig.runtime.json
looptrain/standalone/src/runtime/
looptrain/standalone/dist/runtime/
server.js require ./dist/runtime
LegacyEngineAdapter interface/stub
RuntimeClientState contract
MemoryEventDraft contract
CompanionViewBuilder deterministic skeleton
ActionRegistry loader
ActionPlanner
FallbackTemplateEngine
OutputValidator
ResponseRenderer
LLMProvider interface
DisabledLLMProvider
/api/assistant/ask
/api/assistant/state
public/app.js AskAssistantButton integration

Acceptance:

Current standalone app still starts.
Existing smoke test still passes.
npm run build:runtime succeeds.
npm test runs runtime tests and standalone smoke test.
LLM_PROVIDER disabled path works.
First assistant click returns valid deterministic advice.
Recommended action click fills input box, does not execute.

Slice 1: Browser MemoryRuntime host decision

Deliver:

browser TypeScript loading strategy
IndexedDB adapter decision
MemoryRuntime browser API
legacy app.js integration plan

Slice 2: IndexedDB Memory Runtime

Deliver:

MemoryStorageSchema
IndexedDBMemoryStorage
EventLogStore
SnapshotStore
migration support

Slice 3: Knowledge / Belief / Relationship

Deliver:

Knowledge derivation
Belief records
Relationship records
Knowledge/Belief separation

Slice 4: Timeline / Archive / Reset

Deliver:

Timeline entries
Archive entries
soft/chapter/forget/developer reset

Slice 5: Settlement Runtime

Deliver:

DialogueSettlement
LoopSettlement
AssistantSettlementReader

Slice 6: LLM Expression Layer

Deliver:

PromptBuilder
DeepSeekLLMProvider
JSON output validation
prompt injection tests
fallback on invalid output

Slice 7: UI enhancement

Deliver:

AssistantPanel polish
RecommendedActionList polish
clue/belief reference rendering
optional future React migration plan

20. Slice 0 Entry Checklist

只有满足以下全部条件,才允许进入 Slice 0 功能开发:

1. looptrain/standalone/src/runtime/ 已创建。
2. looptrain/standalone/tsconfig.runtime.json 已创建。
3. looptrain/standalone/package.json 包含 build:runtime/test:runtime/test。
4. Runtime module format 固定为 CommonJS。
5. server.js 使用 require('./dist/runtime'),不得 require TypeScript source。
6. Slice 0 明确不实现完整 browser TypeScript MemoryRuntime。
7. AssistantAskRequest 包含 RuntimeClientState。
8. LegacyEngineAdapter 返回 MemoryEventDraft[],不返回 MemoryEvent[]。
9. MemoryEventType 统一为 UPPER_SNAKE_CASE。
10. Content path 统一为 looptrain/materials/runtime/。
11. Legacy content path 继续为 looptrain/materials/looptrain/。
12. CompanionViewBuilder 位于 runtime/companion-view/。
13. LLM 配置统一为 LT_LLM_PROVIDER。
14. LT_LLM_PROVIDER=disabled 时 Assistant 可用。
15. Test framework 固定为 node:test/assert。
16. npm test 覆盖 Runtime tests 和 standalone smoke test。

未满足任意一项,不得进入功能开发。


21. Final Architecture Principle

LoopTrain 不应该成为一个“LLM 持有真相”的游戏。

它应该成为一个 runtime-driven narrative system:

Engine judges.
Memory records.
CompanionView filters.
Assistant guides.
ActionPlanner recommends.
Settlement explains.
LLM phrases.
Validator gates.
Player decides.

Slice 0 的目标不是完成完整 Memory Runtime,也不是让许知微自由聊天。

Slice 0 的目标是建立一个不会偏离设计意图的、可验证的 Runtime Host 和 Deterministic Assistant Runtime 闭环。

最终约束:

许知微不能自由知道。
许知微不能自由推荐。
许知微不能自由结算。
许知微不能自由写状态。

她只能在 Runtime 允许的范围内,
用角色化语言,
表达已经由 Engine / CompanionView / ActionPlanner / Settlement 确认可以表达的内容。