From 1cd1cddf641ea19911d4bbaad262cc8af253336b Mon Sep 17 00:00:00 2001 From: weichen Date: Wed, 16 Sep 2026 15:40:26 +0000 Subject: [PATCH] =?UTF-8?q?feat(intent-plan):=20independent=20PU8=20U9?= =?UTF-8?q?=E2=80=93U12=20flags=20default=20off?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dry-run/fail-closed adapters; does not raise plan tier. Co-authored-by: Cursor --- ...2026-09-16-pu8-u9-u12-flags-test-report.md | 22 +++++ internal/app/intentflow/expansion.go | 85 +++++++++++++++++++ internal/app/intentflow/expansion_test.go | 63 ++++++++++++++ internal/app/intentflow/flags.go | 21 ++++- internal/app/intentflow/flags_test.go | 12 +++ 5 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 .doc/test-reports/2026-09-16-pu8-u9-u12-flags-test-report.md create mode 100644 internal/app/intentflow/expansion.go create mode 100644 internal/app/intentflow/expansion_test.go diff --git a/.doc/test-reports/2026-09-16-pu8-u9-u12-flags-test-report.md b/.doc/test-reports/2026-09-16-pu8-u9-u12-flags-test-report.md new file mode 100644 index 00000000..b823870f --- /dev/null +++ b/.doc/test-reports/2026-09-16-pu8-u9-u12-flags-test-report.md @@ -0,0 +1,22 @@ +# PU8 U9–U12 独立 flag 测试报告 + +## 一、 测试背景 (Background) +覆盖 no-go 全量 backlog 实施。 + +## 二、 测试目标 (Test Objectives) +见计划对应章节。 + +## 三、 测试方案与环境 (Test Plan & Environment) +Go unit / flag default off。 + +## 四、 预期结果 (Expected Results) +默认 off;开启路径 fail-closed 或 shadow。 + +## 五、 实际测试结果与判定 (Actual Results & Evaluation) +- Verdict: PASS(单元) + +## 六、 自动化测试套件汇总 (Test Suite Execution) +见各 worktree `go test` 输出。 + +## 七、 测试结论与后续优化建议 (Conclusion & Next Steps) +默认 flag off;不升 MULTI_INTENT_PLAN_TIER。 diff --git a/internal/app/intentflow/expansion.go b/internal/app/intentflow/expansion.go new file mode 100644 index 00000000..8a7cd9ee --- /dev/null +++ b/internal/app/intentflow/expansion.go @@ -0,0 +1,85 @@ +package intentflow + +import ( + "context" + "fmt" + "time" +) + +// Expansion adapters for PU8. All gated by independent flags (default OFF). + +// SegmentProposal is a fail-closed LLM segment proposal when U9 is on. +type SegmentProposal struct { + SchemaVersion string `json:"schema_version"` + Spans []string `json:"spans"` + Digest string `json:"digest"` +} + +// ProposeSegments returns ErrExpansionDisabled when flag off; when on, rejects +// empty/malformed input fail-closed (no silent legacy bypass). +func ProposeSegments(ctx context.Context, text string) (*SegmentProposal, error) { + _ = ctx + if !CurrentFeatureFlags().U9Segment { + return nil, ErrExpansionDisabled + } + if len(text) == 0 { + return nil, fmt.Errorf("u9 segment: empty input (fail closed)") + } + select { + case <-ctx.Done(): + return nil, fmt.Errorf("u9 segment: timeout/cancel (fail closed): %w", ctx.Err()) + default: + } + // Deterministic stub proposal (no LLM) for gate wiring; real model behind same schema. + return &SegmentProposal{ + SchemaVersion: "u9-segment-v1", + Spans: []string{text}, + Digest: fmt.Sprintf("sha-stub-%d", len(text)), + }, nil +} + +// MutateDryRun validates mutate adapters when U10 on; never applies side effects here. +func MutateDryRun(ctx context.Context, action string, expectedRev int64) error { + _ = ctx + if !CurrentFeatureFlags().U10Mutate { + return ErrExpansionDisabled + } + if action == "" || expectedRev <= 0 { + return fmt.Errorf("u10 mutate: missing action or expected revision (fail closed)") + } + return nil +} + +// DocGovDryRun validates docgov action when U11 on. +func DocGovDryRun(ctx context.Context, action string, expectedDocVersion int64) error { + _ = ctx + if !CurrentFeatureFlags().U11DocGov { + return ErrExpansionDisabled + } + if action == "" || expectedDocVersion <= 0 { + return fmt.Errorf("u11 docgov: missing action or document version (fail closed)") + } + return nil +} + +// RunControlDryRun validates workflow/run-control when U12 on. +func RunControlDryRun(ctx context.Context, kind string) error { + _ = ctx + if !CurrentFeatureFlags().U12RunControl { + return ErrExpansionDisabled + } + switch kind { + case "workflow_switch", "run_resume", "run_restart", "run_cancel": + return nil + default: + return fmt.Errorf("u12 run-control: unknown kind %q (fail closed)", kind) + } +} + +// ErrExpansionDisabled means the independent PU8 flag is off (legacy path). +var ErrExpansionDisabled = fmt.Errorf("intentflow expansion disabled") + +// WithTimeout is a small helper for U9 fail-closed timeouts in tests. +func WithTimeout(parent context.Context, d time.Duration) (context.Context, context.CancelFunc) { + return context.WithTimeout(parent, d) +} diff --git a/internal/app/intentflow/expansion_test.go b/internal/app/intentflow/expansion_test.go new file mode 100644 index 00000000..ddad29f7 --- /dev/null +++ b/internal/app/intentflow/expansion_test.go @@ -0,0 +1,63 @@ +package intentflow + +import ( + "context" + "errors" + "testing" +) + +func TestPU8FlagsDefaultOff(t *testing.T) { + t.Setenv("MULTI_INTENT_U9_SEGMENT", "") + t.Setenv("MULTI_INTENT_U10_MUTATE", "") + t.Setenv("MULTI_INTENT_U11_DOCGOV", "") + t.Setenv("MULTI_INTENT_U12_RUN_CONTROL", "") + t.Setenv("MULTI_INTENT_PLAN_TIER", "") + ResetFeatureFlagsForTest() + f := CurrentFeatureFlags() + if f.U9Segment || f.U10Mutate || f.U11DocGov || f.U12RunControl { + t.Fatalf("PU8 flags must default off: %+v", f) + } + if f.PlanTier != TierLegacy { + t.Fatalf("tier=%s want legacy", f.PlanTier) + } +} + +func TestPU8ExpansionDisabledWhenOff(t *testing.T) { + ResetFeatureFlagsForTest() + SetFeatureFlagsForTest(FeatureFlags{PlanTier: TierLegacy}) + if _, err := ProposeSegments(context.Background(), "x"); !errors.Is(err, ErrExpansionDisabled) { + t.Fatalf("u9: %v", err) + } + if err := MutateDryRun(context.Background(), "edit", 1); !errors.Is(err, ErrExpansionDisabled) { + t.Fatalf("u10: %v", err) + } + if err := DocGovDryRun(context.Background(), "claim", 1); !errors.Is(err, ErrExpansionDisabled) { + t.Fatalf("u11: %v", err) + } + if err := RunControlDryRun(context.Background(), "run_cancel"); !errors.Is(err, ErrExpansionDisabled) { + t.Fatalf("u12: %v", err) + } +} + +func TestPU8ExpansionEnablePaths(t *testing.T) { + SetFeatureFlagsForTest(FeatureFlags{ + PlanTier: TierLegacy, U9Segment: true, U10Mutate: true, U11DocGov: true, U12RunControl: true, + }) + defer ResetFeatureFlagsForTest() + p, err := ProposeSegments(context.Background(), "hello") + if err != nil || p == nil || p.SchemaVersion != "u9-segment-v1" { + t.Fatalf("u9 on: %v %+v", err, p) + } + if _, err := ProposeSegments(context.Background(), ""); err == nil { + t.Fatal("u9 empty must fail closed") + } + if err := MutateDryRun(context.Background(), "edit", 2); err != nil { + t.Fatal(err) + } + if err := DocGovDryRun(context.Background(), "submit", 3); err != nil { + t.Fatal(err) + } + if err := RunControlDryRun(context.Background(), "run_resume"); err != nil { + t.Fatal(err) + } +} diff --git a/internal/app/intentflow/flags.go b/internal/app/intentflow/flags.go index b50d494d..cf8064c4 100644 --- a/internal/app/intentflow/flags.go +++ b/internal/app/intentflow/flags.go @@ -20,6 +20,11 @@ const ( type FeatureFlags struct { PlanTier PlanTier + // 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 } var ( @@ -28,7 +33,21 @@ var ( ) func defaultFeatureFlags() FeatureFlags { - return FeatureFlags{PlanTier: parsePlanTier(os.Getenv("MULTI_INTENT_PLAN_TIER"))} + return FeatureFlags{ + PlanTier: parsePlanTier(os.Getenv("MULTI_INTENT_PLAN_TIER")), + 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), + } +} + +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 { diff --git a/internal/app/intentflow/flags_test.go b/internal/app/intentflow/flags_test.go index 53496635..054be613 100644 --- a/internal/app/intentflow/flags_test.go +++ b/internal/app/intentflow/flags_test.go @@ -27,6 +27,18 @@ func TestPlanTierIndependentOfLegacyMultiIntentFlag(t *testing.T) { } } +func TestPlanTierIndependentOfExpansionFlags(t *testing.T) { + t.Setenv("MULTI_INTENT_PLAN_TIER", "") + t.Setenv("MULTI_INTENT_U9_SEGMENT", "true") + t.Setenv("MULTI_INTENT_U10_MUTATE", "true") + t.Setenv("MULTI_INTENT_U11_DOCGOV", "true") + t.Setenv("MULTI_INTENT_U12_RUN_CONTROL", "true") + ResetFeatureFlagsForTest() + if CurrentFeatureFlags().PlanTier != TierLegacy { + t.Fatal("U9–U12 flags must not raise MULTI_INTENT_PLAN_TIER") + } +} + func TestPlanTierEnv(t *testing.T) { if parsePlanTier("shadow") != TierShadow { t.Fatal("shadow")