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

tracker/usecase_approval.go

package main

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"net/http"
	"os"
	"strings"
	"time"
)

type DiagramApproval struct {
	Reviewer    string `json:"reviewer"`
	At          string `json:"at"`
	Note        string `json:"note"`
	Revision    int    `json:"revision"`
	SourceHash  string `json:"source_hash"`
	DiagramHash string `json:"diagram_hash"`
}
type UseCaseApprovalStatus struct {
	Approved   bool              `json:"approved"`
	State      string            `json:"state"`
	Revision   int               `json:"revision"`
	SourceHash string            `json:"source_hash"`
	Latest     *DiagramApproval  `json:"latest,omitempty"`
	History    []DiagramApproval `json:"history"`
	Error      string            `json:"error,omitempty"`
}

func digestJSON(v any) string {
	b, _ := json.Marshal(v)
	sum := sha256.Sum256(b)
	return hex.EncodeToString(sum[:])
}
func sourceDigest(u UseCase) string {
	return digestJSON(struct {
		ID     string
		Source UseCase
	}{u.ID, u})
}
func approvalForDocument(u UseCase, doc diagramDocument) UseCaseApprovalStatus {
	s := UseCaseApprovalStatus{State: "pending", Revision: doc.Revision, SourceHash: sourceDigest(u), History: doc.Approvals}
	if s.History == nil {
		s.History = []DiagramApproval{}
	}
	if len(doc.Approvals) > 0 {
		latest := doc.Approvals[len(doc.Approvals)-1]
		s.Latest = &latest
		s.State = "stale"
		if latest.Revision == doc.Revision && latest.SourceHash == s.SourceHash && latest.DiagramHash == digestJSON(doc.Diagram) {
			s.Approved = true
			s.State = "approved"
		}
	}
	return s
}
func (ds *diagramStore) approvalStatus(u UseCase) UseCaseApprovalStatus {
	ds.mu.Lock()
	defer ds.mu.Unlock()
	doc, err := ds.readDocument(u.ID)
	if errors.Is(err, os.ErrNotExist) {
		doc.Diagram = draftDiagram(u)
		err = nil
	}
	s := approvalForDocument(u, doc)
	if err != nil {
		s.Approved = false
		s.State = "unavailable"
		s.Error = "Unable to read approval record"
	}
	return s
}
func (ds *diagramStore) approve(u UseCase, expected int, sourceHash, reviewer, note string, confirmed bool) (UseCaseApprovalStatus, error) {
	ds.mu.Lock()
	defer ds.mu.Unlock()
	reviewer = strings.TrimSpace(reviewer)
	note = strings.TrimSpace(note)
	if !confirmed || reviewer == "" || len(reviewer) > 200 || len(note) > 4000 {
		return UseCaseApprovalStatus{}, errors.New("reviewer and explicit confirmation required")
	}
	doc, err := ds.readDocument(u.ID)
	if errors.Is(err, os.ErrNotExist) {
		doc.Diagram = draftDiagram(u)
		err = nil
	}
	if err != nil {
		return UseCaseApprovalStatus{}, err
	}
	if doc.Revision != expected || sourceHash != sourceDigest(u) {
		return UseCaseApprovalStatus{}, errDiagramConflict
	}
	doc.Approvals = append(doc.Approvals, DiagramApproval{Reviewer: reviewer, At: time.Now().In(ict).Format(time.RFC3339), Note: note, Revision: doc.Revision, SourceHash: sourceHash, DiagramHash: digestJSON(doc.Diagram)})
	if err = ds.writeDocument(u.ID, doc); err != nil {
		return UseCaseApprovalStatus{}, err
	}
	return approvalForDocument(u, doc), nil
}
func registerUseCaseApproval(mux *http.ServeMux, ds *diagramStore) {
	mux.HandleFunc("GET /api/use-cases/{uid}/approval", func(w http.ResponseWriter, r *http.Request) {
		u, ok := useCaseForRequest(w, r)
		if !ok {
			return
		}
		s := ds.approvalStatus(u)
		if s.Error != "" {
			http.Error(w, s.Error, 500)
			return
		}
		diagramResponse(w, s, nil)
	})
	mux.HandleFunc("POST /api/use-cases/{uid}/approval", func(w http.ResponseWriter, r *http.Request) {
		if !sameOrigin(r) {
			http.Error(w, "Forbidden", 403)
			return
		}
		u, ok := useCaseForRequest(w, r)
		if !ok {
			return
		}
		var sub struct {
			Revision   int    `json:"revision"`
			SourceHash string `json:"source_hash"`
			Reviewer   string `json:"reviewer"`
			Note       string `json:"note"`
			Confirmed  bool   `json:"confirmed"`
		}
		if !decodeDiagramRequest(w, r, &sub) {
			return
		}
		if sub.Revision < 0 || strings.TrimSpace(sub.Reviewer) == "" || len(sub.Reviewer) > 200 || len(sub.Note) > 4000 || !sub.Confirmed {
			http.Error(w, "Reviewer name and confirmation are required", 400)
			return
		}
		s, err := ds.approve(u, sub.Revision, sub.SourceHash, sub.Reviewer, sub.Note, sub.Confirmed)
		diagramResponse(w, s, err)
	})
}