merge: fix/workflow-seed-bootstrap-2026-09-20

This commit is contained in:
weichen
2026-09-20 14:03:42 +00:00
7 changed files with 193 additions and 6 deletions
+4 -3
View File
@@ -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 |
@@ -166,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`
@@ -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)
+4 -1
View File
@@ -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)
+41
View File
@@ -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)
@@ -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)
}
}
+1
View File
@@ -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
+2 -2
View File
@@ -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 {