ความคืบหน้างาน ไฟล์ต้นฉบับ

tracker/mockup_catalog.go

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
}