Add an architecture edge ratchet and waiting-state tokens

Why: New cross-layer imports and unused exports could land unnoticed, and confirm-waiting reused the success color.
Evidence: bash scripts/lint-boundaries.sh passed, including a probe that a new edge fails. Both design-guard tests passed. Knip counts stay at the baselines.
Impact: make test-boundaries now checks architecture edges. Frontend and ui test targets also run the knip ratchet.
Refs: feature/architecture-baseline
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-03 16:02:57 +00:00
co-authored by Cursor
parent 17565aab82
commit 937152a563
11 changed files with 403 additions and 8 deletions
+270
View File
@@ -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
}
+26
View File
@@ -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"
+1
View File
@@ -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"