summaryrefslogtreecommitdiff
path: root/internal/story/story.go
diff options
context:
space:
mode:
authorClaude Sonnet 5 <noreply@anthropic.com>2026-07-03 23:46:26 +0000
committerClaude Sonnet 5 <noreply@anthropic.com>2026-07-03 23:46:26 +0000
commit392c7c1ada310b2f928dca89b75ba628478f7694 (patch)
treec2b32420ce5df0da79d6d4b014fe2c784f2bd4be /internal/story/story.go
parent997cd8b56bc086a02b9c7c006dd62b07b9fcd2f3 (diff)
feat(story): add Epic/Story data model + REST CRUD (Phase 7a)
Pure data model + CRUD -- no orchestration behavior yet (that's Phase 7b). Revives ADR-007's stories concept (adapted; ADR-007 itself was deleted as "more machinery than the usage pattern needed") while staying lean: reuses the existing events/projects/task-tree machinery rather than building a parallel hierarchy. - internal/story: Epic/Story types. Epic is a loosely-scoped initiative (OPEN/CLOSED, no execution semantics). Story is a shippable slice of work realized by a task tree rooted at root_task_id; epic_id/project_id/ root_task_id are loose references (no FK enforcement, matching the codebase's existing tolerance for unmatched reference strings like tasks.project). - storage: new epics/stories tables (additive migrations) + CRUD methods. Story status is intentionally unvalidated pass-through in this phase -- no task.ValidTransition-style state machine, since there's no orchestrator yet to make transitions meaningful. - event: 9 new Kind constants for the ceremony this layer will eventually drive (epic_proposed, discovery_proposed, framing_decided, groomed, prioritized, eval_verdict, arbitration_decided, retro_captured, human_accepted) -- defined but nothing emits them yet. - api: GET/POST /api/epics, GET/PUT /api/epics/{id}, GET /api/epics/{id}/stories, GET/POST /api/stories, GET/PUT /api/stories/{id}, and GET /api/stories/{id}/task-tree -- BFS walk from root_task_id following both parent_task_id children and depends_on-edge dependents (visited-set guarded), returning a flat node list for a later UI phase to render as a graph. Unauthenticated, matching the existing projects/tasks endpoints' posture. go build/vet/test -race -count=1 all pass, full suite. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1moSNCJRcP6kykA4tyUSs
Diffstat (limited to 'internal/story/story.go')
-rw-r--r--internal/story/story.go73
1 files changed, 73 insertions, 0 deletions
diff --git a/internal/story/story.go b/internal/story/story.go
new file mode 100644
index 0000000..dc1764f
--- /dev/null
+++ b/internal/story/story.go
@@ -0,0 +1,73 @@
+// Package story defines the Epic and Story data types that sit above
+// internal/task's flat task tree in the planning hierarchy. This phase
+// (7a) is pure data model + CRUD: no orchestration reads or writes these
+// types yet. A later phase builds fan-out/orchestration logic (creating
+// Builder/Evaluator/Arbitration tasks, gating deploys, etc.) on top of what
+// is stored here.
+package story
+
+import (
+ "encoding/json"
+ "time"
+)
+
+// Epic is a large, loosely-scoped initiative that decomposes into one or
+// more Stories. Epics are not execution units — they carry no root_task_id
+// and are never dispatched.
+type Epic struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Description string `json:"description,omitempty"`
+ // Status is "OPEN" or "CLOSED". This phase does not validate transitions
+ // — storage writes whatever status string it is given, the same trust
+ // level as task.Project/UpdateProject.
+ Status string `json:"status"`
+ // DiscoverySource is "human" or "agent" — how the epic came to exist.
+ DiscoverySource string `json:"discovery_source,omitempty"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+}
+
+// Story is a shippable slice of work realized by a tree of internal/task
+// Tasks rooted at RootTaskID. EpicID and ProjectID are loose references
+// (plain strings, no FK enforcement) to epics.id / projects.id, matching
+// the codebase's existing tolerance for unmatched reference strings (e.g.
+// tasks.project).
+//
+// Status is one of DISCOVERY|FRAMING|BACKLOG|PRIORITIZED|IN_PROGRESS|
+// SHIPPABLE|DEPLOYED|VALIDATING|REVIEW_READY|NEEDS_FIX|DONE|CANCELLED, but
+// this phase enforces no state machine on it (unlike task.ValidTransition)
+// — a later phase adds that once there is an orchestrator to make
+// transitions meaningful. UpdateStory writes whatever status string it is
+// given.
+type Story struct {
+ ID string `json:"id"`
+ EpicID string `json:"epic_id,omitempty"`
+ ProjectID string `json:"project_id,omitempty"`
+ Name string `json:"name"`
+ BranchName string `json:"branch_name,omitempty"`
+ Status string `json:"status"`
+ DiscoverySource string `json:"discovery_source,omitempty"`
+ // Spec is the Planner's framing-pass output: product + arch context.
+ // Freeform text, not JSON.
+ Spec string `json:"spec,omitempty"`
+ // AcceptanceCriteria is stored as acceptance_criteria_json; defaults to
+ // an empty list, never null in API responses.
+ AcceptanceCriteria []string `json:"acceptance_criteria"`
+ // Validation is the freeform {type, steps, success_criteria} blob a
+ // later phase's validation agent will consume (curl|tests|playwright|
+ // gradle). Stored verbatim as validation_json; nil/omitted if unset.
+ Validation json.RawMessage `json:"validation,omitempty"`
+ // DeployConfig is optional, freeform JSON — only populated if a story
+ // should deploy-gate. Stored verbatim as deploy_config; nil/omitted if
+ // unset.
+ DeployConfig json.RawMessage `json:"deploy_config,omitempty"`
+ Priority string `json:"priority,omitempty"`
+ // RootTaskID is the top-level task realizing this story (usually a
+ // Planner-role task). Empty/unset means the story has no execution tree
+ // yet — GET .../task-tree returns an empty tree, not an error, in that
+ // case.
+ RootTaskID string `json:"root_task_id,omitempty"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+}