Files
Agent-System/internal/engineeringdashboard/model.go
T
weichenandCursor e46ec0e4e7 feat(engineering-dashboard): U1 freeze snapshot and facet model
Define versioned Snapshot/Fact/Conflict/TaskState contracts with
independent plan/git/verification/deployment axes and acceptance fixtures.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 06:59:02 +00:00

500 lines
18 KiB
Go

// Package engineeringdashboard implements an independent local read-only
// personal engineering control center. It must not import product business packages.
package engineeringdashboard
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"sort"
"strings"
"time"
)
const SchemaVersion = "1"
// Provenance classifies how a fact was obtained.
type Provenance string
const (
ProvenanceDeclared Provenance = "declared"
ProvenanceObserved Provenance = "observed"
ProvenanceVerified Provenance = "verified"
ProvenanceInferred Provenance = "inferred"
)
// Freshness classifies source/data age relative to the current collection.
type Freshness string
const (
FreshnessFresh Freshness = "fresh"
FreshnessStale Freshness = "stale"
FreshnessError Freshness = "error"
FreshnessUnknown Freshness = "unknown"
)
// SourceStatus is the collection outcome for one source.
type SourceStatus string
const (
SourceOK SourceStatus = "ok"
SourceDegraded SourceStatus = "degraded"
SourceError SourceStatus = "error"
SourceUnknown SourceStatus = "unknown"
)
// AgentKind separates Herdr engineering agents from the product PI Agent Runner.
type AgentKind string
const (
AgentKindHerdrEngineering AgentKind = "herdr_engineering"
AgentKindProductPIRunner AgentKind = "product_pi_agent_runner"
AgentKindHuman AgentKind = "human"
AgentKindUnknown AgentKind = "unknown"
)
// DocStatus is the task document declared status (plan axis only).
type DocStatus string
const (
DocStatusPlanned DocStatus = "planned"
DocStatusInProgress DocStatus = "in_progress"
DocStatusBlocked DocStatus = "blocked"
DocStatusDone DocStatus = "done"
DocStatusCancelled DocStatus = "cancelled"
DocStatusUnknown DocStatus = "unknown"
)
// ConflictKind classifies multi-source disagreements.
type ConflictKind string
const (
ConflictDeclaredVsObserved ConflictKind = "declared_vs_observed"
ConflictTaskVsGit ConflictKind = "task_vs_git"
ConflictTestVsSubject ConflictKind = "test_vs_subject"
ConflictReceiptVsRuntime ConflictKind = "receipt_vs_runtime"
ConflictStaleSource ConflictKind = "stale_source"
ConflictMissingEvidence ConflictKind = "missing_evidence"
)
// Snapshot is the immutable delivery model shared by HTML, JSON, and export.
type Snapshot struct {
SchemaVersion string `json:"schema_version"`
GenerationID string `json:"generation_id"`
RevisionID string `json:"revision_id"`
GeneratedAt time.Time `json:"generated_at"`
CollectionDuration time.Duration `json:"-"`
Repository RepositoryIdentity `json:"repository"`
Sources []SourceState `json:"sources"`
Facts []Fact `json:"facts"`
Conflicts []Conflict `json:"conflicts"`
Documents []DocumentNode `json:"documents"`
Modules []ModuleView `json:"modules"`
Tasks []TaskState `json:"tasks"`
Git []GitBranchView `json:"git"`
Environments []EnvironmentState `json:"environments"`
Warnings []string `json:"warnings,omitempty"`
}
// RepositoryIdentity identifies the scanned checkout.
type RepositoryIdentity struct {
Root string `json:"root"`
Branch string `json:"branch"`
Head string `json:"head"`
Dirty bool `json:"dirty"`
}
// SourceState records one collector's outcome.
type SourceState struct {
Name string `json:"name"`
Status SourceStatus `json:"status"`
Freshness Freshness `json:"freshness"`
ObservedAt time.Time `json:"observed_at"`
LastSuccessAt *time.Time `json:"last_success_at,omitempty"`
Error string `json:"error,omitempty"`
DurationMS int64 `json:"duration_ms"`
}
// Fact is an atomic, provenance-tagged observation or declaration.
type Fact struct {
ID string `json:"id"`
Subject string `json:"subject"`
Field string `json:"field"`
Value string `json:"value"`
Provenance Provenance `json:"provenance"`
SourceRef string `json:"source_ref"`
ObservedAt time.Time `json:"observed_at"`
Confidence string `json:"confidence,omitempty"`
}
// Conflict retains all disagreeing fact refs without picking a winner.
type Conflict struct {
Subject string `json:"subject"`
Field string `json:"field"`
Kind ConflictKind `json:"kind"`
FactRefs []string `json:"fact_refs"`
Summary string `json:"summary"`
}
// DocumentNode is an indexed markdown document (body may be truncated/omitted).
type DocumentNode struct {
ID string `json:"id"`
Path string `json:"path"`
Title string `json:"title"`
Taxonomy string `json:"taxonomy,omitempty"`
Status string `json:"status,omitempty"`
FrontMatter map[string]string `json:"front_matter,omitempty"`
Body string `json:"body,omitempty"`
BodyTruncated bool `json:"body_truncated,omitempty"`
LinksOut []string `json:"links_out,omitempty"`
LinksIn []string `json:"links_in,omitempty"`
BrokenLinks []string `json:"broken_links,omitempty"`
Orphan bool `json:"orphan,omitempty"`
Unindexed bool `json:"unindexed,omitempty"`
ParseWarnings []string `json:"parse_warnings,omitempty"`
}
// ModuleView aggregates architecture/module matrix evidence.
type ModuleView struct {
Name string `json:"name"`
Status string `json:"status,omitempty"`
Mutex string `json:"mutex,omitempty"`
Evidence []string `json:"evidence,omitempty"`
Deps []string `json:"deps,omitempty"`
}
// GitFacets are observed implementation facets (never inferred from doc_status).
type GitFacets struct {
Branch string `json:"branch,omitempty"`
Worktree string `json:"worktree,omitempty"`
MergedToDev *bool `json:"merged_to_dev,omitempty"`
Cleanliness string `json:"cleanliness,omitempty"` // clean|dirty|unknown
Ahead int `json:"ahead,omitempty"`
Behind int `json:"behind,omitempty"`
RemoteNote string `json:"remote_note,omitempty"`
}
// VerificationFacets bind test evidence to a subject identity.
type VerificationFacets struct {
Result string `json:"result,omitempty"` // pass|fail|unbound|stale|unknown
Freshness Freshness `json:"freshness,omitempty"`
SubjectID string `json:"subject_id,omitempty"`
ReportPath string `json:"report_path,omitempty"`
}
// DeploymentFacets are per-environment install observations.
type DeploymentFacets struct {
ContainerStatus string `json:"container_status,omitempty"`
AppReadiness string `json:"app_readiness,omitempty"`
ServicesOK string `json:"services_ok,omitempty"` // complete|partial|unknown
ArtifactID string `json:"artifact_id,omitempty"`
IdentityNote string `json:"identity_note,omitempty"`
}
// AgentClaim is declared ownership; activity remains separate.
type AgentClaim struct {
Owner string `json:"owner,omitempty"`
AgentKind AgentKind `json:"agent_kind"`
Claimed bool `json:"claimed"`
Mutex string `json:"mutex,omitempty"`
Branch string `json:"declared_branch,omitempty"`
Worktree string `json:"declared_worktree,omitempty"`
Activity string `json:"activity"` // unknown|observed|...
ActivityAt *time.Time `json:"activity_at,omitempty"`
Evidence string `json:"evidence,omitempty"`
}
// TaskState keeps plan / git / verification / deployment axes independent.
type TaskState struct {
ID string `json:"id"`
Title string `json:"title"`
Path string `json:"path"`
DocStatus DocStatus `json:"doc_status"`
Claim AgentClaim `json:"claim"`
Git GitFacets `json:"git"`
Verification VerificationFacets `json:"verification"`
Deployment map[string]DeploymentFacets `json:"deployment,omitempty"`
EvidenceChain []string `json:"evidence_chain,omitempty"`
}
// GitBranchView is a git observation row.
type GitBranchView struct {
Name string `json:"name"`
IsCurrent bool `json:"is_current,omitempty"`
Head string `json:"head,omitempty"`
Upstream string `json:"upstream,omitempty"`
Ahead int `json:"ahead,omitempty"`
Behind int `json:"behind,omitempty"`
Worktree string `json:"worktree,omitempty"`
Dirty bool `json:"dirty,omitempty"`
RemoteNote string `json:"remote_note,omitempty"`
ObservedAt time.Time `json:"observed_at"`
}
// EnvironmentState is one deploy environment's multi-signal view.
type EnvironmentState struct {
Name string `json:"name"`
Containers []ContainerView `json:"containers,omitempty"`
Readiness string `json:"readiness,omitempty"`
Schema string `json:"schema,omitempty"`
Artifact string `json:"artifact,omitempty"`
Receipt string `json:"receipt,omitempty"`
Facets DeploymentFacets `json:"facets"`
DriftNotes []string `json:"drift_notes,omitempty"`
}
// ContainerView is a whitelist-filtered docker observation.
type ContainerView struct {
Name string `json:"name"`
Image string `json:"image,omitempty"`
Digest string `json:"digest,omitempty"`
Status string `json:"status,omitempty"`
RestartCount int `json:"restart_count,omitempty"`
Health string `json:"health,omitempty"`
Project string `json:"compose_project,omitempty"`
Service string `json:"compose_service,omitempty"`
}
// ValidateProvenance rejects unknown provenance values.
func ValidateProvenance(p Provenance) error {
switch p {
case ProvenanceDeclared, ProvenanceObserved, ProvenanceVerified, ProvenanceInferred:
return nil
default:
return fmt.Errorf("unknown provenance %q", p)
}
}
// ValidateFreshness rejects unknown freshness values.
func ValidateFreshness(f Freshness) error {
switch f {
case FreshnessFresh, FreshnessStale, FreshnessError, FreshnessUnknown:
return nil
default:
return fmt.Errorf("unknown freshness %q", f)
}
}
// ValidateAgentKind rejects unknown agent kinds.
func ValidateAgentKind(k AgentKind) error {
switch k {
case AgentKindHerdrEngineering, AgentKindProductPIRunner, AgentKindHuman, AgentKindUnknown:
return nil
default:
return fmt.Errorf("unknown agent kind %q", k)
}
}
// ComputeRevisionID hashes schema + repo identity + normalized non-volatile payload.
// generated_at, observed_at, and collection duration are excluded.
func ComputeRevisionID(s Snapshot) string {
type revSource struct {
Name string `json:"name"`
Status SourceStatus `json:"status"`
Freshness Freshness `json:"freshness"`
Error string `json:"error,omitempty"`
}
type revFact struct {
ID string `json:"id"`
Subject string `json:"subject"`
Field string `json:"field"`
Value string `json:"value"`
Provenance Provenance `json:"provenance"`
SourceRef string `json:"source_ref"`
Confidence string `json:"confidence,omitempty"`
}
type revPayload struct {
Schema string `json:"schema_version"`
Repo RepositoryIdentity `json:"repository"`
Sources []revSource `json:"sources"`
Facts []revFact `json:"facts"`
Conflicts []Conflict `json:"conflicts"`
Documents []DocumentNode `json:"documents"`
Modules []ModuleView `json:"modules"`
Tasks []TaskState `json:"tasks"`
Git []GitBranchView `json:"git"`
Envs []EnvironmentState `json:"environments"`
Warnings []string `json:"warnings,omitempty"`
}
srcs := make([]revSource, 0, len(s.Sources))
for _, src := range s.Sources {
srcs = append(srcs, revSource{Name: src.Name, Status: src.Status, Freshness: src.Freshness, Error: src.Error})
}
sort.Slice(srcs, func(i, j int) bool { return srcs[i].Name < srcs[j].Name })
facts := make([]revFact, 0, len(s.Facts))
for _, f := range s.Facts {
facts = append(facts, revFact{
ID: f.ID, Subject: f.Subject, Field: f.Field, Value: f.Value,
Provenance: f.Provenance, SourceRef: f.SourceRef, Confidence: f.Confidence,
})
}
sort.Slice(facts, func(i, j int) bool { return facts[i].ID < facts[j].ID })
conflicts := append([]Conflict(nil), s.Conflicts...)
sort.Slice(conflicts, func(i, j int) bool {
a := conflicts[i].Subject + "|" + conflicts[i].Field + "|" + string(conflicts[i].Kind)
b := conflicts[j].Subject + "|" + conflicts[j].Field + "|" + string(conflicts[j].Kind)
return a < b
})
docs := append([]DocumentNode(nil), s.Documents...)
sort.Slice(docs, func(i, j int) bool { return docs[i].ID < docs[j].ID })
mods := append([]ModuleView(nil), s.Modules...)
sort.Slice(mods, func(i, j int) bool { return mods[i].Name < mods[j].Name })
tasks := append([]TaskState(nil), s.Tasks...)
sort.Slice(tasks, func(i, j int) bool { return tasks[i].ID < tasks[j].ID })
git := append([]GitBranchView(nil), s.Git...)
for i := range git {
git[i].ObservedAt = time.Time{}
}
sort.Slice(git, func(i, j int) bool { return git[i].Name < git[j].Name })
envs := append([]EnvironmentState(nil), s.Environments...)
sort.Slice(envs, func(i, j int) bool { return envs[i].Name < envs[j].Name })
warns := append([]string(nil), s.Warnings...)
sort.Strings(warns)
payload := revPayload{
Schema: s.SchemaVersion, Repo: s.Repository, Sources: srcs, Facts: facts,
Conflicts: conflicts, Documents: docs, Modules: mods, Tasks: tasks, Git: git, Envs: envs, Warnings: warns,
}
b, err := json.Marshal(payload)
if err != nil {
sum := sha256.Sum256([]byte(err.Error()))
return "rev_err_" + hex.EncodeToString(sum[:8])
}
sum := sha256.Sum256(b)
return "rev_" + hex.EncodeToString(sum[:16])
}
// MarshalSnapshotJSON encodes a snapshot for API/export.
func MarshalSnapshotJSON(s Snapshot) ([]byte, error) {
type alias Snapshot
type wire struct {
alias
CollectionDurationMS int64 `json:"collection_duration_ms"`
}
w := wire{alias: alias(s), CollectionDurationMS: s.CollectionDuration.Milliseconds()}
return json.MarshalIndent(w, "", " ")
}
// UnmarshalSnapshotJSON decodes a snapshot wire format.
func UnmarshalSnapshotJSON(data []byte) (Snapshot, error) {
type alias Snapshot
type wire struct {
alias
CollectionDurationMS int64 `json:"collection_duration_ms"`
}
var w wire
if err := json.Unmarshal(data, &w); err != nil {
return Snapshot{}, err
}
s := Snapshot(w.alias)
s.CollectionDuration = time.Duration(w.CollectionDurationMS) * time.Millisecond
return s, nil
}
// SampleRDevTaskFixture returns the "planned but not implemented" acceptance sample.
func SampleRDevTaskFixture() TaskState {
return TaskState{
ID: "task:rdev-workflow-integration",
Title: "研发工作流 rdev 接入",
Path: ".doc/ops/short-term/2026-09-17-rdev-workflow-integration.md",
DocStatus: DocStatusPlanned,
Claim: AgentClaim{
Claimed: false,
AgentKind: AgentKindUnknown,
Activity: "unknown",
},
Git: GitFacets{
Cleanliness: "unknown",
RemoteNote: "no claimed branch",
},
Verification: VerificationFacets{Result: "unknown", Freshness: FreshnessUnknown},
Deployment: map[string]DeploymentFacets{
"dev": {ContainerStatus: "unknown", AppReadiness: "unknown", ServicesOK: "unknown", IdentityNote: "not installed"},
"test": {ContainerStatus: "unknown", AppReadiness: "unknown", ServicesOK: "unknown", IdentityNote: "not installed"},
"main": {ContainerStatus: "unknown", AppReadiness: "unknown", ServicesOK: "unknown", IdentityNote: "not installed"},
},
EvidenceChain: []string{
".doc/ops/research/2026-09-17-rdev-workflow-reference-analysis.md",
".doc/system-design/project-workflow/07-研发工作流rdev接入-设计与实施计划.md",
".doc/ops/short-term/2026-09-17-rdev-workflow-integration.md",
".doc/backlog/2026-09-17-rdev-phase2-backlog.md",
},
}
}
// SampleDevAheadGitFixture returns local-ahead-of-origin acceptance sample.
func SampleDevAheadGitFixture() GitBranchView {
return GitBranchView{
Name: "dev",
IsCurrent: true,
Head: "abc1234",
Upstream: "origin/dev",
Ahead: 3,
Behind: 0,
RemoteNote: "based on local refs; not network-refreshed — local present, remote availability unconfirmed",
ObservedAt: time.Date(2026, 9, 17, 6, 0, 0, 0, time.UTC),
}
}
// SampleMainEventsForkFixture returns readiness-ok vs events-restarting sample.
func SampleMainEventsForkFixture() EnvironmentState {
return EnvironmentState{
Name: "main",
Containers: []ContainerView{
{Name: "shuimu-agent-main", Image: "agent:main", Status: "running", Health: "healthy", Project: "agent-main", Service: "app"},
{Name: "shuimu-agent-events-main", Image: "events:main", Status: "restarting", RestartCount: 12, Health: "unhealthy", Project: "agent-main", Service: "events"},
},
Readiness: "ok",
Schema: "47",
Facets: DeploymentFacets{
ContainerStatus: "partial",
AppReadiness: "ok",
ServicesOK: "partial",
IdentityNote: "app ready; events restarting — no aggregate green",
},
DriftNotes: []string{
"CURRENT may declare healthy while events container is restarting",
"app /ready ok coexists with events unhealthy",
},
}
}
// SampleHerdrVsPIClaims returns agent kind isolation sample.
func SampleHerdrVsPIClaims() (AgentClaim, AgentClaim) {
herdr := AgentClaim{
Owner: "herdr_coder",
AgentKind: AgentKindHerdrEngineering,
Claimed: true,
Mutex: "engineering-dashboard",
Activity: "unknown",
Evidence: "CURRENT claim only; no runtime activity source",
}
pi := AgentClaim{
Owner: "product-pi-runner",
AgentKind: AgentKindProductPIRunner,
Claimed: false,
Activity: "unknown",
Evidence: "product runtime identity; must not share herdr type",
}
return herdr, pi
}
// NewConflict builds a conflict retaining all fact refs.
func NewConflict(subject, field string, kind ConflictKind, factRefs []string, summary string) Conflict {
refs := append([]string(nil), factRefs...)
sort.Strings(refs)
return Conflict{Subject: subject, Field: field, Kind: kind, FactRefs: refs, Summary: summary}
}
// FactID builds a stable fact id.
func FactID(subject, field, source string) string {
raw := strings.Join([]string{subject, field, source}, "|")
sum := sha256.Sum256([]byte(raw))
return "fact_" + hex.EncodeToString(sum[:10])
}