diff --git a/.task/feature-architecture-baseline.md b/.task/feature-architecture-baseline.md index 7911793a..a091dad2 100644 --- a/.task/feature-architecture-baseline.md +++ b/.task/feature-architecture-baseline.md @@ -6,11 +6,14 @@ - 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= 5266d81c0d18500cc77d94b346d6ec1b424df260 - 工作区:/home/weichen/projects/Agent-System-Dev-wt-feature-architecture-baseline(worktree);分支:feature/architecture-baseline -- 授权与交付终点:<本次允许的动作与终点;未授权事项写明> +- 授权与交付终点:在 Dev 增加分层棘轮,在 ui 增加确认等待、差异和任务色令牌,在 frontend 与 ui 增加 knip 死导出棘轮,并改 DESIGN.md 与两份 AGENTS.md。允许 frontend、ui、Dev 各建 `feature/architecture-baseline`、推送、建 PR、合入。终点是这些 PR 已合入,然后在本 worktree 运行 `make task-finish`。未授权:改三个库、改 Core、发版、改写历史、变更分支保护。 +- 启动自检(2026-10-03T15:57Z):`make precheck` verdict READY。head=5266d81c0d,origin/main 同步,树干净,linked worktrees=0。必读 core/baseline.md、workflows/delivery.md §1、DESIGN.md 已读。 ## 目标与非目标 -<预期结果、受影响入口/模块/契约;明确不做什么。> +- 目标:backend internal 与前端目录的依赖边只许减少或保持;新增边失败,消失的边告警。frontend 与 ui 的未使用导出数量不得增加。等你确认、差异增删、空闲与定时任务各有令牌。提示气泡、快捷键和文本膨胀写进 DESIGN.md。异步状态只有一个所有者。 +- 受影响:Dev 的 policy、scripts、Makefile、DESIGN.md、AGENTS.md;ui 的 tokens.css;frontend 的 AGENTS.md。 +- 非目标:不改对话运行态、权限、上下文压缩;不改三个库。 ## 同步记录 diff --git a/AGENTS.md b/AGENTS.md index e2badaf4..72972bfd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,6 +11,7 @@ `@ui/motion.css` 的 `mk-*` 工具类;折叠一律用 `@ui` Collapsible 原语,禁止 `
`。 - 守护测试(UI 仓与 Frontend 仓各一份 `design-guard.test.ts`)以棘轮拦截新增裸色值、 私有 `@keyframes` 与行内字号;改风格 = `DESIGN.md` + 令牌文件同 PR 成对修改。 +- 异步状态只有一个所有者。事件按 seq 处理,过期事件丢弃。 ## 任务启动门禁(强制) diff --git a/DESIGN.md b/DESIGN.md index 50f9ade2..480f5280 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -54,7 +54,11 @@ Agent-System-Dev 是桌面优先、Web 兼容的 AI 工作台。界面应**冷 - 主按钮:单色系反色——`--primary`(深 `#ffffff` / 浅 `#0d0d0d`)配 `--primary-foreground` (反向黑/白)。品牌色保持克制。 - 语义:`--ok #46bf72`、`--warn #ff8a30`、`--err #ff5c5c`(浅色 `#1e8a3e/#e07b00/#e03131`)。 - 语义色只表语义;确认等待类统一绿色语言,不用红/橙装饰。 + 语义色只表语义。 +- 等你确认用 `--wait`(深 `#8b93ff` / 浅 `#3a46c8` / 墨绿 `#3d5a9a`)和 `--wait-soft`。 + 不借用成功绿,也不用红或橙装饰等待。 +- 差异视图:增加用 `--diff-add` / `--diff-add-text`,删除用 `--diff-remove` / `--diff-remove-text`。 +- 空闲任务用 `--task-idle`,定时任务用 `--task-scheduled`。两者不要混成同一种灰。 ### 2.5 墨绿主题(可选,`data-theme='molv'`) @@ -83,6 +87,7 @@ Agent-System-Dev 是桌面优先、Web 兼容的 AI 工作台。界面应**冷 | `--fs-ui-caption` | −1px | 13 | 紧凑辅助文案 | | `--fs-ui-sm` | −2px | 12 | 次要信息、行内代码 | | `--fs-ui-xs` | −4px | 10 | 徽标、快捷键、极弱元数据 | +| `--fs-ui-tooltip` | 同 `--fs-ui-sm` | 12 | 提示气泡。不另起字号 | 规则:UI 组件一律用标尺令牌;禁止行内 `font-size` 数值与 Tailwind 式任意字号 (守护测试棘轮拦截)。用户调字号只改 `--ui-font-size`,不改根字号;图标、间距、圆角不随字号缩放。 @@ -208,7 +213,15 @@ Primary(`--primary` 反色)、Ghost(透明,hover 出表面)、Danger - 普通用户界面只暴露任务与对话;插件/代理/治理类能力归系统管理区(管理员可见)。 - 右栏收起时功能零缺失(D31:弹层不承载唯一步骤,每个操作另有对话内路径)。 - 中文文案:动宾结构、不带句点结尾;快捷键用 `⌘`/kbd 标注。 -- 布局容忍长文本与翻译膨胀,不依赖截断作为唯一生存手段。 +- 布局按 `--text-expansion`(1.3)给按钮和标签留膨胀空间。不要把宽度定死成当前中文的字数。截断不是唯一生存手段。 + +### 10.1 快捷键 + +| 动作 | 按键 | +|---|---| +| 新建任务 | ⌘N / Ctrl+N | +| 折叠或展开左栏 | ⌘B / Ctrl+B | +| 打开第 n 个工作区 | ⌘1–9 / Ctrl+1–9 | ## 11. 禁止清单(守护测试覆盖项) diff --git a/Makefile b/Makefile index 63f36276..86409aae 100644 --- a/Makefile +++ b/Makefile @@ -88,10 +88,10 @@ eval-deliverables: cd $(BACKEND) && go test ./eval/ ./internal/quality/ -count=1 test-frontend: contracts - cd $(FRONTEND) && npm ci --no-fund --no-audit && npx vitest run + cd $(FRONTEND) && npm ci --no-fund --no-audit && npx vitest run && node scripts/check-knip.mjs test-ui: contracts - cd $(UI) && npm ci --no-fund --no-audit && npx tsc --noEmit && npx vitest run + cd $(UI) && npm ci --no-fund --no-audit && npx tsc --noEmit && npx vitest run && node scripts/check-knip.mjs test-agentcmd: cd deps/agent-command && go vet ./... && go test -count=1 ./... diff --git a/modules/frontend b/modules/frontend index 8be98b4c..d5f59f18 160000 --- a/modules/frontend +++ b/modules/frontend @@ -1 +1 @@ -Subproject commit 8be98b4cbe2a272541d41107a82e902eb7ccace6 +Subproject commit d5f59f1811268d099a6c354a29542809e640d4fe diff --git a/modules/ui b/modules/ui index b4a4fc1b..9c034575 160000 --- a/modules/ui +++ b/modules/ui @@ -1 +1 @@ -Subproject commit b4a4fc1b5f4d9aaefe142d44aae015cc73f97ab6 +Subproject commit 9c0345750600bd14f1b3144bb37819df97830fc1 diff --git a/policy/architecture-baseline.json b/policy/architecture-baseline.json new file mode 100644 index 00000000..d9147566 --- /dev/null +++ b/policy/architecture-baseline.json @@ -0,0 +1,69 @@ +[ + "backend/app backend/bus", + "backend/app backend/cardsvc", + "backend/app backend/cmdbus", + "backend/app backend/hostapi", + "backend/app backend/intentgate", + "backend/app backend/pgbus", + "backend/app backend/plugins", + "backend/app backend/profiles", + "backend/app backend/prompts", + "backend/app backend/rag", + "backend/app backend/ratelimit", + "backend/app backend/runmgr", + "backend/app backend/runtime", + "backend/app backend/server", + "backend/app backend/settings", + "backend/app backend/skills", + "backend/app backend/squadadapter", + "backend/app backend/store", + "backend/app backend/tools", + "backend/app backend/workflow", + "backend/cardsvc backend/store", + "backend/hostapi backend/store", + "backend/pgbus backend/bus", + "backend/pgbus backend/store", + "backend/plugins backend/settings", + "backend/plugins backend/store", + "backend/plugins backend/tools", + "backend/plugins backend/workflow", + "backend/profiles backend/store", + "backend/prompts backend/store", + "backend/rag backend/settings", + "backend/rag backend/store", + "backend/runmgr backend/bus", + "backend/runmgr backend/cardsvc", + "backend/runmgr backend/ingest", + "backend/runmgr backend/profiles", + "backend/runmgr backend/runtime", + "backend/runmgr backend/settings", + "backend/runmgr backend/store", + "backend/runmgr backend/tools", + "backend/runmgr backend/workflow", + "backend/runtime backend/ctxasm", + "backend/runtime backend/docgen", + "backend/runtime backend/prompts", + "backend/runtime backend/quality", + "backend/runtime backend/squadadapter", + "backend/runtime backend/tools", + "backend/runtime backend/workflow", + "backend/server backend/cardsvc", + "backend/server backend/cmdbus", + "backend/server backend/ingest", + "backend/server backend/intentgate", + "backend/server backend/plugins", + "backend/server backend/profiles", + "backend/server backend/prompts", + "backend/server backend/ratelimit", + "backend/server backend/runmgr", + "backend/server backend/settings", + "backend/server backend/store", + "backend/server backend/tools", + "backend/server backend/workflow", + "backend/settings backend/store", + "backend/squadadapter backend/quality", + "backend/store backend/blobstore", + "backend/tools backend/quality", + "frontend/app frontend/components", + "frontend/components frontend/app" +] diff --git a/policy/architecture.yaml b/policy/architecture.yaml new file mode 100644 index 00000000..af5b48fa --- /dev/null +++ b/policy/architecture.yaml @@ -0,0 +1,12 @@ +# 分层棘轮。新增跨组依赖边失败,消失的边只告警。 +# 基线见 architecture-baseline.json。改分层先改本文件,再更新基线。 +backend: + root: modules/backend/internal + import_prefix: agent-system-backend/internal/ +frontend: + root: modules/frontend/src + groups: + - name: app + dir: . + - name: components + dir: components diff --git a/scripts/lint-architecture.go b/scripts/lint-architecture.go new file mode 100644 index 00000000..9ded0e9c --- /dev/null +++ b/scripts/lint-architecture.go @@ -0,0 +1,270 @@ +// 分层依赖棘轮:backend internal 包与前端目录组之间的导入边。 +// 相对基线多出来的边失败;基线里已经消失的边只告警。 +package main + +import ( + "bufio" + "bytes" + "encoding/json" + "flag" + "fmt" + "go/parser" + "go/token" + "os" + "path/filepath" + "sort" + "strings" +) + +func main() { + root := flag.String("root", ".", "repository root") + baseline := flag.String("baseline", "policy/architecture-baseline.json", "recorded edges") + write := flag.Bool("write", false, "rewrite the baseline from the current tree") + inject := flag.String("inject", "", "extra edge from->to, used to prove a new edge fails") + flag.Parse() + + if err := checkPolicy(*root); err != nil { + fmt.Fprintf(os.Stderr, "architecture: %v\n", err) + os.Exit(1) + } + edges, err := collect(*root) + if err != nil { + fmt.Fprintf(os.Stderr, "architecture: %v\n", err) + os.Exit(1) + } + if *inject != "" { + parts := strings.Split(*inject, "->") + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + fmt.Fprintln(os.Stderr, "architecture: -inject must be from->to") + os.Exit(2) + } + edges = append(edges, parts[0]+" "+parts[1]) + } + sort.Strings(edges) + edges = unique(edges) + if *write { + raw, err := json.MarshalIndent(edges, "", " ") + if err != nil { + fmt.Fprintf(os.Stderr, "architecture: %v\n", err) + os.Exit(1) + } + raw = append(raw, '\n') + if err := os.WriteFile(*baseline, raw, 0o644); err != nil { + fmt.Fprintf(os.Stderr, "architecture: %v\n", err) + os.Exit(1) + } + fmt.Printf("wrote %d edges\n", len(edges)) + return + } + recorded, err := readBaseline(*baseline) + if err != nil { + fmt.Fprintf(os.Stderr, "architecture: %v\n", err) + os.Exit(1) + } + observed := map[string]bool{} + for _, e := range edges { + observed[e] = true + } + var fresh, stale []string + for _, e := range edges { + if !recorded[e] { + fresh = append(fresh, e) + } + } + for e := range recorded { + if !observed[e] { + stale = append(stale, e) + } + } + sort.Strings(stale) + if len(stale) > 0 { + fmt.Fprintln(os.Stderr, "warn: architecture edges no longer present; delete them from the baseline") + for _, e := range stale { + fmt.Fprintln(os.Stderr, " ", e) + } + } + if len(fresh) > 0 { + fmt.Fprintln(os.Stderr, "architecture: new dependency edges") + for _, e := range fresh { + fmt.Fprintln(os.Stderr, " ", e) + } + os.Exit(1) + } + fmt.Printf("architecture ok (%d edges)\n", len(edges)) +} + +func readBaseline(path string) (map[string]bool, error) { + raw, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var list []string + if err := json.Unmarshal(raw, &list); err != nil { + return nil, err + } + out := map[string]bool{} + for _, e := range list { + out[e] = true + } + return out, nil +} + +func checkPolicy(root string) error { + raw, err := os.ReadFile(filepath.Join(root, "policy/architecture.yaml")) + if err != nil { + return err + } + text := string(raw) + for _, need := range []string{"modules/backend/internal", "modules/frontend/src", "components"} { + if !strings.Contains(text, need) { + return fmt.Errorf("policy/architecture.yaml missing %s", need) + } + } + return nil +} + +func collect(root string) ([]string, error) { + var edges []string + be, err := backendEdges(filepath.Join(root, "modules/backend/internal")) + if err != nil { + return nil, err + } + edges = append(edges, be...) + fe, err := frontendEdges(filepath.Join(root, "modules/frontend/src")) + if err != nil { + return nil, err + } + edges = append(edges, fe...) + return edges, nil +} + +func backendEdges(dir string) ([]string, error) { + var edges []string + err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { + if err != nil || d.IsDir() { + return nil + } + if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") { + return nil + } + from := backendNode(dir, path) + if from == "" { + return nil + } + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, path, nil, parser.ImportsOnly) + if err != nil { + return err + } + for _, imp := range f.Imports { + p := strings.Trim(imp.Path.Value, `"`) + const prefix = "agent-system-backend/internal/" + if !strings.HasPrefix(p, prefix) { + continue + } + rest := strings.TrimPrefix(p, prefix) + seg := strings.Split(rest, "/")[0] + to := "backend/" + seg + if to == from { + continue + } + edges = append(edges, from+" "+to) + } + return nil + }) + return edges, err +} + +func backendNode(root, path string) string { + rel, err := filepath.Rel(root, path) + if err != nil { + return "" + } + seg := strings.Split(filepath.ToSlash(rel), "/")[0] + if seg == "" || seg == "." { + return "" + } + return "backend/" + seg +} + +func frontendEdges(src string) ([]string, error) { + var edges []string + err := filepath.WalkDir(src, func(path string, d os.DirEntry, err error) error { + if err != nil || d.IsDir() { + return nil + } + if !strings.HasSuffix(path, ".ts") && !strings.HasSuffix(path, ".tsx") { + return nil + } + if strings.HasSuffix(path, ".d.ts") { + return nil + } + from := frontendNode(src, path) + text, err := os.ReadFile(path) + if err != nil { + return err + } + for _, spec := range importSpecs(text) { + if !strings.HasPrefix(spec, ".") { + continue + } + target := filepath.Clean(filepath.Join(filepath.Dir(path), spec)) + if !strings.HasPrefix(target, src) { + continue + } + to := frontendNode(src, target) + if to == "" || to == from { + continue + } + edges = append(edges, from+" "+to) + } + return nil + }) + return edges, err +} + +func frontendNode(src, path string) string { + rel, err := filepath.Rel(src, path) + if err != nil { + return "" + } + rel = filepath.ToSlash(rel) + if strings.HasPrefix(rel, "components/") || rel == "components" { + return "frontend/components" + } + return "frontend/app" +} + +func importSpecs(src []byte) []string { + var specs []string + sc := bufio.NewScanner(bytes.NewReader(src)) + for sc.Scan() { + line := sc.Text() + for _, marker := range []string{"from '", `from "`, "import('", `import("`} { + i := strings.Index(line, marker) + if i < 0 { + continue + } + rest := line[i+len(marker):] + quote := marker[len(marker)-1] + end := strings.IndexByte(rest, quote) + if end > 0 { + specs = append(specs, rest[:end]) + } + } + } + return specs +} + +func unique(in []string) []string { + if len(in) == 0 { + return in + } + out := in[:1] + for _, s := range in[1:] { + if s != out[len(out)-1] { + out = append(out, s) + } + } + return out +} diff --git a/scripts/lint-architecture.sh b/scripts/lint-architecture.sh new file mode 100755 index 00000000..7451dd8e --- /dev/null +++ b/scripts/lint-architecture.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# 分层棘轮:新增依赖边失败,消失的边只告警。 +# 负例:注入一条基线里没有的边,必须失败。 +set -euo pipefail +root=$(cd "$(dirname "$0")/.." && pwd) +base="$root/policy/architecture-baseline.json" +tool=(go run "$root/scripts/lint-architecture.go" -root "$root" -baseline "$base") + +( cd "$root/scripts" && "${tool[@]}" ) + +set +e +( cd "$root/scripts" && "${tool[@]}" -inject 'backend/store->backend/__architecture_probe__' ) >/tmp/architecture-probe.out 2>&1 +code=$? +set -e +if [ "$code" -eq 0 ]; then + echo "architecture: injected edge was accepted" >&2 + cat /tmp/architecture-probe.out >&2 + exit 1 +fi +if ! grep -q '__architecture_probe__' /tmp/architecture-probe.out; then + echo "architecture: injected edge did not fail for the expected reason" >&2 + cat /tmp/architecture-probe.out >&2 + exit 1 +fi +rm -f /tmp/architecture-probe.out +echo "architecture negative ok" diff --git a/scripts/lint-boundaries.sh b/scripts/lint-boundaries.sh index 2827c982..e39304eb 100755 --- a/scripts/lint-boundaries.sh +++ b/scripts/lint-boundaries.sh @@ -96,4 +96,5 @@ done < <(find "$root/deps/agent-native" "$root/deps/agent-squad" "$root/modules/ if [ "$fail" -ne 0 ]; then exit 1 fi +bash "$root/scripts/lint-architecture.sh" echo "boundaries ok"