Why: Replace repeated submodule source pointers with consumer-owned language versions and fixed application sources, keeping independent repository edits and existing branches intact. Evidence: SELF task receipts contain startup/sync evidence, real Go and npm consumption, Native overlay/unlink, safe transition/ref preservation, credential boundary and historical build regressions. Exact committed full composition and RDM checks follow this checkpoint; formal acceptance and GATE CI remain pending. Impact: Add isolated build/test composition, source provenance and safe owner refresh, adapt governance and service build callers, remove replaced index links while retaining original Git metadata, and adopt reusable dependency standards. Refs: Documents approved dependency-workspace r1/R1-R15 and U1-U7 implementation plan; feature/dependency-workspace task card.
315 lines
7.7 KiB
Go
315 lines
7.7 KiB
Go
// 分层依赖棘轮:backend internal 包与前端目录组之间的导入边。
|
|
// 相对基线多出来的边失败;基线里已经消失的边只告警。
|
|
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"go/parser"
|
|
"go/token"
|
|
"os"
|
|
"os/exec"
|
|
"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)
|
|
backend, frontend := "modules/backend/internal", "modules/frontend/src"
|
|
if _, err := os.Stat(filepath.Join(root, "components.json")); err == nil {
|
|
backend, frontend = ".workspace/Agent-System-Backend/internal", ".workspace/Agent-System-Frontend/src"
|
|
}
|
|
for _, need := range []string{backend, frontend, "components"} {
|
|
if !strings.Contains(text, need) {
|
|
return fmt.Errorf("policy/architecture.yaml missing %s", need)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func componentRoot(root, id, legacy string) (string, error) {
|
|
if os.Getenv("COMPOSITION_SOURCE_MAP") != "" {
|
|
output, err := exec.Command("python3", filepath.Join(root, "scripts/component-source.py"), id).CombinedOutput()
|
|
if err != nil {
|
|
return "", fmt.Errorf("isolated component lookup failed: %s", strings.TrimSpace(string(output)))
|
|
}
|
|
return strings.TrimSpace(string(output)), nil
|
|
}
|
|
if _, err := os.Stat(filepath.Join(root, "components.json")); err == nil {
|
|
return filepath.Join(root, ".workspace", id), nil
|
|
}
|
|
return filepath.Join(root, legacy), nil
|
|
}
|
|
|
|
func collect(root string) ([]string, error) {
|
|
var edges []string
|
|
backend, err := componentRoot(root, "Agent-System-Backend", "modules/backend")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
frontend, err := componentRoot(root, "Agent-System-Frontend", "modules/frontend")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
be, err := backendEdges(filepath.Join(backend, "internal"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
edges = append(edges, be...)
|
|
fe, err := frontendEdges(filepath.Join(frontend, "src"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
edges = append(edges, fe...)
|
|
return edges, nil
|
|
}
|
|
|
|
func backendEdges(dir string) ([]string, error) {
|
|
if info, err := os.Stat(dir); err != nil || !info.IsDir() {
|
|
return nil, fmt.Errorf("backend source directory is unavailable: %s", dir)
|
|
}
|
|
raw, err := os.ReadFile(filepath.Join(filepath.Dir(dir), "go.mod"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
prefix := ""
|
|
for _, line := range strings.Split(string(raw), "\n") {
|
|
fields := strings.Fields(line)
|
|
if len(fields) >= 2 && fields[0] == "module" {
|
|
prefix = fields[1] + "/internal/"
|
|
break
|
|
}
|
|
}
|
|
if prefix == "" {
|
|
return nil, fmt.Errorf("backend module identity is unavailable")
|
|
}
|
|
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, `"`)
|
|
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
|
|
}
|