Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/项目文档/03-规划/01-现状评估.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
- 检索与 notes 已完成第一轮串联,active notes 可作为检索上下文桥帮助召回相关记忆,archived notes 不参与扩展召回
- 调试台信息结构已完成第一轮优化,trace 面板提供前置快读条,复制摘要提供 `Trace Breakdown`
- reviewed apply 生命周期展示已完成第一轮收口,前端可核对 `Human Review -> Second Guardrail -> Manual Apply`
- 下一层安全输出接口已完成第一轮试点评估,当前结论是只适合先探索非最终状态的 pending / draft / diagnostics-like 输出层
- 下一层安全输出接口已完成第一轮试点评估,并已有只读 `NoNameSafeOutputDraft` 探针,当前结论仍是只适合先探索非最终状态的 pending / draft / diagnostics-like 输出层

## 3. 当前主要风险

Expand Down Expand Up @@ -61,7 +61,7 @@

### 3.6 下一层安全输出仍不能扩大权限边界

下一层输出接口目前只完成试点评估,不代表可以直接写最终剧情状态。后续若实现 `draft / pending hint` 类输出,仍必须保留人工复核、二次护栏、显式写入和 stale snapshot 校验。
下一层输出接口目前只完成试点评估与只读探针,不代表可以直接写最终剧情状态。当前 `NoNameSafeOutputDraft` 只从 trace 证据推导 `drafted / reviewed / guardrailAllowed / manuallyApplied / blocked / fallback`,并固定 `finalPlotStateWriteAllowed=false`。后续若实现 `draft / pending hint` 类输出,仍必须保留人工复核、二次护栏、显式写入和 stale snapshot 校验。

## 4. 当前最合理的发展策略

Expand Down
1 change: 1 addition & 0 deletions docs/项目文档/03-规划/02-后续路线图.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@
- 第一轮评估已完成
- 当前只建议试点非最终状态的 pending / draft / diagnostics-like 输出层
- 不建议直接试点最终剧情、世界硬事实、NPC 隐藏意图或绕过 reviewed apply 的自动写入
- 安全输出草稿层第一轮探针已完成:`NoNameSafeOutputDraft` 只读推导 `drafted / reviewed / guardrailAllowed / manuallyApplied / blocked / fallback`,并固定 `finalPlotStateWriteAllowed=false`
- 评估文档见 [下一层安全输出接口试点评估](./05-下一层安全输出接口试点评估.md)

### 任务 8:评估多角色协作与更强知识检索
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 下一层安全输出接口试点评估

更新时间:2026-04-21
更新时间:2026-04-24

## 1. 评估结论

Expand Down Expand Up @@ -54,26 +54,35 @@
- 必须有测试证明被拒绝、fallback、pending 三种状态不会误写入。
- 必须先写设计文档和测试探针,再决定是否落正式产品代码。

## 6. 建议的第一轮试点形态
## 6. 第一轮试点形态

建议第一轮只做“安全输出草稿层”设计,不落正式后端命令。
第一轮只做“安全输出草稿层”测试探针,不落正式后端命令。

候选形态:
当前形态:

- 名称:`NoNameSafeOutputDraft`
- 位置:先只存在于文档和测试探针中
- 内容:`draftId / sourceProposalId / outputKind / safeApplyScope / summary / rationale / lifecycleState`
- 位置:`src/utils/noNameApplyLifecycle.ts` 中的纯推导 utility,与对应测试探针
- 内容:`draftId / sourceProposalId / outputKind / safeApplyScope / summary / rationale / lifecycleState / evidence`
- 生命周期:`drafted -> reviewed -> guardrailAllowed -> manuallyApplied`,其中 `manuallyApplied` 只代表显式命令完成,不代表自动写入
- 默认写入层:trace / pending hint / diagnostics,不写 final plot state
- 证据来源:现有 `controlledOutputReviews`、`proposalTransitionLog`、`applyExecutionLog` 与人工复核记录
- 永久红线:`evidence.finalPlotStateWriteAllowed` 固定为 `false`

如果后续要落代码,应优先从 utility 与测试开始,而不是从 `tauri_commands.rs` 新增命令开始。

## 7. 当前决策

可以开始设计试点,但不能开始扩大 apply scope。
可以继续评估试点,但不能开始扩大 apply scope。

当前状态:

- 第一轮 `NoNameSafeOutputDraft` 探针已完成
- 探针只读取 trace 证据,不新增后端命令,不写 runtime 状态
- 测试已覆盖 pending 只停在 `drafted`,人工批准进入 `reviewed`,二次护栏允许进入 `guardrailAllowed`,显式 manual apply 才进入 `manuallyApplied`
- 测试已覆盖 review reject、second guardrail reject 与 fallback 不会误进入 manual apply

下一步更合理的动作是:

- 先把 A4 的生命周期展示作为前置能力合并稳定。
- 再为 `NoNameSafeOutputDraft` 写一份轻量设计或测试探针。
- 等 trace、mock、后端语义都能说明该草稿层不会自动写终态后,再评估是否进入正式实现。
- 先观察该探针在 review / 调试交接中是否足够表达安全输出草稿层
- 若要展示给 UI,应只做只读展示,不新增 apply scope
- 等 trace、mock、后端语义都能说明该草稿层不会自动写终态后,再评估是否进入正式实现
12 changes: 12 additions & 0 deletions docs/项目文档/03-规划/06-近期开发任务卡片.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,18 @@
- 降低后续试点时误扩权的风险
- 为下一阶段研发提供统一术语和边界

当前状态:

- 第一轮已完成
- `src/utils/noNameApplyLifecycle.ts` 已新增只读 `NoNameSafeOutputDraft` 探针,从现有 trace 的 `controlledOutputReviews`、`proposalTransitionLog`、`applyExecutionLog` 推导草稿状态
- 草稿状态目前限定为 `drafted / reviewed / guardrailAllowed / manuallyApplied / blocked / fallback`
- 探针明确记录 `evidence.finalPlotStateWriteAllowed=false`,不新增后端命令,不新增正式 apply scope,不写最终剧情状态
- 已补充测试覆盖 pending、人工批准、二次护栏允许、显式 manual apply、review reject、guardrail reject 与 fallback

已验证:

- `npm run test -- --run src/utils/noNameApplyLifecycle.test.ts`

## 3. 文档管理动作

### 新任务落地时至少同步
Expand Down
149 changes: 149 additions & 0 deletions src/utils/noNameApplyLifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { NoNameTrace } from '../types/game';
import {
buildNoNameApplyLifecycleCheckpoints,
buildNoNameApplyLifecycle,
buildNoNameSafeOutputDrafts,
formatNoNameApplyExecutionRecord,
summarizeNoNameApplyLifecycleCheckpoints,
summarizeNoNameApplyExecutions,
Expand Down Expand Up @@ -47,6 +48,87 @@ function buildTrace(outcome: string, note: string): NoNameTrace {
};
}

function buildReviewedDraftTrace(options: {
humanReviewDecision?: 'pending' | 'approvedForHigherApply' | 'rejectedForHigherApply';
secondGuardrail?: 'allow' | 'reject' | 'fallback' | null;
manualApply?: boolean;
} = {}): NoNameTrace {
const {
humanReviewDecision = 'pending',
secondGuardrail = null,
manualApply = false,
} = options;
const requestId = 'controlled-output-proposal-draft-plot_augmentation_hint';
const proposalId = 'proposal-draft';
const proposalTransitionLog = [`${requestId}:apply_intent:awaiting_second_guardrail`];
const applyExecutionLog = [{
target: 'plotAugmentationHint',
outcome: 'awaiting_second_guardrail',
}];

if (secondGuardrail) {
proposalTransitionLog.push(`${requestId}:second_guardrail:${secondGuardrail}`);
applyExecutionLog.push({
target: 'plotAugmentationHint',
outcome: secondGuardrail === 'allow'
? 'second_guardrail_allowed'
: secondGuardrail === 'fallback'
? 'second_guardrail_fallback'
: 'second_guardrail_rejected',
});
}
if (manualApply) {
proposalTransitionLog.push(`${requestId}:manual_apply:plot_augmentation_hint`);
applyExecutionLog.push({
target: 'plotAugmentationHint',
outcome: 'manual_plot_augmentation_hint_applied',
});
}

return {
traceId: 'trace-safe-output-draft',
sessionId: 'session-1',
turnId: 'turn-1',
mode: 'assisted',
graphPath: ['CollectTurnInput', 'PlanTurn', 'ApplyProposal'],
capabilityCalls: [],
proposals: [{
proposalId,
kind: 'plotCandidate',
producerRole: 'director',
title: 'draft proposal',
summary: 'stage a non-final safe output draft',
focus: 'safe draft focus',
targetSegment: 'current_turn_tail',
intendedEffect: 'guide the next generation without mutating final state',
rationale: 'test safe output draft',
labels: ['director'],
applyScopes: ['plotAugmentationHint'],
status: 'applied',
applyable: true,
}],
controlledOutputReviews: [{
requestId,
proposalId,
requestedKind: 'nonFinalPlotAugmentation',
decision: 'needsReview',
reason: 'plot augmentation draft requires explicit review',
normalizedKind: 'nonFinalPlotAugmentation',
safeApplyScope: 'plotAugmentationHint',
policyForbiddenScopes: ['finalPlotState', 'canonWorldFact'],
requiresHumanReview: true,
humanReviewDecision,
}],
proposalTransitionLog,
applyPlanLog: [],
applyExecutionLog,
guardrailResult: { outcome: 'accept' },
applyResult: { attempted: true, outcome: 'applied_scoped_outputs' },
fallbackUsed: false,
elapsedMs: 3,
};
}

describe('buildNoNameApplyLifecycle', () => {
it('summarizes the human review, second guardrail, and manual apply order', () => {
const trace: NoNameTrace = {
Expand Down Expand Up @@ -305,4 +387,71 @@ describe('buildNoNameApplyLifecycle', () => {
'剧情增强提示:已消费 [raw=plot_augmentation_hint:pending_plot_augmentation_consumed]',
);
});

it('builds safe output drafts without allowing final plot state writes', () => {
const [draft] = buildNoNameSafeOutputDrafts(buildReviewedDraftTrace());

expect(draft).toEqual(expect.objectContaining({
draftId: 'safe-output-draft-controlled-output-proposal-draft-plot_augmentation_hint',
sourceProposalId: 'proposal-draft',
outputKind: 'nonFinalPlotAugmentation',
safeApplyScope: 'plotAugmentationHint',
lifecycleState: 'drafted',
}));
expect(draft?.evidence).toEqual(expect.objectContaining({
reviewRequestId: 'controlled-output-proposal-draft-plot_augmentation_hint',
humanReviewDecision: 'pending',
secondGuardrailDecision: 'notEntered',
manualApplyRecorded: false,
finalPlotStateWriteAllowed: false,
}));
expect(draft?.evidence.reasons).toContain('draft probe never allows final plot state writes');
});

it('promotes safe output drafts only through explicit review and guardrail evidence', () => {
expect(buildNoNameSafeOutputDrafts(buildReviewedDraftTrace({
humanReviewDecision: 'approvedForHigherApply',
}))[0]?.lifecycleState).toBe('reviewed');

const [allowed] = buildNoNameSafeOutputDrafts(buildReviewedDraftTrace({
humanReviewDecision: 'approvedForHigherApply',
secondGuardrail: 'allow',
}));
expect(allowed?.lifecycleState).toBe('guardrailAllowed');
expect(allowed?.evidence.secondGuardrailDecision).toBe('allow');
expect(allowed?.evidence.finalPlotStateWriteAllowed).toBe(false);

const [applied] = buildNoNameSafeOutputDrafts(buildReviewedDraftTrace({
humanReviewDecision: 'approvedForHigherApply',
secondGuardrail: 'allow',
manualApply: true,
}));
expect(applied?.lifecycleState).toBe('manuallyApplied');
expect(applied?.evidence.manualApplyRecorded).toBe(true);
expect(applied?.evidence.finalPlotStateWriteAllowed).toBe(false);
});

it('keeps rejected and fallback safe output drafts out of manual apply', () => {
const [rejectedByReview] = buildNoNameSafeOutputDrafts(buildReviewedDraftTrace({
humanReviewDecision: 'rejectedForHigherApply',
}));
expect(rejectedByReview?.lifecycleState).toBe('blocked');
expect(rejectedByReview?.evidence.manualApplyRecorded).toBe(false);

const [rejectedByGuardrail] = buildNoNameSafeOutputDrafts(buildReviewedDraftTrace({
humanReviewDecision: 'approvedForHigherApply',
secondGuardrail: 'reject',
}));
expect(rejectedByGuardrail?.lifecycleState).toBe('blocked');
expect(rejectedByGuardrail?.evidence.secondGuardrailDecision).toBe('reject');
expect(rejectedByGuardrail?.evidence.finalPlotStateWriteAllowed).toBe(false);

const [fallback] = buildNoNameSafeOutputDrafts(buildReviewedDraftTrace({
humanReviewDecision: 'approvedForHigherApply',
secondGuardrail: 'fallback',
}));
expect(fallback?.lifecycleState).toBe('fallback');
expect(fallback?.evidence.manualApplyRecorded).toBe(false);
expect(fallback?.evidence.finalPlotStateWriteAllowed).toBe(false);
});
});
Loading
Loading