package intentflow import ( "fmt" "os" "strings" "sync" "chat-agent-system/internal/intent" ) // PlanTier is the intentflow rollout switch. It must not be stored on // intent.ActionSpec or intent.FeatureFlags. type PlanTier string const ( TierLegacy PlanTier = "legacy" TierShadow PlanTier = "shadow" TierAutoRead PlanTier = "auto_read" TierConfirm PlanTier = "confirm" TierFull PlanTier = "full" ) type FeatureFlags struct { PlanTier PlanTier // ProjectAllowlist gates elevated tiers (shadow/auto_read/confirm/full). // Empty denies; AllowAllProjects (-1) allows every project. // Env: INTENT_PLAN_PROJECT_ALLOWLIST (*|id,id,...) ProjectAllowlist []int64 // Independent PU8 expansion gates. Default OFF; do not couple to PlanTier. U9Segment bool // MULTI_INTENT_U9_SEGMENT — LLM segment proposal U10Mutate bool // MULTI_INTENT_U10_MUTATE — low/medium mutate adapters U11DocGov bool // MULTI_INTENT_U11_DOCGOV — docgov action adapters U12RunControl bool // MULTI_INTENT_U12_RUN_CONTROL — workflow/run-control adapters // 逐键 live 放量 flag(P8):域名总 flag 开启后,细粒度键仍须单独打开才走真实引擎; // 未打开的键保持 validated_only dry-run。默认全 OFF,禁止 full 一次放量。 U11LiveReview bool // MULTI_INTENT_U11_LIVE_REVIEW — document.review.approve/reject U11LiveFinal bool // MULTI_INTENT_U11_LIVE_FINAL — document.final.approve/reject U11LiveProposal bool // MULTI_INTENT_U11_LIVE_PROPOSAL — document.proposal.decide U12LiveSwitch bool // MULTI_INTENT_U12_LIVE_SWITCH — workflow.switch U12LiveRestart bool // MULTI_INTENT_U12_LIVE_RESTART — run.restart } var ( flagsMu sync.RWMutex activeFlags = defaultFeatureFlags() ) func defaultFeatureFlags() FeatureFlags { return FeatureFlags{ PlanTier: parsePlanTier(os.Getenv("MULTI_INTENT_PLAN_TIER")), ProjectAllowlist: ParseProjectAllowlist(os.Getenv("INTENT_PLAN_PROJECT_ALLOWLIST")), U9Segment: envBool("MULTI_INTENT_U9_SEGMENT", false), U10Mutate: envBool("MULTI_INTENT_U10_MUTATE", false), U11DocGov: envBool("MULTI_INTENT_U11_DOCGOV", false), U12RunControl: envBool("MULTI_INTENT_U12_RUN_CONTROL", false), U11LiveReview: envBool("MULTI_INTENT_U11_LIVE_REVIEW", false), U11LiveFinal: envBool("MULTI_INTENT_U11_LIVE_FINAL", false), U11LiveProposal: envBool("MULTI_INTENT_U11_LIVE_PROPOSAL", false), U12LiveSwitch: envBool("MULTI_INTENT_U12_LIVE_SWITCH", false), U12LiveRestart: envBool("MULTI_INTENT_U12_LIVE_RESTART", false), } } func envBool(key string, def bool) bool { v := strings.TrimSpace(os.Getenv(key)) if v == "" { return def } return v == "1" || strings.EqualFold(v, "true") || strings.EqualFold(v, "yes") } func parsePlanTier(raw string) PlanTier { switch strings.ToLower(strings.TrimSpace(raw)) { case "", "legacy": return TierLegacy case "shadow": return TierShadow case "auto_read", "autoread": return TierAutoRead case "confirm": return TierConfirm case "full": return TierFull default: return TierLegacy } } func CurrentFeatureFlags() FeatureFlags { flagsMu.RLock() defer flagsMu.RUnlock() return activeFlags } func SetFeatureFlagsForTest(f FeatureFlags) { flagsMu.Lock() activeFlags = f flagsMu.Unlock() } func ResetFeatureFlagsForTest() { flagsMu.Lock() activeFlags = defaultFeatureFlags() flagsMu.Unlock() } // Allows reports whether the current (or supplied) tier may activate a policy row. func (t PlanTier) Allows(min PlanTier) bool { return tierRank(t) >= tierRank(min) } func tierRank(t PlanTier) int { switch t { case TierLegacy: return 0 case TierShadow: return 1 case TierAutoRead: return 2 case TierConfirm: return 3 case TierFull: return 4 default: return 0 } } // ValidateFlagConsistency 校验 intentflow 放量档位与 intent 多意图开关的 // 一致性(FIND-QUAL-01 回归):PlanTier 升档必须建立在 MULTI_INTENT_ENABLED // 之上,否则组合根启动期 fail-fast,不允许「intentflow 已升档、intent 仍关」 // 这类静默漂移配置上线。 func ValidateFlagConsistency(iflags intent.FeatureFlags, pflags FeatureFlags) error { if pflags.PlanTier != TierLegacy && !iflags.MultiIntentEnabled { return fmt.Errorf( "intentflow: MULTI_INTENT_PLAN_TIER=%q requires MULTI_INTENT_ENABLED=true; fix the flag combination instead of silently degrading", pflags.PlanTier, ) } return nil }