package main import ( "encoding/json" "errors" "fmt" "os" "path/filepath" "sort" "strings" ) type MockVariant struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` Preview string `json:"preview"` } type MockScreen struct { ID string `json:"id"` Title string `json:"title"` Module string `json:"module"` ModuleName string `json:"module_name"` Modules []string `json:"modules"` Channel string `json:"channel"` Kind string `json:"kind"` Status string `json:"status"` Description string `json:"description"` Route string `json:"route_proposal"` UseCases []string `json:"use_cases"` Components []string `json:"components"` Provenance map[string]string `json:"provenance"` Variants []MockVariant `json:"variants"` Manifest string `json:"manifest"` } type MockCatalog struct { ModuleNames map[string]string `json:"module_names"` Screens []MockScreen `json:"screens"` ByID map[string]MockScreen `json:"-"` Assets map[string]bool `json:"-"` } func (s MockScreen) Preview() string { if len(s.Variants) == 0 { return "" } return "/mockup-assets/" + s.Variants[0].Preview } func mockFile(root, path string) ([]byte, error) { if filepath.IsAbs(path) || strings.Contains(path, "\\") || strings.Contains(path, "\x00") { return nil, errors.New("invalid artifact path") } clean := filepath.Clean(path) if clean != path || strings.HasPrefix(clean, "..") { return nil, errors.New("invalid artifact path") } base, err := filepath.EvalSymlinks(root) if err != nil { return nil, err } full, err := filepath.EvalSymlinks(filepath.Join(base, path)) if err != nil { return nil, err } rel, err := filepath.Rel(base, full) if err != nil || strings.HasPrefix(rel, "..") { return nil, errors.New("artifact outside repository") } return os.ReadFile(full) } func loadMockCatalog(root string) (MockCatalog, error) { c := MockCatalog{ByID: map[string]MockScreen{}, Assets: map[string]bool{}} b, err := mockFile(root, "mockups/catalog.json") if err != nil { return c, err } var index struct { Paths []string `json:"screen_manifests"` Names map[string]string `json:"module_names"` } if err = json.Unmarshal(b, &index); err != nil { return c, err } c.ModuleNames = index.Names for _, path := range index.Paths { if !strings.HasPrefix(path, "mockups/") { return c, errors.New("invalid manifest") } b, err = mockFile(root, path) if err != nil { return c, err } var s MockScreen if err = json.Unmarshal(b, &s); err != nil { return c, err } if !mockID.MatchString(s.ID) || c.ByID[s.ID].ID != "" || s.Title == "" || len(s.Variants) == 0 { return c, errors.New("invalid screen manifest") } s.Manifest = path s.ModuleName = moduleLabel(c.ModuleNames, s.Module) seen := map[string]bool{} for _, v := range s.Variants { if !mockID.MatchString(v.ID) || seen[v.ID] { return c, errors.New("invalid screen variant") } seen[v.ID] = true if !strings.HasPrefix(v.Preview, "mockups/") && !strings.HasPrefix(v.Preview, "docs/agent/assets/ui/") { return c, errors.New("invalid preview path") } if ext := strings.ToLower(filepath.Ext(v.Preview)); ext != ".svg" && ext != ".png" && ext != ".jpg" && ext != ".webp" { return c, errors.New("invalid preview format") } c.Assets[v.Preview] = true } c.Screens = append(c.Screens, s) c.ByID[s.ID] = s } sort.Slice(c.Screens, func(i, j int) bool { return c.Screens[i].ID < c.Screens[j].ID }) return c, nil } func draftMockSitemap(root string, c MockCatalog) (MockGraph, error) { g := MockGraph{Direction: "TB", SourceHash: digestJSON(c.Screens), Nodes: []MockNode{}, Edges: []DiagramEdge{}} b, err := mockFile(root, "mockups/sitemap/graph.json") if err != nil { return g, err } var seed struct { Edges []DiagramEdge `json:"edges"` } if err = json.Unmarshal(b, &seed); err != nil { return g, err } g.Edges = seed.Edges g.SourceHash = digestJSON([]any{c.Screens, seed.Edges}) for i, s := range c.Screens { g.Nodes = append(g.Nodes, MockNode{ID: s.ID, Type: "screen", ScreenID: s.ID, Variant: "default", Title: s.Title, Description: s.Description, Mapping: "proposed", X: float64(80 + i%8*360), Y: float64(80 + i/8*380)}) } return g, validateMockGraph(g, c) } func draftMockFlow(root string, u UseCase, c MockCatalog) (MockGraph, error) { b, err := mockFile(root, "mockups/page-flows/"+u.ID+"/flow.json") if err != nil { return MockGraph{}, err } var seed struct { Mapping map[string]struct { ScreenID string `json:"screen_id"` Variant string `json:"variant"` Status string `json:"status"` } `json:"mapping"` } if err = json.Unmarshal(b, &seed); err != nil { return MockGraph{}, err } d := draftDiagram(u) g := MockGraph{Direction: "LR", SourceHash: digestJSON([]any{sourceDigest(u), c.Screens, seed.Mapping}), Nodes: []MockNode{}, Edges: d.Edges} for _, n := range d.Nodes { m := MockNode{ID: n.ID, Type: n.Type, Title: n.Title, Description: n.Description, StepID: n.ID, X: n.X, Y: n.Y} if mapping, ok := seed.Mapping[n.ID]; ok { m.Type = "screen" m.ScreenID = mapping.ScreenID m.Variant = mapping.Variant m.Mapping = mapping.Status } g.Nodes = append(g.Nodes, m) } if err = validateMockGraph(g, c); err != nil { return g, fmt.Errorf("%s: %w", u.ID, err) } return g, nil }