From b8e090440dda2573028afc0ceeb6647a75a2ce7b Mon Sep 17 00:00:00 2001 From: 1sjp Date: Sat, 16 May 2026 14:49:44 +0800 Subject: [PATCH] test(noname): add safe output draft probe --- ...60\347\212\266\350\257\204\344\274\260.md" | 4 +- ...55\350\267\257\347\272\277\345\233\276.md" | 1 + ...25\347\202\271\350\257\204\344\274\260.md" | 29 +-- ...73\345\212\241\345\215\241\347\211\207.md" | 12 ++ src/utils/noNameApplyLifecycle.test.ts | 149 ++++++++++++++++ src/utils/noNameApplyLifecycle.ts | 168 ++++++++++++++++++ 6 files changed, 351 insertions(+), 12 deletions(-) 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 701665c..3afa14d 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" @@ -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. 当前主要风险 @@ -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. 当前最合理的发展策略 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 e32c595..efcf859 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" @@ -141,6 +141,7 @@ - 第一轮评估已完成 - 当前只建议试点非最终状态的 pending / draft / diagnostics-like 输出层 - 不建议直接试点最终剧情、世界硬事实、NPC 隐藏意图或绕过 reviewed apply 的自动写入 +- 安全输出草稿层第一轮探针已完成:`NoNameSafeOutputDraft` 只读推导 `drafted / reviewed / guardrailAllowed / manuallyApplied / blocked / fallback`,并固定 `finalPlotStateWriteAllowed=false` - 评估文档见 [下一层安全输出接口试点评估](./05-下一层安全输出接口试点评估.md) ### 任务 8:评估多角色协作与更强知识检索 diff --git "a/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/05-\344\270\213\344\270\200\345\261\202\345\256\211\345\205\250\350\276\223\345\207\272\346\216\245\345\217\243\350\257\225\347\202\271\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/05-\344\270\213\344\270\200\345\261\202\345\256\211\345\205\250\350\276\223\345\207\272\346\216\245\345\217\243\350\257\225\347\202\271\350\257\204\344\274\260.md" index 52a2096..46d8f06 100644 --- "a/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/05-\344\270\213\344\270\200\345\261\202\345\256\211\345\205\250\350\276\223\345\207\272\346\216\245\345\217\243\350\257\225\347\202\271\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/05-\344\270\213\344\270\200\345\261\202\345\256\211\345\205\250\350\276\223\345\207\272\346\216\245\345\217\243\350\257\225\347\202\271\350\257\204\344\274\260.md" @@ -1,6 +1,6 @@ # 下一层安全输出接口试点评估 -更新时间:2026-04-21 +更新时间:2026-04-24 ## 1. 评估结论 @@ -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、后端语义都能说明该草稿层不会自动写终态后,再评估是否进入正式实现 diff --git "a/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/06-\350\277\221\346\234\237\345\274\200\345\217\221\344\273\273\345\212\241\345\215\241\347\211\207.md" "b/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/06-\350\277\221\346\234\237\345\274\200\345\217\221\344\273\273\345\212\241\345\215\241\347\211\207.md" index 448d03c..2d19997 100644 --- "a/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/06-\350\277\221\346\234\237\345\274\200\345\217\221\344\273\273\345\212\241\345\215\241\347\211\207.md" +++ "b/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/06-\350\277\221\346\234\237\345\274\200\345\217\221\344\273\273\345\212\241\345\215\241\347\211\207.md" @@ -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. 文档管理动作 ### 新任务落地时至少同步 diff --git a/src/utils/noNameApplyLifecycle.test.ts b/src/utils/noNameApplyLifecycle.test.ts index 47bc042..22fddbe 100644 --- a/src/utils/noNameApplyLifecycle.test.ts +++ b/src/utils/noNameApplyLifecycle.test.ts @@ -3,6 +3,7 @@ import type { NoNameTrace } from '../types/game'; import { buildNoNameApplyLifecycleCheckpoints, buildNoNameApplyLifecycle, + buildNoNameSafeOutputDrafts, formatNoNameApplyExecutionRecord, summarizeNoNameApplyLifecycleCheckpoints, summarizeNoNameApplyExecutions, @@ -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 = { @@ -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); + }); }); diff --git a/src/utils/noNameApplyLifecycle.ts b/src/utils/noNameApplyLifecycle.ts index a922073..2b5a47c 100644 --- a/src/utils/noNameApplyLifecycle.ts +++ b/src/utils/noNameApplyLifecycle.ts @@ -1,5 +1,7 @@ import type { NoNameApplyExecutionRecord, + NoNameApplyScope, + NoNameControlledOutputKind, NoNameHumanReviewDecision, NoNameTrace, } from '../types/game'; @@ -33,6 +35,34 @@ export interface NoNameApplyExecutionDisplay { outcomeLabel: string; } +export type NoNameSafeOutputDraftState = + | 'drafted' + | 'reviewed' + | 'guardrailAllowed' + | 'manuallyApplied' + | 'blocked' + | 'fallback'; + +export interface NoNameSafeOutputDraftEvidence { + reviewRequestId: string; + humanReviewDecision: NoNameHumanReviewDecision | 'notRequired'; + secondGuardrailDecision: 'allow' | 'reject' | 'fallback' | 'notEntered'; + manualApplyRecorded: boolean; + finalPlotStateWriteAllowed: false; + reasons: string[]; +} + +export interface NoNameSafeOutputDraft { + draftId: string; + sourceProposalId: string; + outputKind: NoNameControlledOutputKind; + safeApplyScope: NoNameApplyScope; + summary: string; + rationale: string; + lifecycleState: NoNameSafeOutputDraftState; + evidence: NoNameSafeOutputDraftEvidence; +} + interface NoNameSecondGuardrailStats { allowed: number; rejected: number; @@ -45,6 +75,22 @@ function normalizeTarget(value: string | undefined) { return (value ?? '').replace(/_/g, '').toLowerCase(); } +function scopeToTransitionSuffix(scope: NoNameApplyScope) { + if (scope === 'plotTextHint') { + return 'plot_text_hint'; + } + if (scope === 'chapterSummaryHint') { + return 'chapter_summary_hint'; + } + if (scope === 'optionBiasHint') { + return 'option_bias_hint'; + } + if (scope === 'plotAugmentationHint') { + return 'plot_augmentation_hint'; + } + return scope; +} + function hasExecution(trace: NoNameTrace, target: string, outcome: string) { const normalizedTarget = normalizeTarget(target); return Boolean(trace.applyExecutionLog?.some((item) => ( @@ -68,6 +114,53 @@ function latestExecutionForTarget( )) ?? null; } +function hasTransition(trace: NoNameTrace, transition: string) { + return trace.proposalTransitionLog?.includes(transition) ?? false; +} + +function hasManualApplyEvidenceForScope( + trace: NoNameTrace, + requestId: string, + scope: NoNameApplyScope, +) { + const scopeSuffix = scopeToTransitionSuffix(scope); + if (hasTransition(trace, `${requestId}:manual_apply:${scopeSuffix}`)) { + return true; + } + return Boolean(latestExecutionForTarget(trace, scope, [ + 'manual_plot_text_applied', + 'manual_chapter_summary_hint_applied', + 'manual_option_bias_hint_applied', + 'manual_plot_augmentation_hint_applied', + ])); +} + +function resolveDraftSecondGuardrailDecision( + trace: NoNameTrace, + requestId: string, + scope: NoNameApplyScope, +) { + if ( + hasTransition(trace, `${requestId}:second_guardrail:allow`) + || latestExecutionForTarget(trace, scope, ['second_guardrail_allowed']) + ) { + return 'allow' as const; + } + if ( + hasTransition(trace, `${requestId}:second_guardrail:reject`) + || latestExecutionForTarget(trace, scope, ['second_guardrail_rejected']) + ) { + return 'reject' as const; + } + if ( + hasTransition(trace, `${requestId}:second_guardrail:fallback`) + || latestExecutionForTarget(trace, scope, ['second_guardrail_fallback']) + ) { + return 'fallback' as const; + } + return 'notEntered' as const; +} + function countTransitions(trace: NoNameTrace, suffix: string) { return trace.proposalTransitionLog?.filter((item) => item.endsWith(suffix)).length ?? 0; } @@ -491,6 +584,81 @@ export function summarizeNoNamePendingPlotAugmentation(trace: NoNameTrace): stri return '无'; } +export function buildNoNameSafeOutputDrafts( + trace: NoNameTrace, + reviewDecisions: Record = {}, +): NoNameSafeOutputDraft[] { + const reviews = trace.controlledOutputReviews ?? []; + return reviews + .filter((review) => review.safeApplyScope) + .map((review) => { + const safeApplyScope = review.safeApplyScope as NoNameApplyScope; + const proposal = trace.proposals.find((item) => item.proposalId === review.proposalId) + ?? trace.proposals[trace.proposals.length - 1]; + const sourceProposalId = review.proposalId + ?? proposal?.proposalId + ?? review.requestId; + const humanReviewDecision = review.requiresHumanReview + ? lifecycleHumanDecision(trace, review.requestId, reviewDecisions) + : 'notRequired'; + const secondGuardrailDecision = resolveDraftSecondGuardrailDecision( + trace, + review.requestId, + safeApplyScope, + ); + const manualApplyRecorded = hasManualApplyEvidenceForScope( + trace, + review.requestId, + safeApplyScope, + ); + const reasons: string[] = [ + `controlled output review decision=${review.decision}`, + `safe apply scope=${safeApplyScope}`, + 'draft probe never allows final plot state writes', + ]; + + let lifecycleState: NoNameSafeOutputDraftState = 'drafted'; + if (humanReviewDecision === 'rejectedForHigherApply' || secondGuardrailDecision === 'reject') { + lifecycleState = 'blocked'; + reasons.push('review or second guardrail blocked higher-layer apply'); + } else if (secondGuardrailDecision === 'fallback') { + lifecycleState = 'fallback'; + reasons.push('second guardrail requested fallback instead of manual apply'); + } else if (manualApplyRecorded) { + lifecycleState = 'manuallyApplied'; + reasons.push('explicit manual apply evidence is recorded'); + } else if (secondGuardrailDecision === 'allow') { + lifecycleState = 'guardrailAllowed'; + reasons.push('second guardrail allowed explicit manual apply only'); + } else if (humanReviewDecision === 'approvedForHigherApply') { + lifecycleState = 'reviewed'; + reasons.push('human review approved the draft for second guardrail'); + } else if (humanReviewDecision === 'notRequired' && review.decision === 'allow') { + reasons.push('low-risk controlled output can stay drafted without higher-layer apply'); + } else { + reasons.push('waiting for explicit human review or guardrail evidence'); + } + + return { + draftId: `safe-output-draft-${review.requestId}`, + sourceProposalId, + outputKind: review.normalizedKind ?? review.requestedKind, + safeApplyScope, + summary: proposal?.summary ?? review.reason, + rationale: review.reason, + lifecycleState, + evidence: { + reviewRequestId: review.requestId, + humanReviewDecision, + secondGuardrailDecision, + manualApplyRecorded, + finalPlotStateWriteAllowed: false, + reasons, + }, + }; + }); +} + export function buildNoNameApplyLifecycle( trace: NoNameTrace, reviewDecisions: Record = {},