diff --git "a/docs/\351\241\271\347\233\256\346\226\207\346\241\243/02-\346\236\266\346\236\204/02-NoName-\344\273\243\347\220\206\347\263\273\347\273\237.md" "b/docs/\351\241\271\347\233\256\346\226\207\346\241\243/02-\346\236\266\346\236\204/02-NoName-\344\273\243\347\220\206\347\263\273\347\273\237.md" index 7724e71..0764871 100644 --- "a/docs/\351\241\271\347\233\256\346\226\207\346\241\243/02-\346\236\266\346\236\204/02-NoName-\344\273\243\347\220\206\347\263\273\347\273\237.md" +++ "b/docs/\351\241\271\347\233\256\346\226\207\346\241\243/02-\346\236\266\346\236\204/02-NoName-\344\273\243\347\220\206\347\263\273\347\273\237.md" @@ -73,7 +73,7 @@ ### 5.1 命令层压力正在下降 -`reviewed apply` 的请求构造、快照校验、显式 apply、人工 review intent 与 second guardrail 记录逻辑已经下沉到 `noname_apply.rs`。 +`reviewed apply` 的 request input 构造、快照校验、显式 apply、人工复核结果写回、review intent、second guardrail 预校验与记录逻辑已经下沉到 `noname_apply.rs`。 `tauri_commands.rs` 在这条链路上主要保留: @@ -82,7 +82,7 @@ - game engine / plot state 读取与回写 - 命令错误返回 -后续仍可继续观察低风险 apply 与生成主链中是否还有适合抽离的重复编排,但不应再把新的 reviewed apply 权限逻辑直接堆回命令层。 +这意味着 reviewed apply 的命令层已经进一步变成薄壳,本轮 A2/A3 可视为阶段性收口完成。后续仍可继续观察低风险 apply 与生成主链中是否还有适合抽离的重复编排,但不应再把新的 reviewed apply 权限逻辑直接堆回命令层。 ### 5.2 Web mock 与后端容易漂移 diff --git "a/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/01-\347\216\260\347\212\266\350\257\204\344\274\260.md" "b/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/01-\347\216\260\347\212\266\350\257\204\344\274\260.md" index a3f4f8b..d63975b 100644 --- "a/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/01-\347\216\260\347\212\266\350\257\204\344\274\260.md" +++ "b/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/01-\347\216\260\347\212\266\350\257\204\344\274\260.md" @@ -24,13 +24,14 @@ `tauri_commands.rs` 仍然承担大量主游戏编排工作,继续向里面叠逻辑会提高维护风险和冲突概率。 -当前 `NoName reviewed apply` 链路已经完成第三轮减压: +当前 `NoName reviewed apply` 链路已经完成第五轮减压: - reviewed apply request 构造、快照校验与显式 apply 已下沉到 `noname_apply.rs` - trace 获取 / plot state 更新 / trace 回写已复用命令层共用壳层 -- human review intent 与 second guardrail 决策记录已下沉到 `noname_apply.rs` +- human review intent、人工复核结果写回与 second guardrail 预校验 / 决策记录已下沉到 `noname_apply.rs` +- `apply_noname_reviewed_output` 的 request input 组装也已回收到 `noname_apply.rs` -后续风险重点不再是 reviewed apply 本身,而是其他低风险 apply 与剧情生成主链是否继续膨胀。 +因此 A2/A3 在当前主开发线可以视为阶段性完成。后续风险重点不再是 reviewed apply 本身,而是其他低风险 apply 与剧情生成主链是否继续膨胀。 ### 3.2 前后端 mock 语义漂移 diff --git "a/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/02-\345\220\216\347\273\255\350\267\257\347\272\277\345\233\276.md" "b/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/02-\345\220\216\347\273\255\350\267\257\347\272\277\345\233\276.md" index ade044e..0167de4 100644 --- "a/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/02-\345\220\216\347\273\255\350\267\257\347\272\277\345\233\276.md" +++ "b/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/02-\345\220\216\347\273\255\350\267\257\347\272\277\345\233\276.md" @@ -53,12 +53,13 @@ 当前状态: -- 已完成第三轮下沉 +- 已完成第五轮下沉 - reviewed apply 的 request builder 与 plotText manual apply helper 已进入 `noname_apply.rs` - `tauri_commands.rs` 在 reviewed apply 路径上已减少内联快照构造与包装逻辑 - reviewed apply 命令已开始复用共享的 trace 读取、plot state 更新和 trace 回写壳层 -- human review intent 与 second guardrail 决策记录已进入 `noname_apply.rs` -- 下一步若继续减压,应优先评估低风险 apply 或剧情生成主链中仍重复的编排,而不是扩大新的 apply scope +- human review intent、人工复核结果写回与 second guardrail 预校验 / 决策记录已进入 `noname_apply.rs` +- `apply_noname_reviewed_output` 的 request input 组装已回收到 `noname_apply.rs` +- 本轮后 A2/A3 可视为阶段性完成;下一步若继续减压,应优先评估低风险 apply 或剧情生成主链中仍重复的编排,而不是扩大新的 apply scope ## P2:收口后继续推进 diff --git "a/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/04-\346\234\252\346\235\245\344\270\244\345\221\250\345\274\200\345\217\221\346\216\222\346\234\237.md" "b/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/04-\346\234\252\346\235\245\344\270\244\345\221\250\345\274\200\345\217\221\346\216\222\346\234\237.md" index 52c5c54..e1d119d 100644 --- "a/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/04-\346\234\252\346\235\245\344\270\244\345\221\250\345\274\200\345\217\221\346\216\222\346\234\237.md" +++ "b/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/04-\346\234\252\346\235\245\344\270\244\345\221\250\345\274\200\345\217\221\346\216\222\346\234\237.md" @@ -91,11 +91,12 @@ 当前状态: -- 第三轮已完成 +- 第五轮已完成 - `NoNameReviewedApplyRequest` 构造与 plotText manual apply helper 已从命令层下沉到 `noname_apply.rs` - trace 获取 / result 回写的共用壳层已开始抽出并复用到两个 reviewed apply 命令 -- human review intent 与 second guardrail 决策记录已从命令层下沉到 `noname_apply.rs` -- 下一轮若继续 A2,应只评估剩余低风险 apply / 生成主链重复编排,不新增 apply scope,不扩张 second guardrail 语义 +- human review intent、人工复核结果写回与 second guardrail 预校验 / 决策记录已从命令层下沉到 `noname_apply.rs` +- `apply_noname_reviewed_output` 的 request input 组装已回收到 `noname_apply.rs` +- 当前 A2 在主开发线已阶段性完成;后续若继续 A2,应只评估剩余低风险 apply / 生成主链重复编排,不新增 apply scope,不扩张 second guardrail 语义 ### 任务 A3:建立 `NoName` 主链变更的文档闭环 @@ -113,6 +114,12 @@ - 最小验证: - 文档自查,无失效路径引用 +当前状态: + +- `NoName` 架构、现状评估、后续路线图已同步 reviewed apply 最新收口进度 +- 两周排期文档已记录 A2 的第五轮下沉结果,后续 review 不再依赖口头补充 +- 后续若继续推进主开发线,应维持“改主链实现就同步三份主文档和排期状态”的节奏 + ## 4.2 协作者线 ### 任务 B1:记忆检索排序解释性增强 diff --git a/src-tauri/src/noname_apply.rs b/src-tauri/src/noname_apply.rs index bc7f060..35d85ce 100644 --- a/src-tauri/src/noname_apply.rs +++ b/src-tauri/src/noname_apply.rs @@ -1,3 +1,4 @@ +use crate::noname_output_interface::NoNameControlledOutputDecision; use crate::noname_trace::{NoNameHumanReviewDecision, NoNameSecondGuardrailDecision, NoNameTrace}; use crate::noname_types::{ NoNameApplyScope, NoNameMode, NoNameProposal, NoNameProposalStatus, NoNameTargetSegment, @@ -171,6 +172,13 @@ fn next_apply_plan_order(trace: &NoNameTrace) -> u32 { + 1 } +fn find_controlled_output_review_index(trace: &NoNameTrace, request_id: &str) -> Option { + trace + .controlled_output_reviews + .iter() + .position(|review| review.request_id == request_id) +} + fn find_review_proposal<'a>( trace: &'a NoNameTrace, request_id: &str, @@ -316,6 +324,45 @@ fn human_review_apply_intent_rejection_reason( } } +pub fn apply_human_review_decision_to_trace( + trace: &mut NoNameTrace, + request_id: &str, + decision: NoNameHumanReviewDecision, + reviewed_at: u64, +) -> Result<(), String> { + let review_index = find_controlled_output_review_index(trace, request_id) + .ok_or_else(|| format!("NoName controlled output review not found: {}", request_id))?; + + let safe_apply_scope = { + let review = &trace.controlled_output_reviews[review_index]; + if review.decision != NoNameControlledOutputDecision::NeedsReview + || !review.requires_human_review + { + return Err(format!( + "NoName controlled output review does not require human review: {}", + request_id + )); + } + + review.safe_apply_scope.ok_or_else(|| { + format!( + "NoName controlled output review has no safe apply scope: {}", + request_id + ) + })? + }; + + let review = &mut trace.controlled_output_reviews[review_index]; + review.human_review_decision = Some(decision); + review.human_reviewed_at = Some(reviewed_at); + review.human_review_note = Some(decision.note().to_string()); + + trace.record_proposal_transition(format!("{}:human_review:{}", request_id, decision.as_str())); + record_human_review_apply_intent(trace, request_id, decision, Some(safe_apply_scope)); + + Ok(()) +} + pub fn record_human_review_apply_intent( trace: &mut NoNameTrace, request_id: &str, @@ -502,6 +549,32 @@ pub fn record_second_guardrail_decision( Ok(()) } +pub fn resolve_second_guardrail_for_trace( + trace: &mut NoNameTrace, + request_id: &str, + decision: NoNameSecondGuardrailDecision, +) -> Result<(), String> { + let safe_apply_scope = trace + .controlled_output_reviews + .iter() + .find(|review| review.request_id == request_id) + .ok_or_else(|| format!("NoName controlled output review not found: {}", request_id)) + .and_then(|review| { + if review.human_review_decision + != Some(NoNameHumanReviewDecision::ApprovedForHigherApply) + { + return Err(format!( + "NoName controlled output review is not approved for second guardrail: {}", + request_id + )); + } + + Ok(review.safe_apply_scope) + })?; + + record_second_guardrail_decision(trace, request_id, decision, safe_apply_scope) +} + fn trace_has_second_guardrail_allow( trace: &NoNameTrace, request_id: &str, @@ -909,6 +982,15 @@ pub fn apply_reviewed_output_to_plot_state( } } +pub fn apply_reviewed_output_input_to_plot_state( + trace: NoNameTrace, + input: NoNameReviewedApplyRequestInput, + plot_state: PlotState, +) -> Result { + let request = build_reviewed_apply_request(input)?; + apply_reviewed_output_to_plot_state(trace, request, plot_state) +} + pub fn apply_manual_plot_text_hint_to_plot_state( trace: NoNameTrace, request_id: &str, @@ -1140,6 +1222,67 @@ mod tests { .any(|entry| entry.ends_with(":apply_intent:awaiting_second_guardrail"))); } + #[test] + fn apply_human_review_decision_to_trace_updates_review_and_logs() { + let (mut trace, request_id) = make_trace_for_plot_text_review(); + + apply_human_review_decision_to_trace( + &mut trace, + &request_id, + NoNameHumanReviewDecision::ApprovedForHigherApply, + 123, + ) + .expect("human review decision should be applied to trace"); + + let review = &trace.controlled_output_reviews[0]; + assert_eq!( + review.human_review_decision, + Some(NoNameHumanReviewDecision::ApprovedForHigherApply) + ); + assert_eq!(review.human_reviewed_at, Some(123)); + assert_eq!( + review.human_review_note.as_deref(), + Some(NoNameHumanReviewDecision::ApprovedForHigherApply.note()) + ); + assert!(trace.proposal_transition_log.iter().any(|entry| { + entry + == &format!( + "{}:human_review:{}", + request_id, + NoNameHumanReviewDecision::ApprovedForHigherApply.as_str() + ) + })); + assert!(trace.apply_execution_log.iter().any(|entry| { + entry.target == "plot_text_hint" && entry.outcome == "awaiting_second_guardrail" + })); + } + + #[test] + fn apply_human_review_decision_to_trace_rejects_missing_safe_scope_without_mutation() { + let (mut trace, request_id) = make_trace_for_plot_text_review(); + trace.controlled_output_reviews[0].safe_apply_scope = None; + + let error = apply_human_review_decision_to_trace( + &mut trace, + &request_id, + NoNameHumanReviewDecision::ApprovedForHigherApply, + 456, + ) + .expect_err("missing safe apply scope should fail"); + + assert_eq!( + error, + format!( + "NoName controlled output review has no safe apply scope: {}", + request_id + ) + ); + let review = &trace.controlled_output_reviews[0]; + assert_eq!(review.human_review_decision, None); + assert_eq!(review.human_reviewed_at, None); + assert_eq!(review.human_review_note, None); + } + #[test] fn human_review_apply_intent_rejects_non_second_guardrail_scope() { let (mut trace, request_id) = make_trace_for_summary_apply(); @@ -1190,6 +1333,49 @@ mod tests { ); } + #[test] + fn resolve_second_guardrail_for_trace_requires_review_approval() { + let (mut trace, request_id) = make_trace_for_plot_text_review(); + + let error = resolve_second_guardrail_for_trace( + &mut trace, + &request_id, + NoNameSecondGuardrailDecision::Allow, + ) + .expect_err("second guardrail should require approved human review"); + + assert_eq!( + error, + format!( + "NoName controlled output review is not approved for second guardrail: {}", + request_id + ) + ); + } + + #[test] + fn resolve_second_guardrail_for_trace_records_allow() { + let (mut trace, request_id) = make_trace_for_plot_text_review(); + apply_human_review_decision_to_trace( + &mut trace, + &request_id, + NoNameHumanReviewDecision::ApprovedForHigherApply, + 789, + ) + .expect("human review should prepare second guardrail"); + + resolve_second_guardrail_for_trace( + &mut trace, + &request_id, + NoNameSecondGuardrailDecision::Allow, + ) + .expect("second guardrail allow should be recorded"); + + assert!(trace.apply_execution_log.iter().any(|entry| { + entry.target == "plot_text_hint" && entry.outcome == "second_guardrail_allowed" + })); + } + #[test] fn second_guardrail_allows_plot_augmentation_hint() { let (mut trace, request_id) = make_trace_for_plot_augmentation_apply(); @@ -1283,6 +1469,34 @@ mod tests { .any(|entry| entry.ends_with(":manual_apply:chapter_summary_hint"))); } + #[test] + fn reviewed_apply_input_can_write_chapter_summary_hint() { + let (trace, request_id) = make_trace_for_summary_apply(); + let plot_state = make_plot_state("existing summary"); + + let outcome = apply_reviewed_output_input_to_plot_state( + trace, + NoNameReviewedApplyRequestInput { + request_id, + scope: NoNameApplyScope::ChapterSummaryHint, + chapter_index: Some(1), + segment_index: None, + expected_segment_text: None, + expected_summary: Some("existing summary".to_string()), + expected_generation_diagnostics: None, + expected_plot_augmentation_hints: None, + }, + plot_state, + ) + .expect("summary hint should be manually applied from request input"); + + assert!(outcome + .plot_state + .current_chapter + .summary + .starts_with("NoName summary hint: sect crisis; existing summary")); + } + #[test] fn reviewed_apply_rejects_stale_chapter_summary_snapshot() { let (trace, request_id) = make_trace_for_summary_apply(); diff --git a/src-tauri/src/tauri_commands.rs b/src-tauri/src/tauri_commands.rs index b8aa8dd..ab78fa5 100644 --- a/src-tauri/src/tauri_commands.rs +++ b/src-tauri/src/tauri_commands.rs @@ -15,16 +15,15 @@ use crate::llm_runtime_config::{ use crate::llm_service::{LLMConfig, LLMRequest, LLMService}; use crate::memory_layers::{ChapterSummary, MemoryEntry, WorldFact}; use crate::noname_apply::{ - apply_manual_plot_text_hint_to_plot_state, apply_reviewed_output_to_plot_state, - build_reviewed_apply_request, record_human_review_apply_intent, - record_second_guardrail_decision, NoNameReviewedApplyRequestInput, + apply_human_review_decision_to_trace, apply_manual_plot_text_hint_to_plot_state, + apply_reviewed_output_input_to_plot_state, resolve_second_guardrail_for_trace, + NoNameReviewedApplyRequestInput, }; use crate::noname_config::NoNameConfig; use crate::noname_context_builder::build_context_packet; use crate::noname_context_types::NoNameContextBuildInput; use crate::noname_guardrails::{NoNameDirectorGuardrailInput, NoNameGuardrailResult}; use crate::noname_memory_manager::NoNameMemoryManager; -use crate::noname_output_interface::NoNameControlledOutputDecision; use crate::noname_roles::NoNameDirectorObservation; use crate::noname_runtime::{NoNameDirectorRunResult, NoNameRuntime, NoNameTurnInput}; use crate::noname_trace::{NoNameHumanReviewDecision, NoNameSecondGuardrailDecision, NoNameTrace}; @@ -4250,37 +4249,7 @@ pub async fn mark_noname_controlled_output_review( .duration_since(UNIX_EPOCH) .map(|duration| duration.as_secs()) .unwrap_or_default(); - let safe_apply_scope = { - let review = trace - .controlled_output_reviews - .iter_mut() - .find(|review| review.request_id == request_id) - .ok_or_else(|| format!("NoName controlled output review not found: {}", request_id))?; - - if review.decision != NoNameControlledOutputDecision::NeedsReview - || !review.requires_human_review - { - return Err(format!( - "NoName controlled output review does not require human review: {}", - request_id - )); - } - - review.human_review_decision = Some(decision); - review.human_reviewed_at = Some(reviewed_at); - review.human_review_note = Some(decision.note().to_string()); - review.safe_apply_scope - }; - - if safe_apply_scope.is_none() { - return Err(format!( - "NoName controlled output review has no safe apply scope: {}", - request_id - )); - } - - trace.record_proposal_transition(format!("{}:human_review:{}", request_id, decision.as_str())); - record_human_review_apply_intent(&mut trace, &request_id, decision, safe_apply_scope); + apply_human_review_decision_to_trace(&mut trace, &request_id, decision, reviewed_at)?; if !runtime.replace_trace(trace.clone()) { return Err(format!("NoName trace not found: {}", trace_id)); @@ -4305,24 +4274,7 @@ pub async fn resolve_noname_second_guardrail( .find(|trace| trace.trace_id == trace_id) .ok_or_else(|| format!("NoName trace not found: {}", trace_id))?; - let safe_apply_scope = { - let review = trace - .controlled_output_reviews - .iter() - .find(|review| review.request_id == request_id) - .ok_or_else(|| format!("NoName controlled output review not found: {}", request_id))?; - - if review.human_review_decision != Some(NoNameHumanReviewDecision::ApprovedForHigherApply) { - return Err(format!( - "NoName controlled output review is not approved for second guardrail: {}", - request_id - )); - } - - review.safe_apply_scope - }; - - record_second_guardrail_decision(&mut trace, &request_id, decision, safe_apply_scope)?; + resolve_second_guardrail_for_trace(&mut trace, &request_id, decision)?; if !runtime.replace_trace(trace.clone()) { return Err(format!("NoName trace not found: {}", trace_id)); @@ -4366,18 +4318,21 @@ pub async fn apply_noname_reviewed_output( expected_plot_augmentation_hints: Option>, engine: State<'_, Mutex>, ) -> Result { - let request = build_reviewed_apply_request(NoNameReviewedApplyRequestInput { - request_id, - scope, - chapter_index, - segment_index, - expected_segment_text, - expected_summary, - expected_generation_diagnostics, - expected_plot_augmentation_hints, - })?; apply_noname_outcome_with_engine(&trace_id, &engine, |trace, plot_state| { - apply_reviewed_output_to_plot_state(trace, request, plot_state) + apply_reviewed_output_input_to_plot_state( + trace, + NoNameReviewedApplyRequestInput { + request_id, + scope, + chapter_index, + segment_index, + expected_segment_text, + expected_summary, + expected_generation_diagnostics, + expected_plot_augmentation_hints, + }, + plot_state, + ) }) } @@ -5082,6 +5037,7 @@ mod tests { use super::*; use crate::event_log::{EventImportance, GameEvent}; use crate::models::{CultivationRealm, Element, Grade, SpiritualRoot}; + use crate::noname_output_interface::NoNameControlledOutputDecision; use crate::script::{InitialState, Location, ScriptType, WorldSetting}; use std::sync::{Mutex as TestMutex, OnceLock as TestOnceLock}; use tempfile::tempdir;