feat(ctxmgr): wildcard session allowlist for semantic replace rollout
- SEMANTIC_SUMMARY_REPLACE_SESSIONS supports * (all sessions, present and future), mirroring INTENT_PLAN_PROJECT_ALLOWLIST; empty allowlist still replaces nothing (fail-closed defense in depth unchanged) - declare SEMANTIC_SUMMARY_REPLACE(_SESSIONS) in dev/test/main compose app env (stale Wave2 "REPLACE unwired" comment fixed); extend A-4 declaration test; .env.example key docs updated - sysadmin feature catalog: SESSIONS description + REPLACE reason note * Prepares dev degraded-flags enable (task degraded-flags-enable-2026-09-26-r4): user-authorized all-session replace, plan tier confirm, shadow summary, PI change-report narrative; env ops and runtime verification follow.
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
# 降级功能开关放量 dev(short-term)
|
||||
|
||||
- **task_id**: degraded-flags-enable-2026-09-26-r4
|
||||
- **owner**: pi
|
||||
- **status**: in_progress
|
||||
- **授权来源**: 用户会话指令「请将已经实现但是降级了的功能都启动」+ 追加确认(① dev 单二进制启动、不用容器;② SEMANTIC_SUMMARY_REPLACE 面向系统全部会话含后续新建;③ MULTI_INTENT_PLAN_TIER 保持 confirm)。该指令同时构成 docgov-pi-narrative 行「dev 常驻开启另授权」与 chat-semantic-replace 行「放量逐环境另授权(dev)」的显式授权。
|
||||
- **write_scope**: internal/ctxmgr, internal/sysadmin, internal/config, docker-compose.{dev,test,main}.yml, .env.example, .doc/ops/CURRENT.md, .doc/ops/short-term, .doc/status/FEATURE-MATRIX.md, .doc/test-reports
|
||||
- **mutex**: summary-flags-rollout
|
||||
- **base_dev_sha**: 66ea4e3e513d5c822e23a6c887ca16d1eab80d66
|
||||
- **capabilities**: git:dev, env:dev, docs:publish
|
||||
- **policy_revision**: AGENTS 2026-09 / agent-worktree-workflow r1 / validation-contract 2026-09-21
|
||||
- **review_state**: pending
|
||||
- **integration_state**: pending
|
||||
- **deployment**: pending
|
||||
- **cleanup**: pending
|
||||
- **remote_sync**: not_requested(本地 dev 领先 origin/dev 192+,未要求 push)
|
||||
|
||||
## 一、范围与现状
|
||||
|
||||
用户面板(sysadmin features 目录)为 fallback 快照;dev 实际运行态 = 单二进制 `launch.sh --no-docker` 只加载根 `.env`(不读 configs/.env.dev),故 intentflow 系列与摘要/叙事开关全部落在默认 legacy/false。dev 容器组当前未运行;test/main 仍为 compose。
|
||||
|
||||
已实现但未启动的降级项及处置:
|
||||
|
||||
| 开关 | 处置 | 依据 |
|
||||
|---|---|---|
|
||||
| MULTI_INTENT_PLAN_TIER(legacy→confirm)+ INTENT_PLAN_PROJECT_ALLOWLIST=* | 根 .env 写入 confirm + * | I2/I3 已按 allowlist 验收 shadow→auto_read→confirm(项目 6579,确认前零写);用户确认保持 confirm |
|
||||
| MULTI_INTENT_U9/U10/U11/U12 | 根 .env 全 true | I3 live + P8 逐键接线;域名 flag 开启后未打开的细粒度键保持 dry-run |
|
||||
| SEMANTIC_SUMMARY_SHADOW | 根 .env true | D3 收口 PASS;shadow 生成不影响 served Narrative |
|
||||
| SEMANTIC_SUMMARY_REPLACE(+SESSIONS) | **代码:SESSIONS 支持 `*` 通配**(原仅枚举 ID,无法覆盖"全部会话含后续");compose 三环境补声明(原 Wave2 注释"REPLACE 未接线"已过期);根 .env true + `*` | chat-semantic-replace 2026-09-26 fail-closed 灰度接线;用户授权全会话 |
|
||||
| PI_CHANGE_REPORT_NARRATIVE | 根 .env true | 2026-09-23 收口复测 PASS;matrix 注明 dev 常驻开启另授权,本任务授权来源满足 |
|
||||
|
||||
不改动:WORKFLOW_SCOPE_MODE(strict 可用);U11/U12 五个逐键 live flag(P8"禁止 full 一次放量",不在本次面板范围,compose 亦未声明,保持 off 待后续运维侧逐键放量)。
|
||||
|
||||
防泄漏:根 `.env` 经 `--env-file` 同样被 test/main compose 读取,故在 `configs/.env.test` / `configs/.env.main`(git-ignored 运维配置)显式钉住上述键为默认值,行为保持不变。
|
||||
|
||||
## 二、实现(代码侧)
|
||||
|
||||
1. `internal/ctxmgr/summary_flags.go`:`SummaryFeatureFlags.ReplaceAllSessions`;`parseReplaceSessions` 返回 `(set, wildcard)`,任意 `*` token → 全会话;`SemanticReplaceEnabledFor` 先过 master 再过通配/名单。空名单仍拒绝一切(纵深防御不变)。
|
||||
2. 单测:`semantic_replace_test.go` 更新 FlagParsing(通配/混合/空)+ 新增 WildcardAllowlistSwapsNarrative / WildcardMasterOffKeepsDeterministic。
|
||||
3. compose ×3 声明 `SEMANTIC_SUMMARY_REPLACE(_SESSIONS)`(默认 false/空);A-4 声明测试扩键;`.env.example` 修 Wave2 过期注释并补 SESSIONS/ALLOWLIST 注释键;sysadmin SESSIONS 描述与 REPLACE reason 注明 `*`。
|
||||
|
||||
## 三、计划
|
||||
|
||||
1. [x] 分析与授权确认
|
||||
2. [x] 代码实现(通配 + compose 声明 + 目录/示例更新)
|
||||
3. [ ] 工程门禁(arch-check block / go vet / go test -short)+ dev 集成(--no-ff)
|
||||
4. [ ] env 运维:根 .env + configs/.env.{test,main} 钉默认;`launch.sh --no-docker` 重启 dev(env:dev 锁内)
|
||||
5. [ ] 运行时验收:/health /ready、features 快照、行为抽查
|
||||
6. [ ] 测试报告 + MATRIX 行注记 + CURRENT 收口
|
||||
|
||||
## 四、结果登记
|
||||
|
||||
(待填)
|
||||
+7
-2
@@ -125,16 +125,21 @@ JEV_BASE_URL=http://127.0.0.1:8767
|
||||
JEV_API_KEY=
|
||||
WORKFLOW_INTENTS_ENABLED=true
|
||||
MULTI_INTENT_ENABLED=true
|
||||
# MULTI_INTENT_PLAN_TIER defaults to legacy; do not raise for U9–U12 scaffolding.
|
||||
# MULTI_INTENT_PLAN_TIER defaults to legacy; elevated tiers additionally need
|
||||
# INTENT_PLAN_PROJECT_ALLOWLIST (empty denies; * allows every project).
|
||||
# MULTI_INTENT_PLAN_TIER=legacy
|
||||
# INTENT_PLAN_PROJECT_ALLOWLIST=
|
||||
MULTI_INTENT_U9_SEGMENT=false
|
||||
MULTI_INTENT_U10_MUTATE=false
|
||||
MULTI_INTENT_U11_DOCGOV=false
|
||||
MULTI_INTENT_U12_RUN_CONTROL=false
|
||||
# 工作流作用域:strict 命中他流即拦截;weighted 按关系权重打分,验收通过后再开。
|
||||
WORKFLOW_SCOPE_MODE=strict
|
||||
# W2: shadow only. Do not set SEMANTIC_SUMMARY_REPLACE=true; REPLACE is unwired.
|
||||
# Semantic summary: shadow generation + fail-closed REPLACE gray release.
|
||||
# REPLACE needs master + session allowlist; empty allowlist replaces nothing,
|
||||
# "*" covers every session (present and future).
|
||||
SEMANTIC_SUMMARY_SHADOW=false
|
||||
SEMANTIC_SUMMARY_REPLACE=false
|
||||
# SEMANTIC_SUMMARY_REPLACE_SESSIONS=
|
||||
# W3: optional PI change-report narrative; default off.
|
||||
PI_CHANGE_REPORT_NARRATIVE=false
|
||||
|
||||
@@ -144,8 +144,11 @@ services:
|
||||
MULTI_INTENT_U11_DOCGOV: ${MULTI_INTENT_U11_DOCGOV:-false}
|
||||
MULTI_INTENT_U12_RUN_CONTROL: ${MULTI_INTENT_U12_RUN_CONTROL:-false}
|
||||
WORKFLOW_SCOPE_MODE: ${WORKFLOW_SCOPE_MODE:-strict}
|
||||
# W2 PU6: shadow semantic summary only. REPLACE stays unwired and is not declared here.
|
||||
# Semantic summary: shadow generation + fail-closed REPLACE gray release.
|
||||
# REPLACE needs master + session allowlist (empty = none; * = all sessions).
|
||||
SEMANTIC_SUMMARY_SHADOW: ${SEMANTIC_SUMMARY_SHADOW:-false}
|
||||
SEMANTIC_SUMMARY_REPLACE: ${SEMANTIC_SUMMARY_REPLACE:-false}
|
||||
SEMANTIC_SUMMARY_REPLACE_SESSIONS: ${SEMANTIC_SUMMARY_REPLACE_SESSIONS:-}
|
||||
PI_CHANGE_REPORT_NARRATIVE: ${PI_CHANGE_REPORT_NARRATIVE:-false}
|
||||
# FAV U7: keep publisher off until separate approval (FAV_DEGRADED_READ declared above)
|
||||
FAV_PUBLISHER_ENABLED: ${FAV_PUBLISHER_ENABLED:-false}
|
||||
|
||||
@@ -143,6 +143,8 @@ services:
|
||||
MULTI_INTENT_U12_RUN_CONTROL: ${MULTI_INTENT_U12_RUN_CONTROL:-false}
|
||||
WORKFLOW_SCOPE_MODE: ${WORKFLOW_SCOPE_MODE:-strict}
|
||||
SEMANTIC_SUMMARY_SHADOW: ${SEMANTIC_SUMMARY_SHADOW:-false}
|
||||
SEMANTIC_SUMMARY_REPLACE: ${SEMANTIC_SUMMARY_REPLACE:-false}
|
||||
SEMANTIC_SUMMARY_REPLACE_SESSIONS: ${SEMANTIC_SUMMARY_REPLACE_SESSIONS:-}
|
||||
PI_CHANGE_REPORT_NARRATIVE: ${PI_CHANGE_REPORT_NARRATIVE:-false}
|
||||
FAV_PUBLISHER_ENABLED: "false"
|
||||
ports:
|
||||
|
||||
@@ -139,6 +139,8 @@ services:
|
||||
MULTI_INTENT_U12_RUN_CONTROL: ${MULTI_INTENT_U12_RUN_CONTROL:-false}
|
||||
WORKFLOW_SCOPE_MODE: ${WORKFLOW_SCOPE_MODE:-strict}
|
||||
SEMANTIC_SUMMARY_SHADOW: ${SEMANTIC_SUMMARY_SHADOW:-false}
|
||||
SEMANTIC_SUMMARY_REPLACE: ${SEMANTIC_SUMMARY_REPLACE:-false}
|
||||
SEMANTIC_SUMMARY_REPLACE_SESSIONS: ${SEMANTIC_SUMMARY_REPLACE_SESSIONS:-}
|
||||
PI_CHANGE_REPORT_NARRATIVE: ${PI_CHANGE_REPORT_NARRATIVE:-false}
|
||||
FAV_PUBLISHER_ENABLED: ${FAV_PUBLISHER_ENABLED:-false}
|
||||
ports:
|
||||
|
||||
@@ -64,12 +64,19 @@ func TestComposeAppDeclaresMultiIntentPlanTierAndExpansionFlags(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A-4: SEMANTIC_SUMMARY_SHADOW / PI_CHANGE_REPORT_NARRATIVE must be declared in
|
||||
// all three environments (test/main were missing before Wave 2).
|
||||
// A-4: SEMANTIC_SUMMARY_SHADOW / REPLACE(+SESSIONS allowlist) /
|
||||
// PI_CHANGE_REPORT_NARRATIVE must be declared in all three environments so
|
||||
// operator-set env vars reach the container (REPLACE is fail-closed gray:
|
||||
// master + non-empty session allowlist; defaults stay off/empty).
|
||||
func TestComposeAppDeclaresSemanticSummaryAndNarrativeShadowFlags(t *testing.T) {
|
||||
for _, name := range composeFilesForFlagCheck {
|
||||
block := serviceBlockText(t, readComposeFile(t, name), "app")
|
||||
for _, key := range []string{"SEMANTIC_SUMMARY_SHADOW:", "PI_CHANGE_REPORT_NARRATIVE:"} {
|
||||
for _, key := range []string{
|
||||
"SEMANTIC_SUMMARY_SHADOW: ${SEMANTIC_SUMMARY_SHADOW:-false}",
|
||||
"SEMANTIC_SUMMARY_REPLACE: ${SEMANTIC_SUMMARY_REPLACE:-false}",
|
||||
"SEMANTIC_SUMMARY_REPLACE_SESSIONS: ${SEMANTIC_SUMMARY_REPLACE_SESSIONS:-}",
|
||||
"PI_CHANGE_REPORT_NARRATIVE: ${PI_CHANGE_REPORT_NARRATIVE:-false}",
|
||||
} {
|
||||
if !strings.Contains(block, key) {
|
||||
t.Errorf("%s: app service missing %s declaration", name, key)
|
||||
}
|
||||
|
||||
@@ -74,6 +74,28 @@ func TestSemanticReplace_AllowlistedSessionSwapsNarrative(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Wildcard allowlist (*) covers sessions never enumerated explicitly,
|
||||
// including sessions created after the flag was set.
|
||||
func TestSemanticReplace_WildcardAllowlistSwapsNarrative(t *testing.T) {
|
||||
t.Cleanup(ResetSummaryFlagsForTest)
|
||||
SetSummaryFlagsForTest(SummaryFeatureFlags{
|
||||
Shadow: true, Replace: true, ReplaceAllSessions: true,
|
||||
})
|
||||
|
||||
m := NewContextManager(nil, nil, DefaultTimeDecayConfig())
|
||||
m.SetSemanticSummarizer(&StubSemanticSummarizer{Narrative: "语义影子叙事:超声刀立项"})
|
||||
if _, err := m.GenerateRollingSummary(context.Background(), "sess_r_wild_new", nil, replaceBatch()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sum := drainShadow(t, m, "sess_r_wild_new")
|
||||
if sum.Source != "semantic_replace" || sum.Narrative != "语义影子叙事:超声刀立项" {
|
||||
t.Fatalf("wildcard session must swap narrative, got source=%s narrative=%.60s", sum.Source, sum.Narrative)
|
||||
}
|
||||
if sum.Facts["device_type"] != "超声手术设备" {
|
||||
t.Fatalf("deterministic facts lost: %+v", sum.Facts)
|
||||
}
|
||||
}
|
||||
|
||||
// Non-allowlisted session on the same config: deterministic retained.
|
||||
func TestSemanticReplace_NonAllowlistedKeepsDeterministic(t *testing.T) {
|
||||
t.Cleanup(ResetSummaryFlagsForTest)
|
||||
@@ -183,13 +205,24 @@ func TestSemanticReplace_StaleWatermarkKeepsDeterministic(t *testing.T) {
|
||||
// Flag parse: allowlist parsing and fail-closed accessor.
|
||||
func TestSemanticReplace_FlagParsing(t *testing.T) {
|
||||
t.Cleanup(ResetSummaryFlagsForTest)
|
||||
got := parseReplaceSessions(" a , b ,, ")
|
||||
got, all := parseReplaceSessions(" a , b ,, ")
|
||||
if all {
|
||||
t.Fatal("id list must not widen to all sessions")
|
||||
}
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("expected 2 sessions, got %d", len(got))
|
||||
}
|
||||
if _, ok := got["a"]; !ok {
|
||||
t.Fatal("session a missing")
|
||||
}
|
||||
if sessions, all := parseReplaceSessions(""); all || len(sessions) != 0 {
|
||||
t.Fatal("empty allowlist must stay empty and non-wildcard")
|
||||
}
|
||||
for _, raw := range []string{"*", " a , * "} {
|
||||
if sessions, all := parseReplaceSessions(raw); !all || len(sessions) != 0 {
|
||||
t.Fatalf("%q must parse to the all-sessions wildcard", raw)
|
||||
}
|
||||
}
|
||||
if SemanticReplaceEnabledFor("") {
|
||||
t.Fatal("empty session must never be eligible")
|
||||
}
|
||||
@@ -204,6 +237,30 @@ func TestSemanticReplace_FlagParsing(t *testing.T) {
|
||||
if SemanticReplaceEnabledFor("s1") {
|
||||
t.Fatal("master on + empty allowlist must replace nothing")
|
||||
}
|
||||
SetSummaryFlagsForTest(SummaryFeatureFlags{Replace: true, ReplaceAllSessions: true})
|
||||
if !SemanticReplaceEnabledFor("any-future-session") {
|
||||
t.Fatal("wildcard allowlist must cover every nonempty session id")
|
||||
}
|
||||
SetSummaryFlagsForTest(SummaryFeatureFlags{ReplaceAllSessions: true})
|
||||
if SemanticReplaceEnabledFor("s1") {
|
||||
t.Fatal("wildcard + master off must replace nothing")
|
||||
}
|
||||
}
|
||||
|
||||
// Wildcard allowlist (*) + master off: fail-closed keeps deterministic.
|
||||
func TestSemanticReplace_WildcardMasterOffKeepsDeterministic(t *testing.T) {
|
||||
t.Cleanup(ResetSummaryFlagsForTest)
|
||||
SetSummaryFlagsForTest(SummaryFeatureFlags{Shadow: true, ReplaceAllSessions: true})
|
||||
|
||||
m := NewContextManager(nil, nil, DefaultTimeDecayConfig())
|
||||
m.SetSemanticSummarizer(&StubSemanticSummarizer{Narrative: "语义影子叙事:超声刀立项"})
|
||||
if _, err := m.GenerateRollingSummary(context.Background(), "sess_r_wild_off", nil, replaceBatch()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sum := drainShadow(t, m, "sess_r_wild_off")
|
||||
if sum.Source != "deterministic" || strings.Contains(sum.Narrative, "语义影子叙事") {
|
||||
t.Fatalf("wildcard + master off must keep deterministic, got source=%s", sum.Source)
|
||||
}
|
||||
}
|
||||
|
||||
// Rollback residue: after a replace, turning flags off must regenerate a
|
||||
|
||||
@@ -17,7 +17,9 @@ const (
|
||||
EnvSemanticSummaryReplace = "SEMANTIC_SUMMARY_REPLACE"
|
||||
// EnvSemanticSummaryReplaceSessions is the fail-closed gray-release
|
||||
// allowlist: comma-separated chat session IDs eligible for narrative
|
||||
// replacement. Empty allowlist (default) replaces no session.
|
||||
// replacement. Empty allowlist (default) replaces no session. "*" widens
|
||||
// eligibility to every session (present and future), mirroring
|
||||
// INTENT_PLAN_PROJECT_ALLOWLIST=*; the master flag still gates globally.
|
||||
EnvSemanticSummaryReplaceSessions = "SEMANTIC_SUMMARY_REPLACE_SESSIONS"
|
||||
)
|
||||
|
||||
@@ -26,6 +28,10 @@ const (
|
||||
type SummaryFeatureFlags struct {
|
||||
Shadow bool
|
||||
Replace bool
|
||||
// ReplaceAllSessions is the "*" wildcard for ReplaceSessions: every
|
||||
// session (present and future) is eligible while the master Replace flag
|
||||
// stays on. An empty allowlist still replaces nothing.
|
||||
ReplaceAllSessions bool
|
||||
ReplaceSessions map[string]struct{}
|
||||
}
|
||||
|
||||
@@ -35,22 +41,29 @@ var (
|
||||
)
|
||||
|
||||
func loadSummaryFlagsFromEnv() SummaryFeatureFlags {
|
||||
sessions, all := parseReplaceSessions(os.Getenv(EnvSemanticSummaryReplaceSessions))
|
||||
return SummaryFeatureFlags{
|
||||
Shadow: envBoolDefault(EnvSemanticSummaryShadow, false),
|
||||
Replace: envBoolDefault(EnvSemanticSummaryReplace, false),
|
||||
ReplaceSessions: parseReplaceSessions(os.Getenv(EnvSemanticSummaryReplaceSessions)),
|
||||
ReplaceAllSessions: all,
|
||||
ReplaceSessions: sessions,
|
||||
}
|
||||
}
|
||||
|
||||
func parseReplaceSessions(raw string) map[string]struct{} {
|
||||
// parseReplaceSessions splits the comma-separated allowlist. Any "*" token
|
||||
// widens to all sessions; empty or ID-only values keep the explicit set.
|
||||
func parseReplaceSessions(raw string) (map[string]struct{}, bool) {
|
||||
out := make(map[string]struct{})
|
||||
for _, part := range strings.Split(raw, ",") {
|
||||
id := strings.TrimSpace(part)
|
||||
if id == "*" {
|
||||
return nil, true
|
||||
}
|
||||
if id != "" {
|
||||
out[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
return out
|
||||
return out, false
|
||||
}
|
||||
|
||||
func envBoolDefault(key string, def bool) bool {
|
||||
@@ -81,13 +94,16 @@ func SemanticSummaryReplaceEnabled() bool {
|
||||
}
|
||||
|
||||
// SemanticReplaceEnabledFor reports fail-closed gray eligibility: the master
|
||||
// flag must be on AND the session must be in the allowlist (empty allowlist
|
||||
// replaces no session).
|
||||
// flag must be on AND the session must be in the allowlist (or the allowlist
|
||||
// is the "*" wildcard). Empty allowlist replaces no session.
|
||||
func SemanticReplaceEnabledFor(sessionID string) bool {
|
||||
f := CurrentSummaryFlags()
|
||||
if !f.Replace || sessionID == "" {
|
||||
return false
|
||||
}
|
||||
if f.ReplaceAllSessions {
|
||||
return true
|
||||
}
|
||||
_, ok := f.ReplaceSessions[sessionID]
|
||||
return ok
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ var featureMaturityCatalog = map[string]featureMaturity{
|
||||
},
|
||||
"SEMANTIC_SUMMARY_REPLACE": {
|
||||
state: implShadow, adapter: adapterDegraded,
|
||||
reason: "fail-closed 灰度已接线(2026-09-26):验证 OK 后仅替换白名单会话 served Narrative;Facts/计数保持 deterministic;digest 仍 advisory;默认关",
|
||||
reason: "fail-closed 灰度已接线(2026-09-26):验证 OK 后仅替换白名单会话 served Narrative(支持 * 全部会话,含后续新建);Facts/计数保持 deterministic;digest 仍 advisory;默认关",
|
||||
},
|
||||
"PI_CHANGE_REPORT_NARRATIVE": {
|
||||
state: implShadow, adapter: adapterDegraded,
|
||||
@@ -122,7 +122,7 @@ var featureCatalog = []featureSpec{
|
||||
{key: "WORKFLOW_SCOPE_MODE", label: "工作流作用域裁决", group: "intent", fallback: "strict", description: "文档命中裁决:strict 命中他流即拦截;weighted 按工作流关系权重打分"},
|
||||
{key: "SEMANTIC_SUMMARY_SHADOW", label: "语义摘要影子", group: "ctxmgr", fallback: "false", description: "上下文语义摘要影子生成"},
|
||||
{key: "SEMANTIC_SUMMARY_REPLACE", label: "语义摘要替换", group: "ctxmgr", fallback: "false", description: "验证 OK 后以语义叙事替换会话 served Narrative;fail-closed 灰度,需同时配置会话白名单"},
|
||||
{key: "SEMANTIC_SUMMARY_REPLACE_SESSIONS", label: "语义摘要替换白名单", group: "ctxmgr", fallback: "", description: "逗号分隔会话 ID;空则不替换任何会话(纵深防御)"},
|
||||
{key: "SEMANTIC_SUMMARY_REPLACE_SESSIONS", label: "语义摘要替换白名单", group: "ctxmgr", fallback: "", description: "逗号分隔会话 ID;* 全部会话(含后续新建);空则不替换任何会话(纵深防御)"},
|
||||
{key: "PI_CHANGE_REPORT_NARRATIVE", label: "PI 变更报告叙事", group: "docrevision", fallback: "false", description: "变更报告 PI 叙事增强;默认仅确定性回退"},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user