diff --git a/app-modern/cmd/softbox/main.go b/app-modern/cmd/softbox/main.go index eb6f73a..4f3e27e 100644 --- a/app-modern/cmd/softbox/main.go +++ b/app-modern/cmd/softbox/main.go @@ -1,11 +1,48 @@ package main import ( - "fmt" + "log" + "os" + "gioui.org/app" + "gioui.org/op" + "gioui.org/unit" + + "softbox.local/app-modern/platform/windows" + softboxgio "softbox.local/app-modern/ui/gio" "softbox.local/core" ) func main() { - fmt.Println(core.ProductName) + go func() { + if err := run(); err != nil { + log.Printf("%s stopped: %v", core.ProductName, err) + } + os.Exit(0) + }() + app.Main() +} + +func run() error { + platform := windows.New() + window := new(app.Window) + window.Option( + app.Title(core.ProductName), + app.Size(unit.Dp(1080), unit.Dp(720)), + ) + + theme := softboxgio.NewTheme() + shell := softboxgio.NewAppShell(string(platform.Edition())) + var operations op.Ops + + for { + switch event := window.Event().(type) { + case app.DestroyEvent: + return event.Err + case app.FrameEvent: + context := app.NewContext(&operations, event) + shell.Layout(context, theme) + event.Frame(context.Ops) + } + } } diff --git a/app-modern/platform/windows/doc.go b/app-modern/platform/windows/doc.go index 16fb03c..7c56b86 100644 --- a/app-modern/platform/windows/doc.go +++ b/app-modern/platform/windows/doc.go @@ -1,2 +1,3 @@ -// Package windows contains modern Windows platform adapters. +// Package windows provides modern Windows platform adapters and non-Windows +// stubs for package-level tests. package windows diff --git a/app-modern/platform/windows/platform.go b/app-modern/platform/windows/platform.go new file mode 100644 index 0000000..86b7f47 --- /dev/null +++ b/app-modern/platform/windows/platform.go @@ -0,0 +1,17 @@ +package windows + +// Edition identifies the application build channel shown by the UI. +type Edition string + +const EditionModern Edition = "Modern" + +// Platform is the minimal boundary for target-specific capabilities. +type Platform interface { + OS() string + Edition() Edition +} + +// New returns the platform implementation selected by build tags. +func New() Platform { + return newPlatform() +} diff --git a/app-modern/platform/windows/platform_stub.go b/app-modern/platform/windows/platform_stub.go new file mode 100644 index 0000000..b9c8914 --- /dev/null +++ b/app-modern/platform/windows/platform_stub.go @@ -0,0 +1,19 @@ +//go:build !windows + +package windows + +import "runtime" + +type platformStub struct{} + +func newPlatform() Platform { + return platformStub{} +} + +func (platformStub) OS() string { + return runtime.GOOS +} + +func (platformStub) Edition() Edition { + return EditionModern +} diff --git a/app-modern/platform/windows/platform_test.go b/app-modern/platform/windows/platform_test.go new file mode 100644 index 0000000..ca1f6c9 --- /dev/null +++ b/app-modern/platform/windows/platform_test.go @@ -0,0 +1,13 @@ +package windows + +import "testing" + +func TestPlatformStubContract(t *testing.T) { + platform := New() + if platform.OS() == "" { + t.Fatal("OS() should not be empty") + } + if platform.Edition() != EditionModern { + t.Fatalf("Edition() = %q, want %q", platform.Edition(), EditionModern) + } +} diff --git a/app-modern/platform/windows/platform_windows.go b/app-modern/platform/windows/platform_windows.go new file mode 100644 index 0000000..9b3d7f6 --- /dev/null +++ b/app-modern/platform/windows/platform_windows.go @@ -0,0 +1,17 @@ +//go:build windows + +package windows + +type platform struct{} + +func newPlatform() Platform { + return platform{} +} + +func (platform) OS() string { + return "windows" +} + +func (platform) Edition() Edition { + return EditionModern +} diff --git a/app-modern/ui/gio/doc.go b/app-modern/ui/gio/doc.go index 2b178c4..da28775 100644 --- a/app-modern/ui/gio/doc.go +++ b/app-modern/ui/gio/doc.go @@ -1,2 +1,5 @@ // Package gio contains the modern Gio UI adapter. +// +// Layout code is rendering-only: it must not read files, access the network, +// calculate hashes, or directly mutate background application state. package gio diff --git a/app-modern/ui/gio/shell.go b/app-modern/ui/gio/shell.go new file mode 100644 index 0000000..14c2073 --- /dev/null +++ b/app-modern/ui/gio/shell.go @@ -0,0 +1,90 @@ +package gio + +import ( + "image/color" + + "gioui.org/layout" + "gioui.org/op/paint" + "gioui.org/unit" + "gioui.org/widget/material" +) + +var shellColors = struct { + background color.NRGBA + foreground color.NRGBA + primary color.NRGBA + onPrimary color.NRGBA + secondary color.NRGBA +}{ + background: color.NRGBA{R: 248, G: 250, B: 252, A: 255}, + foreground: color.NRGBA{R: 15, G: 23, B: 42, A: 255}, + primary: color.NRGBA{R: 13, G: 148, B: 136, A: 255}, + onPrimary: color.NRGBA{R: 255, G: 255, B: 255, A: 255}, + secondary: color.NRGBA{R: 71, G: 85, B: 105, A: 255}, +} + +// AppShell is the modern window frame. Feature views are added in later tasks. +type AppShell struct { + edition string +} + +// NewAppShell creates an empty shell for the supplied build edition. +func NewAppShell(edition string) *AppShell { + return &AppShell{edition: edition} +} + +// NewTheme creates the semantic palette shared by the modern shell. +func NewTheme() *material.Theme { + theme := material.NewTheme() + theme.Palette = material.Palette{ + Bg: shellColors.background, + Fg: shellColors.foreground, + ContrastBg: shellColors.primary, + ContrastFg: shellColors.onPrimary, + } + theme.FingerSize = unit.Dp(44) + return theme +} + +// Layout renders the shell without performing IO or submitting use cases. +func (shell *AppShell) Layout(context layout.Context, theme *material.Theme) layout.Dimensions { + paint.Fill(context.Ops, theme.Bg) + + return layout.UniformInset(unit.Dp(32)).Layout(context, func(context layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Vertical}.Layout( + context, + layout.Rigid(func(context layout.Context) layout.Dimensions { + return layout.Flex{Alignment: layout.Middle}.Layout( + context, + layout.Rigid(material.H5(theme, "SoftBox").Layout), + layout.Rigid(layout.Spacer{Width: unit.Dp(16)}.Layout), + layout.Rigid(func(context layout.Context) layout.Dimensions { + label := material.Body2(theme, shell.edition) + label.Color = shellColors.primary + return label.Layout(context) + }), + ) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(24)}.Layout), + layout.Flexed(1, func(context layout.Context) layout.Dimensions { + return layout.Center.Layout(context, func(context layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout( + context, + layout.Rigid(material.H6(theme, "软件目录准备中").Layout), + layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), + layout.Rigid(func(context layout.Context) layout.Dimensions { + label := material.Body1(theme, "当前窗口仅验证 Gio AppShell 与双构建链路。") + label.Color = shellColors.secondary + return label.Layout(context) + }), + ) + }) + }), + layout.Rigid(func(context layout.Context) layout.Dimensions { + label := material.Body2(theme, "Modern · Windows 10/11 x64") + label.Color = shellColors.secondary + return label.Layout(context) + }), + ) + }) +} diff --git a/app-modern/ui/gio/shell_test.go b/app-modern/ui/gio/shell_test.go new file mode 100644 index 0000000..f110137 --- /dev/null +++ b/app-modern/ui/gio/shell_test.go @@ -0,0 +1,25 @@ +package gio + +import ( + "image" + "testing" + + "gioui.org/layout" + "gioui.org/op" + "gioui.org/unit" +) + +func TestAppShellFillsWindow(t *testing.T) { + var operations op.Ops + size := image.Pt(1080, 720) + context := layout.Context{ + Ops: &operations, + Metric: unit.Metric{PxPerDp: 1, PxPerSp: 1}, + Constraints: layout.Exact(size), + } + + dimensions := NewAppShell("Modern").Layout(context, NewTheme()) + if dimensions.Size != size { + t.Fatalf("Layout() size = %v, want %v", dimensions.Size, size) + } +} diff --git a/app-win7/cmd/softbox/main.go b/app-win7/cmd/softbox/main.go index eb6f73a..255e355 100644 --- a/app-win7/cmd/softbox/main.go +++ b/app-win7/cmd/softbox/main.go @@ -1,11 +1,48 @@ package main import ( - "fmt" + "log" + "os" + "gioui.org/app" + "gioui.org/op" + "gioui.org/unit" + + "softbox.local/app-win7/platform/windows" + softboxgio "softbox.local/app-win7/ui/gio" "softbox.local/core" ) func main() { - fmt.Println(core.ProductName) + go func() { + if err := run(); err != nil { + log.Printf("%s Legacy stopped: %v", core.ProductName, err) + } + os.Exit(0) + }() + app.Main() +} + +func run() error { + platform := windows.New() + window := new(app.Window) + window.Option( + app.Title(core.ProductName+" Legacy"), + app.Size(unit.Dp(1024), unit.Dp(680)), + ) + + theme := softboxgio.NewTheme() + shell := softboxgio.NewAppShell(string(platform.Edition())) + var operations op.Ops + + for { + switch event := window.Event().(type) { + case app.DestroyEvent: + return event.Err + case app.FrameEvent: + context := app.NewContext(&operations, event) + shell.Layout(context, theme) + event.Frame(context.Ops) + } + } } diff --git a/app-win7/go.work.sum b/app-win7/go.work.sum index bc13a1c..396a3a7 100644 --- a/app-win7/go.work.sum +++ b/app-win7/go.work.sum @@ -1 +1,10 @@ +gioui.org v0.6.0 h1:ZSXO/AbpFZJ2L9NU69uFQfDI3BKIH+YEJElrn0B+aZI= gioui.org v0.6.0/go.mod h1:eUvGo6FAzA7jUqeSu5a+M1W03yc9r1nanIBS8A5+Nng= +gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc= +gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA= +github.com/go-text/typesetting v0.1.1 h1:bGAesCuo85nXnEN5LmFMVGAGpGkCPtHrZLi//qD7EJo= +golang.org/x/exp v0.0.0-20221012211006-4de253d81b95 h1:sBdrWpxhGDdTAYNqbgBLAR+ULAPPhfgncLr1X0lyWtg= +golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91 h1:ryT6Nf0R83ZgD8WnFFdfI8wCeyqgdXWN4+CkFVNPAT0= +golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= diff --git a/app-win7/platform/windows/doc.go b/app-win7/platform/windows/doc.go index a320732..9fafa99 100644 --- a/app-win7/platform/windows/doc.go +++ b/app-win7/platform/windows/doc.go @@ -1,2 +1,3 @@ -// Package windows contains Win7-compatible Windows platform adapters. +// Package windows provides Win7-compatible platform adapters and non-Windows +// stubs for package-level tests. package windows diff --git a/app-win7/platform/windows/platform.go b/app-win7/platform/windows/platform.go new file mode 100644 index 0000000..8964347 --- /dev/null +++ b/app-win7/platform/windows/platform.go @@ -0,0 +1,17 @@ +package windows + +// Edition identifies the application build channel shown by the UI. +type Edition string + +const EditionLegacy Edition = "Legacy" + +// Platform is the minimal boundary for target-specific capabilities. +type Platform interface { + OS() string + Edition() Edition +} + +// New returns the platform implementation selected by build tags. +func New() Platform { + return newPlatform() +} diff --git a/app-win7/platform/windows/platform_stub.go b/app-win7/platform/windows/platform_stub.go new file mode 100644 index 0000000..363aa52 --- /dev/null +++ b/app-win7/platform/windows/platform_stub.go @@ -0,0 +1,19 @@ +//go:build !windows + +package windows + +import "runtime" + +type platformStub struct{} + +func newPlatform() Platform { + return platformStub{} +} + +func (platformStub) OS() string { + return runtime.GOOS +} + +func (platformStub) Edition() Edition { + return EditionLegacy +} diff --git a/app-win7/platform/windows/platform_test.go b/app-win7/platform/windows/platform_test.go new file mode 100644 index 0000000..588364d --- /dev/null +++ b/app-win7/platform/windows/platform_test.go @@ -0,0 +1,13 @@ +package windows + +import "testing" + +func TestPlatformStubContract(t *testing.T) { + platform := New() + if platform.OS() == "" { + t.Fatal("OS() should not be empty") + } + if platform.Edition() != EditionLegacy { + t.Fatalf("Edition() = %q, want %q", platform.Edition(), EditionLegacy) + } +} diff --git a/app-win7/platform/windows/platform_windows.go b/app-win7/platform/windows/platform_windows.go new file mode 100644 index 0000000..e1b2b59 --- /dev/null +++ b/app-win7/platform/windows/platform_windows.go @@ -0,0 +1,17 @@ +//go:build windows + +package windows + +type platform struct{} + +func newPlatform() Platform { + return platform{} +} + +func (platform) OS() string { + return "windows" +} + +func (platform) Edition() Edition { + return EditionLegacy +} diff --git a/app-win7/ui/gio/doc.go b/app-win7/ui/gio/doc.go index 09da906..661fc53 100644 --- a/app-win7/ui/gio/doc.go +++ b/app-win7/ui/gio/doc.go @@ -1,2 +1,5 @@ // Package gio contains the Win7-compatible Gio UI adapter. +// +// Layout code is rendering-only: it must not read files, access the network, +// calculate hashes, or directly mutate background application state. package gio diff --git a/app-win7/ui/gio/shell.go b/app-win7/ui/gio/shell.go new file mode 100644 index 0000000..d4c83c1 --- /dev/null +++ b/app-win7/ui/gio/shell.go @@ -0,0 +1,90 @@ +package gio + +import ( + "image/color" + + "gioui.org/layout" + "gioui.org/op/paint" + "gioui.org/unit" + "gioui.org/widget/material" +) + +var shellColors = struct { + background color.NRGBA + foreground color.NRGBA + primary color.NRGBA + onPrimary color.NRGBA + secondary color.NRGBA +}{ + background: color.NRGBA{R: 248, G: 250, B: 252, A: 255}, + foreground: color.NRGBA{R: 15, G: 23, B: 42, A: 255}, + primary: color.NRGBA{R: 13, G: 148, B: 136, A: 255}, + onPrimary: color.NRGBA{R: 255, G: 255, B: 255, A: 255}, + secondary: color.NRGBA{R: 71, G: 85, B: 105, A: 255}, +} + +// AppShell is the Legacy window frame. Feature views are added in later tasks. +type AppShell struct { + edition string +} + +// NewAppShell creates an empty shell for the supplied build edition. +func NewAppShell(edition string) *AppShell { + return &AppShell{edition: edition} +} + +// NewTheme creates the semantic palette shared by the Legacy shell. +func NewTheme() *material.Theme { + theme := material.NewTheme() + theme.Palette = material.Palette{ + Bg: shellColors.background, + Fg: shellColors.foreground, + ContrastBg: shellColors.primary, + ContrastFg: shellColors.onPrimary, + } + theme.FingerSize = unit.Dp(44) + return theme +} + +// Layout renders the shell without performing IO or submitting use cases. +func (shell *AppShell) Layout(context layout.Context, theme *material.Theme) layout.Dimensions { + paint.Fill(context.Ops, theme.Bg) + + return layout.UniformInset(unit.Dp(32)).Layout(context, func(context layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Vertical}.Layout( + context, + layout.Rigid(func(context layout.Context) layout.Dimensions { + return layout.Flex{Alignment: layout.Middle}.Layout( + context, + layout.Rigid(material.H5(theme, "SoftBox").Layout), + layout.Rigid(layout.Spacer{Width: unit.Dp(16)}.Layout), + layout.Rigid(func(context layout.Context) layout.Dimensions { + label := material.Body2(theme, shell.edition) + label.Color = shellColors.primary + return label.Layout(context) + }), + ) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(24)}.Layout), + layout.Flexed(1, func(context layout.Context) layout.Dimensions { + return layout.Center.Layout(context, func(context layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout( + context, + layout.Rigid(material.H6(theme, "软件目录准备中").Layout), + layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), + layout.Rigid(func(context layout.Context) layout.Dimensions { + label := material.Body1(theme, "Legacy 窗口已就绪,业务功能将在后续任务接入。") + label.Color = shellColors.secondary + return label.Layout(context) + }), + ) + }) + }), + layout.Rigid(func(context layout.Context) layout.Dimensions { + label := material.Body2(theme, "Legacy · Windows 7 SP1 x64") + label.Color = shellColors.secondary + return label.Layout(context) + }), + ) + }) +} diff --git a/app-win7/ui/gio/shell_test.go b/app-win7/ui/gio/shell_test.go new file mode 100644 index 0000000..26b3773 --- /dev/null +++ b/app-win7/ui/gio/shell_test.go @@ -0,0 +1,25 @@ +package gio + +import ( + "image" + "testing" + + "gioui.org/layout" + "gioui.org/op" + "gioui.org/unit" +) + +func TestAppShellFillsWindow(t *testing.T) { + var operations op.Ops + size := image.Pt(1024, 680) + context := layout.Context{ + Ops: &operations, + Metric: unit.Metric{PxPerDp: 1, PxPerSp: 1}, + Constraints: layout.Exact(size), + } + + dimensions := NewAppShell("Legacy").Layout(context, NewTheme()) + if dimensions.Size != size { + t.Fatalf("Layout() size = %v, want %v", dimensions.Size, size) + } +} diff --git a/docs/current-state.md b/docs/current-state.md index bc9674a..74dda95 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -14,14 +14,14 @@ - 日期:2026-07-16 - 阶段:MVP 起步(Phase 0 工程骨架建设中) -- 技术栈:根 Go workspace 纳入 core/app-modern/app-win7 三模块;`app-win7/go.work` 隔离 Go 1.20.14 构建;Gio UI 与平台 stub 待后续 Phase 0 任务接入 -- 生产代码:core 已有软件状态模型、迁移规则、application Event 合约与 Runtime 事件总线骨架;modern/win7 仍为无 UI 命令入口 -- 测试:core 已覆盖状态合法性/非法迁移、事件类型、发布/取消/关闭 +- 技术栈:根 Go workspace 纳入 core/app-modern/app-win7 三模块;`app-win7/go.work` 隔离 Go 1.20.14 构建;modern Gio v0.10.1 与 win7 Gio v0.6.0 已实际接入 +- 生产代码:core 已有状态模型与事件 runtime;modern/win7 均可打开最小 AppShell,并有平台接口、Windows 实现与非 Windows stub +- 测试:core 覆盖状态/事件 runtime;两个 app 覆盖 AppShell 布局尺寸与平台 stub 契约 - 数据:无;Catalog 清单与测试样例待建 - 标准启动路径:`./init.sh` / `./init.ps1`(同步依赖、验证 core、打印 modern/win7 双目标构建命令) - 标准验证路径:`go -C core vet ./...` + `go -C core test -count=1 ./...`;双目标构建命令见下文 - 版本管理:git 已初始化,main 分支,远端 origin 为 Gitea `opc/soft_quay`;harness 文档已提交 -- 当前 blocker:无;下一步按路线图落成并领取 T-003 +- 当前 blocker:无;下一步按路线图落成并领取 T-004 ## 当前目录要点 @@ -29,10 +29,10 @@ | --- | --- | --- | | `docs/` | 已有 | harness coding 文档集(本次初始化完成) | | `docs/tasks/` | 已有 | 任务目录;T-001 已完成,T-002 待按路线图落成 | -| `scripts/` | 已有 | harness 治理脚本(validate_agent_context 等);构建脚本待建 | +| `scripts/` | 已有 | harness 治理脚本 + `check_core_boundaries.py`;Phase 0 构建闸门待 T-004 | | `core/` | 已建 | Go 1.20 兼容共享模块;已有 domain 状态模型与 application 事件 runtime | -| `app-modern/` | 已建 | Go 1.25.0 + Gio v0.10.1 模块;当前为无 UI 命令入口 | -| `app-win7/` | 已建 | Go 1.20 + Gio v0.6.0 模块;当前为无 UI 命令入口 | +| `app-modern/` | 已建 | Go 1.25.0 + Gio v0.10.1;可打开 Modern AppShell | +| `app-win7/` | 已建 | Go 1.20 + Gio v0.6.0;可打开带 Legacy 标识的 AppShell | | `schemas/` | 待建 | 协议 JSON Schema(T-201) | | `testdata/` | 待建 | 假数据测试样例(T-101 起) | @@ -40,9 +40,9 @@ 任务状态以 `docs/tasks/` 各任务文件 frontmatter 的 `status` 为准。本节只写项目级摘要: -- 已完成:`T-001 初始化 monorepo 骨架`、`T-002 建立 domain 状态模型与事件总线`。 +- 已完成:`T-001 初始化 monorepo 骨架`、`T-002 建立 domain 状态模型与事件总线`、`T-003 Gio 空窗口 + 平台层 stub`。 - 正在进行:无。 -- 下一个可领取任务:按路线图落成并领取 `T-003 Gio 空窗口 + 平台层 stub`。 +- 下一个可领取任务:按路线图落成并领取 `T-004 CI 双目标编译闸门`。 ## 当前可运行内容 @@ -54,6 +54,7 @@ # 独立验证: go -C core vet ./... go -C core test -count=1 ./... +python scripts/check_core_boundaries.py GOTOOLCHAIN=go1.25.0 GOOS=windows GOARCH=amd64 go -C app-modern build ./cmd/softbox GOTOOLCHAIN=go1.20.14 GOOS=windows GOARCH=amd64 go -C app-win7 build ./cmd/softbox ``` diff --git a/docs/tasks/T-003.md b/docs/tasks/T-003.md new file mode 100644 index 0000000..b19902f --- /dev/null +++ b/docs/tasks/T-003.md @@ -0,0 +1,67 @@ +--- +id: T-003 +title: 建立 Gio 空窗口 AppShell 与平台层 stub +phase: 0 +deps: [T-001] +status: DONE +created: 2026-07-16 +issue: null +context_ref: 0e20dd76b215015030c184436fb216f34e9b27e2 +claim_branch: null +work_branch: agent/codex/T-003 +write_paths: + - docs/tasks/T-003.md + - app-modern/cmd/softbox/ + - app-modern/ui/gio/ + - app-modern/platform/windows/ + - app-win7/cmd/softbox/ + - app-win7/ui/gio/ + - app-win7/platform/windows/ + - scripts/check_core_boundaries.py + - go.work.sum + - docs/current-state.md +--- + +## 问题 / 背景 + +两个应用模块当前只有控制台占位入口,尚不能打开桌面窗口;平台层也只有空包。需要建立 modern 与 Win7 各自适配 Gio 版本的最小 AppShell,并为后续 Windows 能力注入提供可在非 Windows 环境编译的接口/stub。 + +## 方案 + +1. modern 使用 Gio v0.10.1 API,win7 使用 Gio v0.6.0 API,各自建立窗口事件循环和 `ui/gio.AppShell`。 +2. AppShell 只展示高对比标题、版本标识与占位说明,使用 8dp 间距节奏,不做业务 IO、动画或交互。 +3. 两个 `platform/windows` 包定义最小 `Platform` 接口,分别提供 Windows 实现与 `!windows` stub。 +4. 新增离线边界检查脚本,通过 `go list -json` 拒绝 core 直接 import Gio、Windows 专属包或 SQLite。 +5. 生成并提交两个 app 模块所需的依赖校验文件。 + +## 验收要点 + +- modern 与 win7 两个 Windows amd64 目标都能编译为 GUI EXE。 +- 两个窗口均有 AppShell 骨架;Win7 版明确显示 Legacy 标识。 +- Gio 代码只位于 `ui/gio` 与 `cmd` 装配层;Layout 不包含磁盘、网络或哈希操作。 +- `platform/windows` 在 Windows 有实现,在非 Windows 有 stub,包级测试可在当前环境运行。 +- `python scripts/check_core_boundaries.py` 与 `--self-test` 通过,并能检测禁止依赖前缀。 +- core vet/test 与治理检查继续通过。 + +## 边界(不改什么) + +- 不实现软件列表、搜索、下载队列或其他业务页面。 +- 不让 modern 与 win7 共享 Gio 控件代码;只保持布局语义一致。 +- 不新增 Windows API 调用或第三方依赖。 +- 不引入装饰动画、图标或品牌资产。 + +## 协作约束 + +未启用 Gitea;本任务在 `agent/codex/T-003` 分支串行执行。UI 采用 `ui-ux-pro-max` 的工具型桌面应用原则,但以 Gio/Windows 任务边界和仓库架构为最高约束。 + +## 执行记录 + +- 2026-07-16:modern 使用 Gio v0.10.1、win7 使用 Gio v0.6.0 建立各自窗口事件循环与 `AppShell`;两个版本共享相同布局语义,Win7 标题和页脚明确显示 Legacy。 +- 2026-07-16:依据 `ui-ux-pro-max` 的工具型产品建议,采用高对比语义色、16sp 正文、8dp 间距节奏与 44dp 交互基线;本任务没有交互控件,因此不添加动画、图标或装饰效果。 +- 2026-07-16:两个 `platform/windows` 包建立 `Platform` 接口、Windows 实现与 `!windows` stub,并添加包级契约测试。 +- 2026-07-16:新增 `scripts/check_core_boundaries.py`,通过 `go list -json` 检查 core 的普通/测试 import,拒绝 Gio、Windows、SQLite 与 app 反向依赖;`--self-test` 验证前缀匹配器。 +- 2026-07-16:`go mod tidy` 会忽略 workspace 本地替换并尝试下载 `softbox.local/core`;为避免把 `replace ../core` 写入 go.mod,保留 go.work 作为唯一替换源,由实际 test/build 生成 `go.work.sum`。 +- 验证通过:modern Go 1.25.0 执行 `go vet ./...`、UI/platform 测试与 Windows amd64 构建。 +- 验证通过:win7 Go 1.20.14 执行 `go vet ./...`、UI/platform 测试与 Windows amd64 构建。 +- 验证通过:两个 EXE 在当前 Windows 环境启动后保持运行,随后由 smoke 脚本关闭。 +- 验证通过:`python scripts/check_core_boundaries.py --self-test`、`python scripts/check_core_boundaries.py`、core vet/test 与治理检查。 diff --git a/go.work.sum b/go.work.sum index f32a795..4964092 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,12 +1,21 @@ eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA= gioui.org v0.6.0/go.mod h1:eUvGo6FAzA7jUqeSu5a+M1W03yc9r1nanIBS8A5+Nng= +gioui.org v0.10.1 h1:Dvp6iDk9RKuZk19jxhOmb4p673CLVvb656LyMxQ+uO0= gioui.org v0.10.1/go.mod h1:MZJZsdEPkTBzChdqeE8CiiQhreUQBj43qusDxQNDf7k= gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= +gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA= gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM= +github.com/go-text/typesetting v0.3.4 h1:YYurUOtEb9kGSOz4uE3k4OpBGsp1dDL8+fjCeaFamAU= github.com/go-text/typesetting v0.3.4/go.mod h1:4qZCQphq4KSgGTAeI0uMEkVbROgfah8BuyF5LRYr7XY= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= +golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0 h1:tMSqXTK+AQdW3LpCbfatHSRPHeW6+2WuxaVQuHftn80= golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8= +golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY= golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= diff --git a/scripts/check_core_boundaries.py b/scripts/check_core_boundaries.py new file mode 100644 index 0000000..e62f727 --- /dev/null +++ b/scripts/check_core_boundaries.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +"""Reject UI, Windows, SQLite, and app-layer imports from the core module.""" + +import json +import os +import pathlib +import subprocess +import sys + + +FORBIDDEN_IMPORT_PREFIXES = ( + "gioui.org", + "golang.org/x/sys/windows", + "github.com/mattn/go-sqlite3", + "modernc.org/sqlite", + "softbox.local/app-modern", + "softbox.local/app-win7", +) + + +def is_forbidden(import_path): + return any( + import_path == prefix or import_path.startswith(prefix + "/") + for prefix in FORBIDDEN_IMPORT_PREFIXES + ) + + +def decode_json_stream(text): + decoder = json.JSONDecoder() + index = 0 + while index < len(text): + while index < len(text) and text[index].isspace(): + index += 1 + if index >= len(text): + return + value, index = decoder.raw_decode(text, index) + yield value + + +def list_core_packages(repo_root): + environment = os.environ.copy() + environment["GOWORK"] = "off" + result = subprocess.run( + ["go", "list", "-json", "./..."], + cwd=str(repo_root / "core"), + env=environment, + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + if result.returncode != 0: + print(result.stderr, file=sys.stderr, end="") + raise SystemExit(result.returncode) + return list(decode_json_stream(result.stdout)) + + +def run_self_test(): + cases = ( + ("gioui.org/layout", True), + ("golang.org/x/sys/windows/registry", True), + ("modernc.org/sqlite/lib", True), + ("softbox.local/app-modern/ui/gio", True), + ("context", False), + ("crypto/sha256", False), + ) + failures = [ + import_path + for import_path, expected in cases + if is_forbidden(import_path) != expected + ] + if failures: + print( + "ERROR: boundary matcher self-test failed for {}".format( + ", ".join(failures) + ), + file=sys.stderr, + ) + raise SystemExit(1) + print("core boundary matcher self-test passed.") + + +def main(): + if sys.argv[1:] == ["--self-test"]: + run_self_test() + return + if len(sys.argv) != 1: + print( + "usage: python scripts/check_core_boundaries.py [--self-test]", + file=sys.stderr, + ) + raise SystemExit(2) + + repo_root = pathlib.Path(__file__).resolve().parents[1] + violations = [] + + for package in list_core_packages(repo_root): + imports = set(package.get("Imports", [])) + imports.update(package.get("TestImports", [])) + imports.update(package.get("XTestImports", [])) + for import_path in sorted(imports): + if is_forbidden(import_path): + violations.append((package["ImportPath"], import_path)) + + if violations: + for package, import_path in violations: + print( + "ERROR: {} imports forbidden dependency {}".format( + package, import_path + ), + file=sys.stderr, + ) + raise SystemExit(1) + + print( + "core boundary check passed: {} forbidden prefixes absent.".format( + len(FORBIDDEN_IMPORT_PREFIXES) + ) + ) + + +if __name__ == "__main__": + main()