Files
OpenFlare/docs/superpowers/plans/2026-08-30-openflare-cordis-alignment.md
T
ryan dd333643f2 docs(cordis): 添加与 Wavelet 对齐的实施计划
按 W1–W9、装配根、删平台副本、stamp 升级与 git merge 拆成
带测试的任务,金标准 v3.5.4 只读。
2026-08-30 10:37:13 +08:00

46 KiB
Raw Blame History

OpenFlare Cordis 对齐 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 让 OpenFlare 控制面与 Wavelet Cordis 同构装配,删除全部平台副本,用 git merge 接入 Wavelet 上游,并保证金标准 v3.5.4 库可升级且测试全绿。

Architecture: 先在 Wavelet 补齐通用扩展点 W1–W9;再把 core/pkg/plugins 同步进 OpenFlare;将 cmd 改为 newWaveletApp + server.New() + WithMigrationBaseline;server 只留 of_* 业务;历史库经 Baseline stamp,不再执行 76 条混合链。对齐完成后再做一次 unrelated merge。

Tech Stack: Go 1.25、Gin、GORM、goose v3、Cobra、PostgreSQL/SQLite、可选 ClickHouse。

规格来源: docs/superpowers/specs/2026-08-30-openflare-cordis-alignment-design.md

Global Constraints

  • 禁止修改 /Users/ryan/Code/Go/OpenFlare(金标准,9f79fb99 / v3.5.4)。
  • 禁止修改 frontend/ 源码。
  • 禁止在 OpenFlare server 注册 /api/health、/api/v1/user/self、/api/cap/*。
  • 禁止把 OpenFlare / of_ / 边缘节点写进 Wavelet。
  • 禁止 rebase / filter-repo / force-push 改写 OpenFlare 历史。
  • 禁止把 pow 抽到 pkg/pow;禁止第二套关系库 migrator。
  • Wavelet 工作在 /Users/ryan/Code/Go/Wavelet;OpenFlare 工作在 /Users/ryan/Code/Go/OpenFlare-cordis。
  • 执行期隔离工作区按 using-git-worktrees 在动手时创建,本计划不预先建。

文件结构

仓库 路径 职责
Wavelet backend/core/extpoints/router.go、scoped_extpoints.go W1 HandleRaw / BasePath
Wavelet backend/core/contracts/captcha.go CaptchaService
Wavelet backend/core/contracts/config_public.go PublicConfigProvider
Wavelet backend/core/contracts/push.go PushRegistry + PushEventMeta
Wavelet backend/core/app.go、context.go WithMigrationBaseline
Wavelet backend/plugins/domain/cap/plugin.go Provide CaptchaService;挂 /api/cap/*
Wavelet backend/plugins/domain/user/plugin.go 消费 CaptchaService;GET /self
Wavelet backend/plugins/domain/system/plugin.go GET /api/health;PublicConfigProvider
Wavelet backend/plugins/domain/upload/plugin.go 补挂 /my 等用户路由
Wavelet backend/plugins/domain/message_gateway/ 实现 PushRegistry
Wavelet backend/plugins/drivers/driver_http/ RedirectTrailingSlash
Wavelet backend/cmd/app.go gooseEngine:Baseline + advisory lock
OpenFlare backend/cmd/{app,api,all,worker,scheduler,root}.go 与 Wavelet 同构 + server.New()
OpenFlare backend/OpenFlare/plugins/server/plugin.go 只注册 of_* 业务
OpenFlare backend/OpenFlare/plugins/server/stamp/stamp.go Baseline stamp 实现
OpenFlare backend/OpenFlare/plugins/server/migrations/{postgres,sqlite}/00001_initial.sql 仅 of_*
OpenFlare .gitattributes merge=ours 规则

Task 1: 金标准 L1(只读,失败即停)

Files: 无(只读 /Users/ryan/Code/Go/OpenFlare)

Interfaces:

  • Consumes: 金标准树 9f79fb99

  • Produces: 确认 go test ./... 在金标准上为绿,作为后续门禁前提

  • Step 1: 确认 HEAD

git -C /Users/ryan/Code/Go/OpenFlare rev-parse HEAD
git -C /Users/ryan/Code/Go/OpenFlare status --short

Expected: 9f79fb99...,工作区空。若不是,停下来问人,不要改金标准。

  • Step 2: 跑金标准测试
cd /Users/ryan/Code/Go/OpenFlare && go test ./...

Expected: exit 0。失败则本计划停止。

  • Step 3: 不提交

本任务无代码变更。


Task 2: Wavelet W1 HandleRaw

Files:

  • Modify: /Users/ryan/Code/Go/Wavelet/backend/core/extpoints/router.go(接口 + HandleRaw)
  • Modify: /Users/ryan/Code/Go/Wavelet/backend/core/scoped_extpoints.go
  • Create: /Users/ryan/Code/Go/Wavelet/backend/core/extpoints/router_raw_test.go
  • Test: 同上

Interfaces:

  • Consumes: 无
  • Produces: RouterExtension.HandleRaw(method, path string, handlers ...any) RouteDefinition;BasePath() string

参考实现已在 OpenFlare-cordis(不要发明第二套语义): /Users/ryan/Code/Go/OpenFlare-cordis/backend/core/extpoints/router.go 的 addRoute / HandleRaw / joinPathPreservingTrailing / ensureLeadingSlash,以及 scoped_extpoints.go 的 HandleRaw。

  • Step 1: 在 Wavelet 写入失败测试

把下面文件写到 /Users/ryan/Code/Go/Wavelet/backend/core/extpoints/router_raw_test.go(与 OpenFlare 仓同文):

package extpoints

import "testing"

func TestHandleRawPreservesTrailingSlash(t *testing.T) {
	r := &RouterRegistry{}
	g := r.Group("/api/v1/nodes")

	if got := g.BasePath(); got != "/api/v1/nodes" {
		t.Fatalf("BasePath() = %q, want %q", got, "/api/v1/nodes")
	}
	slashless := g.Handle("GET", "")
	slashed := g.HandleRaw("GET", "/")

	if slashless.Path != "/api/v1/nodes" {
		t.Errorf("Handle(\"\") path = %q, want %q", slashless.Path, "/api/v1/nodes")
	}
	if slashed.Path != "/api/v1/nodes/" {
		t.Errorf("HandleRaw(\"/\") path = %q, want %q", slashed.Path, "/api/v1/nodes/")
	}
	if slashed.ID == slashless.ID {
		t.Error("HandleRaw must allocate its own route ID")
	}
	if got := len(r.Routes()); got != 2 {
		t.Errorf("registry routes = %d, want 2", got)
	}
}
  • Step 2: 跑测试确认失败
cd /Users/ryan/Code/Go/Wavelet/backend && go test ./core/extpoints/ -run TestHandleRaw -count=1

Expected: FAIL(HandleRaw undefined 或 BasePath undefined)。

  • Step 3: 实现

在 RouterExtension 增加 HandleRaw 与 BasePath。Handle 继续 cleanPath。HandleRaw 走 ensureLeadingSlash / joinPathPreservingTrailing,不剥尾斜杠。RouterRegistry 抽出 addRoute。scopedRouterExtension.HandleRaw 与 Handle 一样登记 OnDispose → UnregisterByID。从 OpenFlare-cordis 对应文件拷语义,不要改行为。

  • Step 4: 测试通过
cd /Users/ryan/Code/Go/Wavelet/backend && go test ./core/extpoints/ ./core/ -count=1

Expected: PASS。

  • Step 5: 提交(Wavelet 仓)
cd /Users/ryan/Code/Go/Wavelet
git add backend/core/extpoints/router.go backend/core/extpoints/router_raw_test.go backend/core/scoped_extpoints.go
git commit -m "feat(core): add HandleRaw and BasePath for trailing-slash routes"

Task 3: Wavelet W2 CaptchaService + /api/cap/* + user 门禁

Files:

  • Create: /Users/ryan/Code/Go/Wavelet/backend/core/contracts/captcha.go
  • Modify: /Users/ryan/Code/Go/Wavelet/backend/plugins/domain/cap/plugin.go
  • Modify: /Users/ryan/Code/Go/Wavelet/backend/plugins/domain/user/plugin.go
  • Create: /Users/ryan/Code/Go/Wavelet/backend/plugins/domain/cap/plugin_captcha_contract_test.go
  • Create: /Users/ryan/Code/Go/Wavelet/backend/plugins/domain/user/plugin_captcha_test.go

Interfaces:

  • Consumes: 现有 cap.VerifyMiddleware、Challenge、Redeem
  • Produces:
package contracts

type CaptchaService interface {
    VerifyMiddleware(scope string) any
    ChallengeHandler() any
    RedeemHandler() any
}
  • Step 1: 写契约测试(cap 插件 Apply 后能 Inject CaptchaService)
package cap

import (
	"context"
	"testing"

	"Wavelet/core"
	"Wavelet/core/contracts"
)

func TestApplyProvidesCaptchaService(t *testing.T) {
	ctx := core.NewContext(context.Background())
	if err := New().Apply(ctx); err != nil {
		t.Fatal(err)
	}
	svc, err := core.Inject[contracts.CaptchaService](ctx)
	if err != nil || svc == nil {
		t.Fatalf("Inject CaptchaService: svc=%v err=%v", svc, err)
	}
	if svc.ChallengeHandler() == nil || svc.RedeemHandler() == nil {
		t.Fatal("handlers must be non-nil")
	}
	if svc.VerifyMiddleware("login") == nil {
		t.Fatal("VerifyMiddleware(login) must be non-nil")
	}
}

func TestApplyRegistersUnversionedCapRoutes(t *testing.T) {
	ctx := core.NewContext(context.Background())
	if err := New().Apply(ctx); err != nil {
		t.Fatal(err)
	}
	want := map[string]bool{
		"POST /api/v1/cap/challenge": false,
		"POST /api/cap/challenge":    false,
		"POST /api/cap/redeem":       false,
	}
	for _, rd := range ctx.Router().Routes() {
		key := rd.Method + " " + rd.Path
		if _, ok := want[key]; ok {
			want[key] = true
		}
	}
	for key, ok := range want {
		if !ok {
			t.Errorf("missing route %s", key)
		}
	}
}
  • Step 2: 跑测试确认失败
cd /Users/ryan/Code/Go/Wavelet/backend && go test ./plugins/domain/cap/ -run TestApplyProvidesCaptchaService -count=1

Expected: FAIL(CaptchaService 未定义或未 Provide)。

  • Step 3: 实现契约与 cap 插件

captcha.go 只含接口,无 OpenFlare 字样。

cap 内未导出适配:

type captchaService struct{}

func (captchaService) VerifyMiddleware(scope string) any {
	return VerifyMiddleware(GetDefaultManager(), scope)
}
func (captchaService) ChallengeHandler() any { return Challenge }
func (captchaService) RedeemHandler() any    { return Redeem }

Apply 中:core.Provide[contracts.CaptchaService](ctx, captchaService{})。在现有 /api/v1/cap 组之外再:

legacy := ctx.Router().Group("/api/cap")
legacy.POST("/challenge", Challenge)
legacy.POST("/redeem", Redeem)
ctx.Router().RegisterWhitelist("/api/cap/challenge", "/api/cap/redeem")

原 /api/v1/cap 组保留。

  • Step 4: user 插件消费 CaptchaService

在 user/plugin.go 注册 login/register/send-email-code 时:

passThrough := gin.HandlerFunc(func(c *gin.Context) { c.Next() })
loginCap, registerCap, emailCap := passThrough, passThrough, passThrough
if capSvc, err := core.Inject[contracts.CaptchaService](ctx); err == nil && capSvc != nil {
	if mw, ok := capSvc.VerifyMiddleware("login").(gin.HandlerFunc); ok {
		loginCap = mw
	}
	if mw, ok := capSvc.VerifyMiddleware("register").(gin.HandlerFunc); ok {
		registerCap = mw
	}
	if mw, ok := capSvc.VerifyMiddleware("send_email_code").(gin.HandlerFunc); ok {
		emailCap = mw
	}
}
userGroup.POST("/login", loginCap, Login)
userGroup.POST("/register", registerCap, Register)
userGroup.POST("/send-email-code", emailCap, SendEmailCode)

无 CaptchaService 时行为与现在相同(裸路由)。user 的 Inject() 不要把 CaptchaService 变成硬依赖。

user 测试:Apply 且未 Provide captcha 时三条路由仍在;Provide 假 CaptchaService 后 login 的 handler 链长度大于 1。

  • Step 5: 跑测试
cd /Users/ryan/Code/Go/Wavelet/backend && go test ./plugins/domain/cap/ ./plugins/domain/user/ ./core/contracts/ -count=1

Expected: PASS。

  • Step 6: 提交
cd /Users/ryan/Code/Go/Wavelet
git add backend/core/contracts/captcha.go backend/plugins/domain/cap backend/plugins/domain/user
git commit -m "feat(cap): expose CaptchaService and unversioned /api/cap routes"

Task 4: Wavelet W3 PublicConfigProvider

Files:

  • Create: /Users/ryan/Code/Go/Wavelet/backend/core/contracts/config_public.go
  • Modify: /Users/ryan/Code/Go/Wavelet/backend/plugins/domain/system/plugin.go
  • Create: /Users/ryan/Code/Go/Wavelet/backend/plugins/domain/system/public_config_test.go

Interfaces:

  • Produces:
package contracts

import "context"

type PublicConfigProvider interface {
    PublicConfig(ctx context.Context) (any, error)
}
  • Step 1: 失败测试
package system

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"Wavelet/core"
	"Wavelet/core/contracts"

	"github.com/gin-gonic/gin"
)

type stubPublic struct{ payload any }

func (s stubPublic) PublicConfig(context.Context) (any, error) { return s.payload, nil }

func TestPublicConfigUsesProviderWhenPresent(t *testing.T) {
	gin.SetMode(gin.TestMode)
	ctx := core.NewContext(context.Background())
	core.Provide[contracts.PublicConfigProvider](ctx, stubPublic{payload: map[string]string{"k": "v"}})
	if err := New().Apply(ctx); err != nil {
		t.Fatal(err)
	}
	var handler gin.HandlerFunc
	for _, rd := range ctx.Router().Routes() {
		if rd.Method == "GET" && rd.Path == "/api/v1/config/public" {
			handler = rd.Handlers[0].(gin.HandlerFunc)
		}
	}
	if handler == nil {
		t.Fatal("public config route missing")
	}
	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/config/public", nil)
	handler(c)
	var body struct {
		Data json.RawMessage `json:"data"`
	}
	if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
		t.Fatal(err)
	}
	if string(body.Data) != `{"k":"v"}` && string(body.Data) != `{"k": "v"}` {
		t.Fatalf("data = %s, want flat map", body.Data)
	}
}

再加一个无 provider 的测试:响应 JSON 含 "configs" 与 "app"(保持 Wavelet 默认)。

  • Step 2: 跑测试确认失败
cd /Users/ryan/Code/Go/Wavelet/backend && go test ./plugins/domain/system/ -run TestPublicConfig -count=1

Expected: FAIL。

  • Step 3: 实现

GET /api/v1/config/public handler 内:

if p, err := core.Inject[contracts.PublicConfigProvider](ctx); err == nil && p != nil {
    data, err := p.PublicConfig(c.Request.Context())
    if err != nil {
        logger.ErrorF(c.Request.Context(), "[System] public config provider failed: %v", err)
        response.AbortInternal(c, "public config unavailable")
        return
    }
    c.JSON(http.StatusOK, response.OK(data))
    return
}
// 原 {configs, app} 默认分支

Inject 在请求时解析,这样 server 后挂 Provide 仍生效(若 Apply 顺序导致 Provide 晚于 system.Apply,改用 core.When 存到局部变量,或在 handler 里每次 Inject)。必须在 handler 里 Inject,不要在 Apply 时缓存成 nil。

  • Step 4: 测试通过并提交
cd /Users/ryan/Code/Go/Wavelet/backend && go test ./plugins/domain/system/ -count=1
git -C /Users/ryan/Code/Go/Wavelet add backend/core/contracts/config_public.go backend/plugins/domain/system
git -C /Users/ryan/Code/Go/Wavelet commit -m "feat(system): allow PublicConfigProvider to replace public config payload"

Task 5: Wavelet W4 RedirectTrailingSlash

Files:

  • Modify: /Users/ryan/Code/Go/Wavelet/backend/plugins/drivers/driver_http/config.go
  • Modify: /Users/ryan/Code/Go/Wavelet/backend/plugins/drivers/driver_http/engine.go
  • Create: /Users/ryan/Code/Go/Wavelet/backend/plugins/drivers/driver_http/engine_slash_test.go

Interfaces:

  • Produces: httpAppConfig.RedirectTrailingSlash bool,tag config:"redirect_trailing_slash" env:"APP_REDIRECT_TRAILING_SLASH" default:"true"

  • Step 1: 失败测试

package driver_http

import "testing"

func TestBuildEngineDefaultRedirectsTrailingSlash(t *testing.T) {
	eng, err := BuildEngineWithConfig(httpAppConfig{}, httpRedisConfig{})
	if err != nil {
		t.Fatal(err)
	}
	if !eng.RedirectTrailingSlash {
		t.Fatal("default RedirectTrailingSlash must be true")
	}
}

func TestBuildEngineCanDisableRedirectTrailingSlash(t *testing.T) {
	eng, err := BuildEngineWithConfig(httpAppConfig{RedirectTrailingSlash: false}, httpRedisConfig{})
	if err != nil {
		t.Fatal(err)
	}
	if eng.RedirectTrailingSlash {
		t.Fatal("RedirectTrailingSlash must honor false")
	}
}

注意:httpAppConfig 里 bool 零值是 false,与 default true 冲突。测试里「默认」必须走 BuildEngineWithConfig 在 未显式设 false 且 Bind 之后 的行为。实现上用 *bool 或单独 redirectTrailingSlashSet。推荐:

RedirectTrailingSlash *bool `config:"redirect_trailing_slash" env:"APP_REDIRECT_TRAILING_SLASH"`

BuildEngineWithConfig:redirect := true;若 cfg.RedirectTrailingSlash != nil { redirect = *cfg.RedirectTrailingSlash }。测试「默认」传 httpAppConfig{}(nil → true);「关闭」传 ptr(false)。

  • Step 2: 跑测试确认失败
cd /Users/ryan/Code/Go/Wavelet/backend && go test ./plugins/drivers/driver_http/ -run TestBuildEngine -count=1

Expected: FAIL。

  • Step 3: 在 BuildEngineWithConfig 里 r.RedirectTrailingSlash = redirect,默认 true。

  • Step 4: 测试通过并提交

cd /Users/ryan/Code/Go/Wavelet/backend && go test ./plugins/drivers/driver_http/ -count=1
git -C /Users/ryan/Code/Go/Wavelet add backend/plugins/drivers/driver_http
git -C /Users/ryan/Code/Go/Wavelet commit -m "feat(http): make trailing-slash redirect configurable"

Task 6: Wavelet W5 upload 用户路由补挂

Files:

  • Modify: /Users/ryan/Code/Go/Wavelet/backend/plugins/domain/upload/plugin.go(约 123–129 行)
  • Modify: /Users/ryan/Code/Go/Wavelet/backend/plugins/domain/upload/plugin_test.go(若无路由测试则创建)

Interfaces:

  • Consumes: 已有 handler.ListMyFiles、handler.UpdateMyFile、handler.DownloadFile、handler.BatchDownloadFiles

  • Produces: 用户组额外路径 GET /api/v1/upload/my、PUT /api/v1/upload/:id、GET /api/v1/upload/download/:id、POST /api/v1/upload/download/batch

  • Step 1: 写失败测试(Apply 后检查 Routes 包含上述四条,且仍含 GET /api/v1/upload 与 POST /api/v1/upload/batch-download)

Apply 需要 AuthService / DB 等。若现有 plugin_test.go 已有假依赖,跟它走。没有则用 core.Provide 三个 stub:contracts.DBService、contracts.StorageService、contracts.AuthService(RequireAuthMiddleware 返回 gin.HandlerFunc pass-through)。

  • Step 2: 跑测试确认缺路由
cd /Users/ryan/Code/Go/Wavelet/backend && go test ./plugins/domain/upload/ -run TestUserUploadRoutes -count=1

Expected: FAIL missing /my。

  • Step 3: 在 uploadGroup 增加
uploadGroup.GET("/my", handler.ListMyFiles)
uploadGroup.PUT("/:id", handler.UpdateMyFile)
uploadGroup.GET("/download/:id", handler.DownloadFile)
uploadGroup.POST("/download/batch", handler.BatchDownloadFiles)

原 GET ""、POST /batch-download 不动。

  • Step 4: 测试通过并提交
cd /Users/ryan/Code/Go/Wavelet/backend && go test ./plugins/domain/upload/ -count=1
git -C /Users/ryan/Code/Go/Wavelet add backend/plugins/domain/upload
git -C /Users/ryan/Code/Go/Wavelet commit -m "feat(upload): mount existing my/update/download routes on user API"

Task 7: Wavelet W6 PushRegistry

Files:

  • Create: /Users/ryan/Code/Go/Wavelet/backend/core/contracts/push.go
  • Modify: /Users/ryan/Code/Go/Wavelet/backend/plugins/domain/message_gateway/plugin.go
  • Modify: /Users/ryan/Code/Go/Wavelet/backend/plugins/domain/message_gateway/service/push.go(适配器,不改注册语义)
  • Create: /Users/ryan/Code/Go/Wavelet/backend/plugins/domain/message_gateway/plugin_registry_test.go

Interfaces:

  • Produces:
package contracts

import "context"

type PushNotificationTemplate struct {
    Title   string
    Content string
    Level   string
    Ext     map[string]any
}

type PushEventMeta struct {
    Key             string
    Name            string
    Description     string
    DefaultTemplate PushNotificationTemplate
}

type PushRegistry interface {
    RegisterBuiltInEvent(meta PushEventMeta)
    SyncEvents(ctx context.Context) error
}
  • Step 1: 失败测试 — Apply 后 Inject[PushRegistry] 成功;RegisterBuiltInEvent 一个 key 后再 GetBuiltInEvents(或 registry 再读)能看到该 key。

  • Step 2: 跑测试确认失败

cd /Users/ryan/Code/Go/Wavelet/backend && go test ./plugins/domain/message_gateway/ -run TestPushRegistry -count=1

Expected: FAIL。

  • Step 3: 实现 — message_gateway 内部把 PushEventMeta 转成现有 model.EventMetadata 再调 RegisterBuiltInEvent / SyncEvents。Apply 里 core.Provide[contracts.PushRegistry](ctx, ...)。禁止在契约里出现 openflare。

  • Step 4: 测试通过并提交

cd /Users/ryan/Code/Go/Wavelet/backend && go test ./plugins/domain/message_gateway/ -count=1
git -C /Users/ryan/Code/Go/Wavelet add backend/core/contracts/push.go backend/plugins/domain/message_gateway
git -C /Users/ryan/Code/Go/Wavelet commit -m "feat(message_gateway): expose PushRegistry contract"

Task 8: Wavelet W7 + W8 health 与 /user/self

Files:

  • Modify: /Users/ryan/Code/Go/Wavelet/backend/plugins/domain/system/plugin.go
  • Modify: /Users/ryan/Code/Go/Wavelet/backend/plugins/domain/user/plugin.go
  • Create: /Users/ryan/Code/Go/Wavelet/backend/plugins/domain/system/health_route_test.go
  • Create: /Users/ryan/Code/Go/Wavelet/backend/plugins/domain/user/self_route_test.go

Interfaces:

  • Produces: GET /api/health → response.OKNil();GET /api/v1/user/self 使用 loginMW + 返回当前用户(与 profile 同源,handler 调 AuthService.GetCurrentUser 或现有 UserInfo 等价实现)

  • Step 1: 失败测试

system:Apply 后 Routes 含 GET /api/health,用 httptest 调 handler,body 的 error_msg 为空且 data 为 null。GET /healthz 仍在。

user:Apply 后 Routes 含 GET /api/v1/user/self,且该路由的 middleware/handler 数量 ≥ login 保护的 /profile(必须带鉴权)。

  • Step 2: 跑测试确认失败
cd /Users/ryan/Code/Go/Wavelet/backend && go test ./plugins/domain/system/ ./plugins/domain/user/ -run 'TestHealth|TestSelf' -count=1

Expected: FAIL missing routes。

  • Step 3: 实现
ctx.Router().GET("/api/health", func(c *gin.Context) {
    c.JSON(http.StatusOK, response.OKNil())
})
ctx.Router().RegisterWhitelist("/api/health")

/healthz 保持 {status: ok},不要改。

user:

userGroup.GET("/self", loginMW, Self)

Self 用 contracts.AuthService.GetCurrentUser(已有 SetAuthService)写成与 UpdateProfile 同文件的小 handler,JSON response.OK(user)。

  • Step 4: 测试通过并提交
cd /Users/ryan/Code/Go/Wavelet/backend && go test ./plugins/domain/system/ ./plugins/domain/user/ -count=1
git -C /Users/ryan/Code/Go/Wavelet add backend/plugins/domain/system backend/plugins/domain/user
git -C /Users/ryan/Code/Go/Wavelet commit -m "feat(platform): add GET /api/health and GET /api/v1/user/self"

Task 9: Wavelet W9 MigrationBaseline + 锁

Files:

  • Modify: /Users/ryan/Code/Go/Wavelet/backend/core/app.go
  • Modify: /Users/ryan/Code/Go/Wavelet/backend/core/context.go
  • Modify: /Users/ryan/Code/Go/Wavelet/backend/core/app_test.go
  • Modify: /Users/ryan/Code/Go/Wavelet/backend/cmd/app.go(gooseEngine.Migrate)

Interfaces:

  • Produces:
func WithMigrationBaseline(fn func(*Context) error) AppOption
func (c *Context) MigrationBaseline() func(*Context) error

语义:Prepare 把 App 上的 fn 拷到 root Context;gooseEngine.Migrate 在第一次成功 CreateVersionTable 之后、任何 provider.Up 之前调用 ctx.MigrationBaseline()(nil 则跳过)。引擎与选项名禁止产品名。

  • Step 1: 失败测试(core)
func TestWithMigrationBaselineRunsBeforeEngineMigrate(t *testing.T) {
    var order []string
    engine := MigrationRunner(func(ctx *Context, _ []MigrationEntry) error {
        order = append(order, "engine")
        if ctx.MigrationBaseline() == nil {
            t.Fatal("baseline must be visible on context inside Migrate")
        }
        return ctx.MigrationBaseline()(ctx)
    })
    app := NewApp(
        WithMigrationEngine(engine),
        WithMigrationBaseline(func(*Context) error {
            order = append(order, "baseline")
            return nil
        }),
    )
    // 注册一个空迁移条目的测试插件,使 RunMigrations 不会因 entries==0 直接返回
    // ...
}

若现有测试插件难写:最小插件 Name() string + Apply 里 ctx.Migrations().Register("t", someFS)。也可用不走完整 Prepare、只测 WithMigrationBaseline 把 fn 放到 app.ctx:调用内部 Prepare 后 app.Context().MigrationBaseline() != nil。

另测:fn 返回 error 时 RunMigrations 返回该 error,且自定义 engine 的 Migrate 不被调用——注意 spec 是表创建之后、Up 之前。core 层只负责把 fn 放到 Context;调用点在 gooseEngine。core 测试断言 Prepare 后 Context 上能取到 fn。cmd 测试断言顺序 create-table → baseline → up。

  • Step 2: 跑测试确认失败
cd /Users/ryan/Code/Go/Wavelet/backend && go test ./core/ -run TestWithMigrationBaseline -count=1

Expected: FAIL。

  • Step 3: 实现

App 加字段 migrationBaseline func(*Context) error。WithMigrationBaseline 写入。Prepare 成功装好 ctx 后:a.ctx.setMigrationBaseline(a.migrationBaseline)(未导出 setter)。

gooseEngine.Migrate:解析 DB 后,用与 CreateVersionTable 相同的 DDL 先建一次 w_schema_versions(已有 IF NOT EXISTS),然后:

if fn := ctx.MigrationBaseline(); fn != nil {
    if err := fn(ctx); err != nil {
        return fmt.Errorf("migration baseline: %w", err)
    }
}

再进入现有 per-plugin Up 循环。

锁:Postgres 在 baseline 与 Up 外包 pg_advisory_lock(0x77617665)(任意稳定 int64,注释写明用途),defer pg_advisory_unlock。SQLite 不额外加锁(单写者)。

  • Step 4: 测试通过并提交
cd /Users/ryan/Code/Go/Wavelet/backend && go test ./core/ ./cmd/ -count=1
git -C /Users/ryan/Code/Go/Wavelet add backend/core backend/cmd/app.go
git -C /Users/ryan/Code/Go/Wavelet commit -m "feat(core): add WithMigrationBaseline hook before goose Up"
  • Step 5: Wavelet 全量测试
cd /Users/ryan/Code/Go/Wavelet/backend && go test ./...

Expected: PASS。失败先修,再进入 OpenFlare 任务。


Task 10: 同步框架进 OpenFlare-cordis

Files:

  • Modify: backend/{core,pkg,plugins}(rsync)
  • Modify: backend/OpenFlare/upstream-patches.md(清空条目)

Interfaces:

  • Consumes: Task 2–9 已进 Wavelet 工作树

  • Produces: OpenFlare backend/{core,pkg,plugins} 与 Wavelet 对应目录零差(排除 uploads/dist/data/*.db)

  • Step 1: 同步

cd /Users/ryan/Code/Go/OpenFlare-cordis
scripts/sync-upstream.sh /Users/ryan/Code/Go/Wavelet/backend
scripts/sync-upstream.sh --check /Users/ryan/Code/Go/Wavelet/backend

Expected: --check 无 >f/<f 内容差异。

  • Step 2: 清空补丁清单

backend/OpenFlare/upstream-patches.md 改为只保留标题与一句「当前无本地内核补丁」。删除过时的 HandleRaw 说明。

  • Step 3: 编译
cd /Users/ryan/Code/Go/OpenFlare-cordis/backend && go test ./core/... ./pkg/... ./plugins/...

Expected: PASS。OpenFlare 业务测试此步允许仍绿或因接口变化失败;失败留到 Task 12 修,但 上游包必须绿。

  • Step 4: 提交
cd /Users/ryan/Code/Go/OpenFlare-cordis
git add backend/core backend/pkg backend/plugins backend/OpenFlare/upstream-patches.md
git commit -m "chore(cordis): sync Wavelet core/pkg/plugins after W1-W9"

Task 11: 重导金标准夹具

Files:

  • Modify: docs/superpowers/specs/baseline/{schema-A.sql,versions-A.txt,routes.txt,routes-engine.txt}(仅当与金标准现跑不一致)

Interfaces:

  • Consumes: /Users/ryan/Code/Go/OpenFlare 二进制

  • Produces: 与金标准现跑一致的夹具

  • Step 1: 用金标准导出 schema 与版本

在 临时目录 跑金标准(不要写金标准树):

GOLD=/Users/ryan/Code/Go/OpenFlare
TMP=$(mktemp -d)
(cd "$GOLD" && go build -o "$TMP/of-baseline" .)
OF_DB_PATH="$TMP/a.db" timeout 25 "$TMP/of-baseline" api >"$TMP/a.log" 2>&1 || true
sqlite3 "$TMP/a.db" "SELECT name,sql FROM sqlite_master WHERE type='table' ORDER BY name;" > /tmp/schema-A.sql
sqlite3 "$TMP/a.db" "SELECT version_id FROM goose_db_version ORDER BY version_id;" > /tmp/versions-A.txt || \
sqlite3 "$TMP/a.db" "SELECT version FROM goose_db_version ORDER BY version;" > /tmp/versions-A.txt

与 docs/superpowers/specs/baseline/ 对比。不一致则以金标准为准更新夹具。版本最大应为 202608090003。

  • Step 2: 提交夹具(仅有 diff 时)
git add docs/superpowers/specs/baseline
git commit -m "docs(cordis): refresh baseline fixtures from OpenFlare v3.5.4"

无 diff 则不提交。


Task 12: OpenFlare cmd 同构 + server 只挂业务

Files:

  • Create: backend/cmd/app.go(从 Wavelet backend/cmd/app.go 拷贝后改)
  • Modify: backend/cmd/{api.go,all.go,worker.go,scheduler.go,root.go,banner.go}
  • Delete: backend/cmd/bootstrap.go、backend/cmd/http_server.go
  • Modify: backend/OpenFlare/plugins/server/plugin.go
  • Modify: backend/OpenFlare/plugins/server/openflare/apiutil/middleware.go
  • Modify: backend/OpenFlare/plugins/server/router/v1/v1.go、openflare/*.go(不再调 RegisterUserRoutes/RegisterAdminRoutes)
  • Modify: config.example.yaml(app.redirect_trailing_slash: false 或与 W4 字段对齐)

Interfaces:

  • Consumes: Wavelet newWaveletApp 插件列表;core.WithMigrationBaseline(stamp 函数可先用 func(*core.Context) error { return nil } 占位,Task 14 替换成真 stamp)

  • Produces: func newOpenFlareApp(profile core.Profile, opts ...core.AppOption) *core.App

  • Step 1: 写失败测试 backend/cmd/app_test.go

func TestNewOpenFlareAppRegistersServerAndWaveletUser(t *testing.T) {
    app := newOpenFlareApp(core.ProfileAPI, core.WithConfigSource(testSource(t)))
    if err := app.Prepare(); err != nil {
        t.Fatal(err)
    }
    names := map[string]bool{}
    for _, p := range app.Plugins() { // 若 Plugins() 未导出,改为检查 ctx.Router 路径
        names[p.Name()] = true
    }
    for _, n := range []string{"user", "auth", "cap", "admin", "server"} {
        if !names[n] {
            t.Errorf("missing plugin %s", n)
        }
    }
}

若 App 没有导出插件列表,改为 Prepare 后从 app.Context().Router().Routes() 断言同时存在 GET /api/v1/user/self(Wavelet user)与一条 OpenFlare 业务路径(例如 GET /api/v1/d/nodes 或现网 zone 路径——以 docs/superpowers/specs/baseline/routes-engine.txt 里 of_ 控制台路径为准)。不得再由 server 注册 POST /api/cap/challenge 作为「server 自己的」——这条应来自 cap 插件。

另测:GET /api/health 存在。

  • Step 2: 跑测试确认失败
cd /Users/ryan/Code/Go/OpenFlare-cordis/backend && go test ./cmd/ -run TestNewOpenFlareApp -count=1

Expected: FAIL(没有 newOpenFlareApp)。

  • Step 3: 实现装配根
  1. 复制 Wavelet cmd/app.go 为 OpenFlare cmd/app.go(保留 gooseEngine)。
  2. newOpenFlareApp = newWaveletApp 的 Use 列表,在 domain 之后、driver_http 之前插入 ofserver.New()。
  3. core.NewApp 增加 core.WithMigrationBaseline(stamp.Legacy)——此时 stamp 可返回 nil。
  4. driver_http.New() 不要 WithEngine。
  5. api.go/all.go/worker.go/scheduler.go 改为 runProfileApp(...),与 Wavelet 逐字同一调用。
  6. root.go 改为 Wavelet 的 hostConfig + idgen.Init;去掉 PreRun migrator。
  7. 删除 bootstrap.go、http_server.go。

server.Apply:

func (p *Plugin) Apply(ctx *core.Context) error {
    var auth contracts.AuthService
    if err := core.Using[contracts.AuthService](ctx, func(s contracts.AuthService) { auth = s }); err != nil {
        return err
    }
    ofrouter.RegisterV1Routes(ctx.Router().Group("/api/v1"), auth)
    ofrouter.RegisterRoutes(ctx.Router().Group("/api/v1"), auth)
    return nil
}

删除 RegisterUserRoutes / RegisterAdminRoutes / RegisterRootRoutes 对平台路径的调用。apiutil.AdminMiddlewares 改为接收 contracts.AuthService:

func AdminMiddlewares(auth contracts.AuthService) []any {
    return []any{auth.RequireAuthMiddleware(), auth.RequireAdminMiddleware()}
}

updater 路由改挂在 OpenFlare 自己的 admin 组上(/api/v1/admin/update*),中间件用 AdminMiddlewares(auth),不要 import 已删的 admin handler 包。

config.example.yaml 增加 app.redirect_trailing_slash: false(若 W4 把 key 放在 app 下;若放在独立 http 前缀则以实际 tag 为准)。DeclareConfig 在 driver_http,OpenFlare 只要 yaml 有该键。

  • Step 4: 测试通过
cd /Users/ryan/Code/Go/OpenFlare-cordis/backend && go test ./cmd/ -count=1

Expected: PASS。此时 go test ./OpenFlare/... 会因删除平台路由引用而红,下一任务修。

  • Step 5: 提交
git add backend/cmd backend/OpenFlare/plugins/server/plugin.go backend/OpenFlare/plugins/server/openflare/apiutil backend/OpenFlare/plugins/server/router config.example.yaml
git commit -m "feat(cmd): assemble control plane via Wavelet plugins plus server"

Task 13: 删除平台副本并改 import

Files:

  • Delete: backend/OpenFlare/plugins/server/{oauth,cap,user,upload,config,health,listener}
  • Delete: backend/OpenFlare/plugins/server/pkg/{cap,push}
  • Delete: backend/OpenFlare/plugins/server/admin/ 下除 updater/ 外全部
  • Delete: backend/OpenFlare/plugins/server/infra/(config/persistence/task/objectstore/diskcache;CH 小函数先挪走再删)
  • Delete: backend/OpenFlare/plugins/server/model 与 repository 中 w_* 实体(users、uploads、auth_source、system_configs、push_*、task_execution、templates、schedule、access_token、analytics/user_access_log)
  • Modify: 所有仍 import 上述包的 openflare/**、admin/updater、测试
  • Modify: platform/bootstrap — 删除或缩成空;任务注册改到 server.Apply 的 ctx.Task() / ctx.Schedule()
  • Create: backend/OpenFlare/plugins/server/publicconfig/provider.go

Interfaces:

  • Consumes: contracts.AuthService、CaptchaService(不要在 server 再挂 cap)、PushRegistry、PublicConfigProvider、contracts.DBService、StorageService、TaskService

  • Produces: publicconfig.Provider 实现 PublicConfigProvider,PublicConfig 返回 map[string]string(visibility=1 的 key/value,与金标准 GetPublicConfig 相同)

  • Step 1: 写失败测试 plugin_parity_test.go 改为子集断言

当前测试要求精确相等且只 server.New().Apply。改成:

func TestPluginRoutesContainGoldenBaseline(t *testing.T) {
    app := newOpenFlareAppForTest(t) // 与 cmd 测试同一装配,可用 sqlite + WithConfigValues
    if err := app.Prepare(); err != nil {
        t.Fatal(err)
    }
    got := routeSet(app.Context())
    want := loadBaseline(t)
    for k := range want {
        if !got[k] {
            t.Errorf("missing golden route %s", k)
        }
    }
}

newOpenFlareAppForTest 必须在 cmd 或 server 测试里能引用装配函数。若 cmd 测试不能 import 业务,把装配函数放 cmd,parity 测试移到 backend/cmd/parity_test.go。

先提交测试改动并确认:在平台副本仍在、Apply 已不挂平台路由时,缺 POST /api/v1/user/login 等会失败——等 Wavelet 插件挂上后应变绿。

  • Step 2: 跑 parity 看缺哪些金标准路径
cd /Users/ryan/Code/Go/OpenFlare-cordis/backend && go test ./cmd/ -run TestPluginRoutesContainGoldenBaseline -count=1

记下 missing 列表。缺 login/cap/health/self/upload/my 说明 Wavelet 插件没挂上;缺 /api/v1/d/... 说明 OpenFlare 业务没挂上。

  • Step 3: 按 missing 修 Apply / 契约中间件,然后删副本

替换规则(全文 grep):

旧 新
oauth.LoginRequired() auth.RequireAuthMiddleware().(gin.HandlerFunc)
admin.LoginAdminRequired() auth.RequireAdminMiddleware().(gin.HandlerFunc)
db.DB(ctx) contracts.DBService.GORM().WithContext(ctx)(经 Inject/字段)
config.Config.* ctx.Config().Bind / String/Bool
push.RegisterBuiltInEvent PushRegistry.RegisterBuiltInEvent
repository 的 user/upload/w_* 禁止;改走契约或删调用

server.Apply 增加:

core.Provide[contracts.PublicConfigProvider](ctx, publicconfig.New(ctx))
if pr, err := core.Inject[contracts.PushRegistry](ctx); err == nil {
    for _, meta := range ofevents.All() {
        pr.RegisterBuiltInEvent(meta)
    }
    _ = pr.SyncEvents(ctx.GoContext())
}
ctx.Migrations().Register("server", ofMigrations)

openflare 异步任务:现 infra/task/handlers 里属于 OF 的 handler 迁到 openflare/tasks,在 Apply 里 ctx.Task().Register。平台任务(user:send_email_code 等)不要再注册。

删目录后 go test ./OpenFlare/... 以编译器为准修到绿。

  • Step 4: 测试
cd /Users/ryan/Code/Go/OpenFlare-cordis/backend && go test ./cmd/ ./OpenFlare/...

Expected: PASS。rg 'OpenFlare/plugins/server/(oauth|cap|user|upload|config|health)\\b' backend/OpenFlare 无匹配。

  • Step 5: 提交
git add -A backend/OpenFlare backend/cmd
git commit -m "refactor(server): drop Wavelet-duplicated domains and use contracts"

Task 14: stamp + of_* 压缩迁移 + 删除关系库 migrator

Files:

  • Create: backend/OpenFlare/plugins/server/stamp/stamp.go
  • Create: backend/OpenFlare/plugins/server/stamp/stamp_test.go
  • Create: backend/OpenFlare/plugins/server/migrations/postgres/00001_initial.sql
  • Create: backend/OpenFlare/plugins/server/migrations/sqlite/00001_initial.sql
  • Modify: backend/cmd/app.go(WithMigrationBaseline(stamp.Legacy))
  • Create: backend/OpenFlare/plugins/server/chmigrate/chmigrate.go(从旧 migrator.MigrateClickHouse 抽出,只处理 of_node_* 文件)
  • Delete: backend/OpenFlare/plugins/server/infra/persistence/migrator/(关系库部分;CH sql 挪到 chmigrate/goose/clickhouse,去掉 create_user_access_logs)

Interfaces:

  • Consumes: core.Context + contracts.DBService
  • Produces: func Legacy(ctx *core.Context) error

Legacy 行为:

  1. Inject DBService;拿 sql.DB。
  2. 探测 goose_db_version 是否存在。不存在则 return nil(新装)。
  3. SELECT COALESCE(MAX(version_id),0) 或 goose 默认列名 version(金标准用 goose 默认表:列是 version_id 还是 version 以 Task 11 导出为准,代码按实表扫描 information_schema/pragma)。
  4. 插入 w_schema_versions (plugin_id, version_id):('openflare/legacy', 0)、('openflare/legacy', maxVer)、('server', 1),全部 ON CONFLICT DO NOTHING。
  5. 若 maxVer >= 202607120002 && maxVer < 202607130001,调用 zone.ImportLegacyTx。金标准 202608090003 跳过。
  6. 幂等:再调一次零 INSERT。
  • Step 1: 写失败测试(用 t.TempDir() sqlite)

建一张假 goose_db_version(版本 202608090003)和空的 w_schema_versions(用与 Wavelet 相同的 DDL)。调用 Legacy。断言三行 stamp 存在;再调一次行数不变。另测:无 goose_db_version 时不写 openflare/legacy。

  • Step 2: 跑测试确认失败
cd /Users/ryan/Code/Go/OpenFlare-cordis/backend && go test ./OpenFlare/plugins/server/stamp/ -count=1

Expected: FAIL。

  • Step 3: 实现 stamp;cmd 传入 WithMigrationBaseline(stamp.Legacy)。

生成 of_* initial:从 Task 11 的 $TMP/a.db(或金标准跑出来的库)抽取:

sqlite3 "$TMP/a.db" "SELECT sql FROM sqlite_master WHERE type='table' AND name LIKE 'of_%' ORDER BY name;"

写成 sqlite 00001_initial.sql(每条 CREATE TABLE IF NOT EXISTS,加 -- +goose Up)。postgres 目录用同一批表,类型按现有 OF postgres 历史链的最终形态(对照金标准若只有 sqlite 夹具,postgres 从 migrator/goose/postgres 里 of_* 的最终 CREATE/ALTER 手工收敛成一张 IF NOT EXISTS,禁止 DROP)。双方言版本号都是 00001。

CH:复制 goose/clickhouse 到 chmigrate,删除用户访问日志那个文件。server.Apply 在 API profile 下调用 chmigrate.Up(保留旧错误日志行为)。

删除 migrator.Migrate 与 Cobra PreRun。76 条 sql 不再 embed。可留在 git 里但不要 go:embed。

  • Step 4: 测试通过
cd /Users/ryan/Code/Go/OpenFlare-cordis/backend && go test ./OpenFlare/plugins/server/stamp/ ./cmd/ -count=1

Expected: PASS。

  • Step 5: 提交
git add backend/OpenFlare/plugins/server/stamp backend/OpenFlare/plugins/server/migrations backend/OpenFlare/plugins/server/chmigrate backend/cmd
git commit -m "feat(server): stamp legacy goose versions and own of_* migrations only"

Task 15: L3 升级测试(金标准库 → Cordis)

Files:

  • Create: backend/cmd/upgrade_from_golden_test.go

Interfaces:

  • Consumes: 金标准二进制(只读调用)、newOpenFlareApp

  • Produces: 自动化 A→C 升级断言

  • Step 1: 写失败测试

测试内:

  1. t.TempDir()。
  2. exec.Command 构建金标准:go build -o $tmp/gold /Users/ryan/Code/Go/OpenFlare(module 路径按金标准 main.go)。
  3. 设 OF_DB_PATH/CONFIG 指向 temp sqlite,跑金标准 api 数秒直到 goose 成功(看日志或轮询表 of_nodes 存在)。超时失败。
  4. 抽样 INSERT 一行可识别数据到 of_zones 或读 seed 行数。
  5. 对 同一 db 文件 newOpenFlareApp(ProfileAPI) + Prepare()(不要 Run 以免占端口;若 Prepare 含迁移即足够)。
  6. 断言:w_schema_versions 有 openflare/legacy 与 server;goose_db_version max 仍为 202608090003;抽样行仍在;of_* 表列不丢。
  7. 第二次 Prepare() 不增加 openflare/legacy 行数。

Postgres:若 CI/本机有 DATABASE,用 t.Skip 以外的短测试连本机 PG;无 PG 则 sqlite 必跑,PG 用 go test -tags pgupgrade 可选。规格要求 Postgres 必做——测试读 TEST_PG_DSN,未设则 t.Fatal 在 TestUpgradePostgresFromGolden 除非 文档写明开发机需提供。计划要求:sqlite 子测试无条件跑;Postgres 子测试在 TEST_PG_DSN 为空时 Skip,并在 Task 16 的人工清单里用 docker postgres 跑一遍(下面 Step 4)。

  • Step 2: 跑测试确认失败(stamp 未接好会红)
cd /Users/ryan/Code/Go/OpenFlare-cordis/backend && go test ./cmd/ -run TestUpgradeFromGolden -count=1 -timeout 120s
  • Step 3: 修到 PASS

  • Step 4: 本地 Postgres

docker run -d --name of-pg-test -e POSTGRES_PASSWORD=test -p 5432:5432 postgres:16
TEST_PG_DSN='postgres://postgres:test@127.0.0.1:5432/postgres?sslmode=disable' \
  go test ./cmd/ -run TestUpgradePostgresFromGolden -count=1 -timeout 180s

Expected: PASS。

  • Step 5: 提交
git add backend/cmd/upgrade_from_golden_test.go
git commit -m "test(cmd): upgrade sqlite/postgres from OpenFlare v3.5.4 golden"

Task 16: L2 全量门禁

Files: 无新功能文件;修测试/swagger 直到绿

  • Step 1: Cordis 全测
cd /Users/ryan/Code/Go/OpenFlare-cordis/backend && go test ./...

Expected: PASS。修到绿。

  • Step 2: swagger 超集
cd /Users/ryan/Code/Go/OpenFlare-cordis && make swagger
python3 - <<'PY'
import json
gold=json.load(open("/Users/ryan/Code/Go/OpenFlare/docs/swagger.json"))
new=json.load(open("docs/swagger.json"))
def ops(d):
    s=set()
    for p,v in d.get("paths",{}).items():
        for m in v:
            if m in ("get","post","put","delete","patch","head","options"):
                s.add(f"{m.upper()} {p}")
    return s
missing=sorted(ops(gold)-ops(new))
if missing:
    raise SystemExit("missing: "+"\n".join(missing))
print("ok", len(ops(gold)), "golden ops present; extra", len(ops(new)-ops(gold)))
PY

Expected: 无 missing。GET /api/v1/config/public 的成功响应仍是扁平 map(查 swagger schema / 手写测试打 handler)。

  • Step 3: 前端零 diff
git -C /Users/ryan/Code/Go/OpenFlare-cordis diff --name-only -- frontend/

Expected: 空。

  • Step 4: 金标准树未被修改
git -C /Users/ryan/Code/Go/OpenFlare status --short

Expected: 空。

  • Step 5: 提交本任务产生的测试/swagger 修复(若有)

Task 17: git merge 接入 Wavelet 上游

Files:

  • Modify: .gitattributes
  • Delete: scripts/sync-upstream.sh(接线成功后)
  • Modify: backend/OpenFlare/README.md(同步方式改为 git merge wavelet/main)

Interfaces:

  • Consumes: Task 16 全绿;Wavelet 含 W1–W9 的 main/工作树

  • Produces: git merge-base HEAD wavelet/main 非空

  • Step 1: 写 .gitattributes 追加

backend/OpenFlare/**    merge=ours
frontend/**             merge=ours
docs/changelog/**       merge=ours
docs/superpowers/**     merge=ours

先提交这一文件。

git add .gitattributes
git commit -m "chore(git): keep OpenFlare-owned paths on merge"
  • Step 2: 添加 remote 并第一次 merge
git remote add wavelet /Users/ryan/Code/Go/Wavelet 2>/dev/null || git remote set-url wavelet /Users/ryan/Code/Go/Wavelet
git fetch wavelet
git merge --allow-unrelated-histories wavelet/main

冲突:backend/{core,pkg,plugins} 以 Wavelet 为准;frontend/、backend/OpenFlare/、docs/superpowers/、docs/changelog/ 保持 OpenFlare(git checkout --ours)。backend/cmd 三路解决:保留 server.New()、WithMigrationBaseline、agent/relay/flared。不要 rebase。

  • Step 3: 验证历史
git merge-base HEAD wavelet/main   # 非空
git log --first-parent -5          # 仍是 OpenFlare 提交叙事
git diff --name-only -- frontend/  # 相对 merge 前应无意外
cd backend && go test ./cmd/ ./OpenFlare/plugins/server/stamp/ -count=1
  • Step 4: 删除 rsync 脚本并提交 merge
git rm scripts/sync-upstream.sh
git add backend
git commit --no-edit  # 若 merge 已产生 commit 则再 chore 删脚本

若 Step 2 的 merge 已提交,则:

git rm scripts/sync-upstream.sh
git commit -m "chore(cordis): drop rsync sync after git upstream is connected"

自检(对照 spec)

Spec Task
D1 契约冻结 / 不改前端 16 Step 2–3
D2 删全部平台副本 13
D3 stamp 14、15
D4 health/self/cap 在 Wavelet 3、8;12 禁止 server 再挂
D5 git merge 17
D6 不承担同类职责 12–14
D7 金标准只读 1、16 Step 4
D8 先对齐再 merge 16 在 17 前
W1–W9 2–9
cmd 同构 12
L1/L2/L3 1、16、15
不做 pow / 第二套 migrator / 改 agent CLI 全局约束

无 TBD。CaptchaService / PublicConfigProvider / PushRegistry / WithMigrationBaseline / stamp.Legacy 名称前后一致。