按 W1–W9、装配根、删平台副本、stamp 升级与 git merge 拆成 带测试的任务,金标准 v3.5.4 只读。
46 KiB
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,tagconfig:"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(从 Waveletbackend/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: 实现装配根
- 复制 Wavelet
cmd/app.go为 OpenFlarecmd/app.go(保留gooseEngine)。 newOpenFlareApp=newWaveletApp的 Use 列表,在 domain 之后、driver_http之前插入ofserver.New()。core.NewApp增加core.WithMigrationBaseline(stamp.Legacy)——此时 stamp 可返回 nil。driver_http.New()不要WithEngine。api.go/all.go/worker.go/scheduler.go改为runProfileApp(...),与 Wavelet 逐字同一调用。root.go改为 Wavelet 的 hostConfig + idgen.Init;去掉PreRunmigrator。- 删除
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 行为:
- Inject
DBService;拿sql.DB。 - 探测
goose_db_version是否存在。不存在则 return nil(新装)。 SELECT COALESCE(MAX(version_id),0)或 goose 默认列名version(金标准用 goose 默认表:列是version_id还是version以 Task 11 导出为准,代码按实表扫描information_schema/pragma)。- 插入
w_schema_versions (plugin_id, version_id):('openflare/legacy', 0)、('openflare/legacy', maxVer)、('server', 1),全部ON CONFLICT DO NOTHING。 - 若
maxVer >= 202607120002 && maxVer < 202607130001,调用zone.ImportLegacyTx。金标准202608090003跳过。 - 幂等:再调一次零 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: 写失败测试
测试内:
t.TempDir()。exec.Command构建金标准:go build -o $tmp/gold /Users/ryan/Code/Go/OpenFlare(module 路径按金标准main.go)。- 设
OF_DB_PATH/CONFIG指向 temp sqlite,跑金标准api数秒直到goose成功(看日志或轮询表of_nodes存在)。超时失败。 - 抽样
INSERT一行可识别数据到of_zones或读 seed 行数。 - 对 同一 db 文件
newOpenFlareApp(ProfileAPI)+Prepare()(不要Run以免占端口;若 Prepare 含迁移即足够)。 - 断言:
w_schema_versions有openflare/legacy与server;goose_db_versionmax 仍为202608090003;抽样行仍在;of_*表列不丢。 - 第二次
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 名称前后一致。