From badfc19812342d958e7ac1a97c831184473eada0 Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 20 Sep 2026 14:03:30 +0000 Subject: [PATCH 1/3] docs(ops): claim rdev workflow seed hotfix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dev 库 rdev 停在 retired,test 库因 publishedBy 外键失败未发布研发工作流。 Co-authored-by: Cursor --- .doc/ops/CURRENT.md | 5 +- .../short-term/2026-09-20-rdev-seed-hotfix.md | 52 +++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 .doc/ops/short-term/2026-09-20-rdev-seed-hotfix.md diff --git a/.doc/ops/CURRENT.md b/.doc/ops/CURRENT.md index 0c2d8e11..0168c155 100644 --- a/.doc/ops/CURRENT.md +++ b/.doc/ops/CURRENT.md @@ -5,14 +5,15 @@ ## 焦点 -1. **2026-09-20 模块解耦规范与审计 done(仅文档)**:耦合审计、开发规范、批次 A–G 计划已写;不改业务代码,不改 `production_ready`。见 [审计](../audit/2026-09-20-module-coupling-audit.md) · [规范](../system-design/2026-09-20-module-development-standards.md) · [批次](../remediation/2026-09-20-module-decoupling-batch-plan.md)。 -2. **2026-09-20 P0 系统体检热修 done**:迁移 00058、三项 IDOR、panic/race/XSS、凭证轮换;见 [报告](../test-reports/2026-09-20-p0-hotfixes-test-report.md) · [审计](../audit/2026-09-20-system-triage-audit.md)。 +1. **2026-09-20 rdev 种子热修 in_progress**:dev 库 rdev 停在 `retired`;test 库因 `publishedBy=1` 外键失败未发布 rdev。见 [short-term](./short-term/2026-09-20-rdev-seed-hotfix.md)。 +2. **2026-09-20 模块解耦规范与审计 done(仅文档)**:耦合审计、开发规范、批次 A–G 计划已写;不改业务代码,不改 `production_ready`。见 [审计](../audit/2026-09-20-module-coupling-audit.md) · [规范](../system-design/2026-09-20-module-development-standards.md) · [批次](../remediation/2026-09-20-module-decoupling-batch-plan.md)。 3. **红线**:禁止整支 merge FAV;不默认升 `MULTI_INTENT_PLAN_TIER=full`;`SEMANTIC_SUMMARY_REPLACE` 不随 W2 开启;FAV live 观察窗证据驱动;`FAV_DEGRADED_READ=false`;项目 143 只读;SWBOOT-10 保持 fail-closed stub;D7 待合规签字;**P0 期间不改 `production_ready`**;模块解耦批次 A–G 未单独认领前保持 `planned`。 ## 并行认领 | owner | task | branch | worktree | mutex | status | |-------|------|--------|----------|-------|--------| +| Cursor | 2026-09-20-rdev-seed-hotfix | fix/workflow-seed-bootstrap-2026-09-20 | - | workflow-def | in_progress | | Cursor-Auto | 2026-09-20-module-decoupling-spec-audit | docs/module-decoupling-spec-2026-09-20 | - | module-decoupling-spec | done | | Cursor-Auto | 2026-09-20-p0-closeout | docs/p0-closeout-2026-09-20 | - | docs | done | | Cursor-Auto | 2026-09-20-p0-hotfixes(Wave 0 文档) | docs/p0-triage-2026-09-20 | - | docs | done | diff --git a/.doc/ops/short-term/2026-09-20-rdev-seed-hotfix.md b/.doc/ops/short-term/2026-09-20-rdev-seed-hotfix.md new file mode 100644 index 00000000..0726789a --- /dev/null +++ b/.doc/ops/short-term/2026-09-20-rdev-seed-hotfix.md @@ -0,0 +1,52 @@ +--- +title: "修复工作流种子:publishedBy 外键与 retired 不自愈" +status: in_progress +created: 2026-09-20 +updated: 2026-09-20 +owner: Cursor +branch: fix/workflow-seed-bootstrap-2026-09-20 +worktree: "-" +mutex: workflow-def +--- + +# 修复工作流种子:publishedBy 外键与 retired 不自愈 + +## 目标 + +dev 与 test 启动后,正式工作流目录包含「研发设计控制流程」(`rdev`),且数据库 `status=active`、已发布版本外键有效。 + +## 范围 + +- `BootstrapSeeds`:`publishedBy` 用户不存在时回落到库内最小 id;无用户则跳过并记日志 +- `UpsertPublishedSeed` / `RetireNonOfficialDefinitions`:白名单 key 幂等更新时把 `status` 恢复为 `active` +- `bizdev` 启动 seed 失败改为 `slog.Warn`,不再静默丢弃 + +## 非目标 + +- 不改 rdev manifest / 60 份模板内容 +- 不改 `production_ready` +- 不启用 FAV / 语义摘要 REPLACE + +## 步骤 + +1. [x] 从 dev 切 `fix/workflow-seed-bootstrap-2026-09-20` 并登记认领 +2. [ ] 修复 seed actor 回落与 retired 自愈 +3. [ ] `go vet` / `go test -short` / PG 集成 +4. [ ] 合入 dev 后 rebuild;晋升 test(`rc-v1.0.0-beta.7`)并 rebuild + +## 验收标准 + +- [ ] dev/test catalog 均含 rdev,DB `status=active` +- [ ] test 库 `workflow_versions` 无 `created_by` 外键错误 +- [ ] `go vet` / `go test -short` 通过 + +## 相关链接 + +- Checklist: - +- Handoff: - +- Test report: `.doc/test-reports/2026-09-20-rdev-seed-hotfix-test-report.md` + +## 证据 / 备注 + +- test PG `2026-09-20 09:49:51`:`workflow_versions_created_by_fkey`(`publishedBy=1`,test 用户最小 id=4) +- dev 库 rdev v3 `status=retired`,`project_selectable=true`(元数据更新不恢复 status) From 079225495e881f1ba3924b8af70bb55e666857af Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 20 Sep 2026 14:03:30 +0000 Subject: [PATCH 2/3] fix(workflowdef): heal retired seeds and fall back published_by MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 启动种子不再依赖固定用户 id=1,并把白名单工作流的 retired 状态恢复为 active。 Co-authored-by: Cursor --- internal/bizdev/service.go | 5 +- internal/workflowdef/bootstrap.go | 41 ++++++++++ internal/workflowdef/bootstrap_seed_test.go | 89 +++++++++++++++++++++ internal/workflowdef/catalog_store.go | 1 + internal/workflowdef/store.go | 4 +- 5 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 internal/workflowdef/bootstrap_seed_test.go diff --git a/internal/bizdev/service.go b/internal/bizdev/service.go index 63af33fd..aa7f8762 100644 --- a/internal/bizdev/service.go +++ b/internal/bizdev/service.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "log" + "log/slog" "os" "path/filepath" "strings" @@ -125,7 +126,9 @@ func NewService(cfg *config.Config) (*Service, error) { fallbackCatalog := projectworkflow.NewDefaultCatalog() wfStore := workflowdef.NewStore(pool) if pool != nil && workflowdef.TablesReady(context.Background(), pool) { - _ = workflowdef.BootstrapSeeds(context.Background(), pool, casStore, "templates/docs", 1) + if seedErr := workflowdef.BootstrapSeeds(context.Background(), pool, casStore, "templates/docs", 1); seedErr != nil { + slog.Warn("workflow seed bootstrap failed", "error", seedErr) + } } catalog := workflowdef.NewCatalog(context.Background(), wfStore, fallbackCatalog) pwSvc := projectworkflow.NewService(pwStore, catalog) diff --git a/internal/workflowdef/bootstrap.go b/internal/workflowdef/bootstrap.go index 1d0ab329..54b4f4e7 100644 --- a/internal/workflowdef/bootstrap.go +++ b/internal/workflowdef/bootstrap.go @@ -2,10 +2,13 @@ package workflowdef import ( "context" + "errors" + "log/slog" "os" "path/filepath" "strings" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "chat-agent-system/internal/storage" @@ -48,11 +51,49 @@ var officialSeedMeta = map[string]struct { }, } +// resolveSeedActor 返回可作为 workflow_versions.created_by 外键的用户 id。 +// 请求的 publishedBy 不存在时回落到库内最小 id;没有任何用户时返回 0。 +func resolveSeedActor(ctx context.Context, pool *pgxpool.Pool, publishedBy int64) (int64, error) { + if pool == nil { + return 0, nil + } + if publishedBy > 0 { + var exists bool + if err := pool.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM users WHERE id = $1)`, publishedBy).Scan(&exists); err != nil { + return 0, err + } + if exists { + return publishedBy, nil + } + } + var id int64 + err := pool.QueryRow(ctx, `SELECT id FROM users ORDER BY id ASC LIMIT 1`).Scan(&id) + if errors.Is(err, pgx.ErrNoRows) { + return 0, nil + } + if err != nil { + return 0, err + } + return id, nil +} + // BootstrapSeeds 幂等导入模板与正式 v2 工作流;非正式定义标记为不可选。 func BootstrapSeeds(ctx context.Context, pool *pgxpool.Pool, cas *storage.CAS, templatesDir string, publishedBy int64) error { if pool == nil || !TablesReady(ctx, pool) { return nil } + actor, err := resolveSeedActor(ctx, pool, publishedBy) + if err != nil { + return err + } + if actor == 0 { + slog.Warn("workflow seed skipped: no users available for published_by") + return nil + } + if actor != publishedBy { + slog.Warn("workflow seed published_by fallback", "requested", publishedBy, "actor", actor) + } + publishedBy = actor store := NewStore(pool) svc := NewService(pool, cas) diff --git a/internal/workflowdef/bootstrap_seed_test.go b/internal/workflowdef/bootstrap_seed_test.go new file mode 100644 index 00000000..5db58493 --- /dev/null +++ b/internal/workflowdef/bootstrap_seed_test.go @@ -0,0 +1,89 @@ +package workflowdef + +import ( + "context" + "testing" + "time" + + "github.com/jackc/pgx/v5/pgxpool" + + "chat-agent-system/internal/db" +) + +func openSeedPool(t *testing.T) *pgxpool.Pool { + t.Helper() + dsn := db.SkipIfNoTestDatabaseURL(t) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + pool, err := pgxpool.New(ctx, dsn) + if err != nil { + t.Skip(err) + } + if err := pool.Ping(ctx); err != nil { + pool.Close() + t.Skip(err) + } + if !TablesReady(ctx, pool) { + pool.Close() + t.Skip("workflowdef tables not migrated") + } + return pool +} + +func TestResolveSeedActor_FallsBackWhenUserMissing(t *testing.T) { + pool := openSeedPool(t) + defer pool.Close() + ctx := context.Background() + + var minID int64 + if err := pool.QueryRow(ctx, `SELECT id FROM users ORDER BY id ASC LIMIT 1`).Scan(&minID); err != nil { + t.Skip("no users to fall back to") + } + + got, err := resolveSeedActor(ctx, pool, minID) + if err != nil { + t.Fatal(err) + } + if got != minID { + t.Fatalf("existing user: got %d want %d", got, minID) + } + + const missing int64 = 9_000_000_001 + got, err = resolveSeedActor(ctx, pool, missing) + if err != nil { + t.Fatal(err) + } + if got != minID { + t.Fatalf("missing user fallback: got %d want %d", got, minID) + } +} + +func TestBootstrapSeeds_RestoresRetiredOfficialStatus(t *testing.T) { + pool := openSeedPool(t) + defer pool.Close() + ctx := context.Background() + + var exists bool + if err := pool.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM workflow_definitions WHERE def_key = 'rdev')`).Scan(&exists); err != nil { + t.Fatal(err) + } + if !exists { + t.Skip("rdev definition not present") + } + + if _, err := pool.Exec(ctx, `UPDATE workflow_definitions SET status = 'retired' WHERE def_key = 'rdev'`); err != nil { + t.Fatal(err) + } + + if err := BootstrapSeeds(ctx, pool, nil, t.TempDir(), 9_000_000_001); err != nil { + t.Fatal(err) + } + + var status string + if err := pool.QueryRow(ctx, `SELECT status FROM workflow_definitions WHERE def_key = 'rdev'`).Scan(&status); err != nil { + t.Fatal(err) + } + if status != "active" { + t.Fatalf("rdev status = %q, want active", status) + } +} diff --git a/internal/workflowdef/catalog_store.go b/internal/workflowdef/catalog_store.go index 35054da7..c5fbf2a8 100644 --- a/internal/workflowdef/catalog_store.go +++ b/internal/workflowdef/catalog_store.go @@ -55,6 +55,7 @@ func (s *Store) RetireNonOfficialDefinitions(ctx context.Context) error { _, err = s.pool.Exec(ctx, ` UPDATE workflow_definitions SET project_selectable = true, + status = 'active', catalog_order = CASE def_key WHEN 'main' THEN 10 WHEN 'product_manager' THEN 20 diff --git a/internal/workflowdef/store.go b/internal/workflowdef/store.go index 7743defd..a8d28cd8 100644 --- a/internal/workflowdef/store.go +++ b/internal/workflowdef/store.go @@ -401,7 +401,7 @@ func (s *Store) UpsertPublishedSeed(ctx context.Context, in CreateDefinitionInpu if latestHash == defHash { _, _ = tx.Exec(ctx, ` UPDATE workflow_definitions SET name = $2, description = $3, project_selectable = true, - catalog_order = $4, updated_at = now() + catalog_order = $4, status = 'active', updated_at = now() WHERE id = $1 `, defID, in.Name, in.Description, OfficialCatalogOrder(in.Key)) return tx.Commit(ctx) @@ -410,7 +410,7 @@ func (s *Store) UpsertPublishedSeed(ctx context.Context, in CreateDefinitionInpu } _, err = tx.Exec(ctx, ` UPDATE workflow_definitions SET name = $2, description = $3, product_domain = $4, - project_selectable = true, catalog_order = $5, updated_at = now() + project_selectable = true, catalog_order = $5, status = 'active', updated_at = now() WHERE id = $1 `, defID, in.Name, in.Description, in.ProductDomain, OfficialCatalogOrder(in.Key)) if err != nil { From 7da2c1fdefede967cb735665d1a7e35bb136b2b0 Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 20 Sep 2026 14:03:42 +0000 Subject: [PATCH 3/3] docs(ops): occupy integration slot for rdev seed hotfix Co-authored-by: Cursor --- .doc/ops/CURRENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.doc/ops/CURRENT.md b/.doc/ops/CURRENT.md index 0168c155..85fc02c9 100644 --- a/.doc/ops/CURRENT.md +++ b/.doc/ops/CURRENT.md @@ -167,7 +167,7 @@ | holder | phase | since | scope | queue | |--------|-------|-------|-------|-------| -| - | idle | 2026-09-20 | 模块解耦文档已合入 `133ceb45`;`go vet` 通过。`go test -short` 仅两个 docker 拉镜像用例因镜像源 EOF 失败,与文档 diff 无关 | - | +| Cursor | merging | 2026-09-20 | fix/workflow-seed-bootstrap-2026-09-20:rdev 种子 publishedBy 回落与 retired 自愈 | - | **phase**: `idle` | `merging` | `smoke` | `e2e`