Define versioned Snapshot/Fact/Conflict/TaskState contracts with independent plan/git/verification/deployment axes and acceptance fixtures. Co-authored-by: Cursor <cursoragent@cursor.com>
500 lines
18 KiB
Go
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])
|
|
}
|