Skip to content

Commit d4f321b

Browse files
committed
feat!: remove history feature, add PLN/PHS lifecycle docs
BREAKING CHANGE: history subcommand removed, --reason/--actor/--source flags removed - Delete history CLI command and tests - Remove AppendHistory/ReadHistory/DeleteHistory from TOML store - Remove HistoryFile/HistoryEntry structs and MarshalHistoryFile - Remove --reason, --actor, --source flags from entity/relation commands - Remove --force requires --reason check (--force now standalone) - Strip history migration logic from migrate command - Add PLN/PHS lifecycle section to spec-graph, spec-executor, spec-verifier skills - Add phase selection heuristic to spec-verifier - Add git audit trail guidance (git is sole history mechanism) - Update cli-reference.md to remove history section
1 parent 8f321be commit d4f321b

21 files changed

Lines changed: 185 additions & 1158 deletions

File tree

internal/cli/bootstrap.go

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package cli
33
import (
44
"fmt"
55
"os"
6-
"time"
76

87
"github.com/spf13/cobra"
98
"github.com/tyeongkim/spec-graph/internal/bootstrap"
@@ -117,16 +116,6 @@ func applyCandidatesViaToml(input bootstrap.ScanResult) bootstrap.ApplyResult {
117116
continue
118117
}
119118

120-
if err := tomlStore.AppendHistory(c.ID, spectoml.HistoryEntry{
121-
Action: model.ActionCreate,
122-
Reason: "bootstrap import",
123-
Actor: "",
124-
Detail: "bootstrap",
125-
Timestamp: time.Now(),
126-
}); err != nil {
127-
fmt.Fprintf(os.Stderr, "warning: failed to write history for %s: %v\n", c.ID, err)
128-
}
129-
130119
result.Created = append(result.Created, c.ID)
131120
}
132121

@@ -216,16 +205,6 @@ func applyCandidatesViaToml(input bootstrap.ScanResult) bootstrap.ApplyResult {
216205
continue
217206
}
218207

219-
if err := tomlStore.AppendHistory(ownerID, spectoml.HistoryEntry{
220-
Action: model.ActionUpdate,
221-
Reason: "bootstrap import",
222-
Actor: "",
223-
Detail: fmt.Sprintf("add relation %s -> %s [%s]; source=bootstrap", c.From, c.To, c.Type),
224-
Timestamp: time.Now(),
225-
}); err != nil {
226-
fmt.Fprintf(os.Stderr, "warning: failed to write history for %s: %v\n", ownerID, err)
227-
}
228-
229208
result.Created = append(result.Created, key)
230209
}
231210

internal/cli/e2e_v04_test.go

Lines changed: 0 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -9,133 +9,6 @@ import (
99
"github.com/tyeongkim/spec-graph/internal/jsoncontract"
1010
)
1111

12-
func TestE2E_V04_HistoryTracking(t *testing.T) {
13-
dbFile := initTestProject(t)
14-
dir := t.TempDir()
15-
16-
t.Run("entity_create", func(t *testing.T) {
17-
r := runCLI(t, dir, "--db", dbFile, "entity", "add",
18-
"--type", "requirement", "--id", "REQ-001", "--title", "Auth Required",
19-
"--reason", "initial setup", "--actor", "dev-1", "--source", "spec.md")
20-
if r.exitCode != 0 {
21-
t.Fatalf("entity add failed: exit=%d stderr=%s", r.exitCode, r.stderr)
22-
}
23-
})
24-
25-
t.Run("entity_update", func(t *testing.T) {
26-
r := runCLI(t, dir, "--db", dbFile, "entity", "update", "REQ-001",
27-
"--title", "Authentication Required",
28-
"--reason", "clarify title", "--actor", "dev-1")
29-
if r.exitCode != 0 {
30-
t.Fatalf("entity update failed: exit=%d stderr=%s", r.exitCode, r.stderr)
31-
}
32-
})
33-
34-
t.Run("entity_deprecate", func(t *testing.T) {
35-
r := runCLI(t, dir, "--db", dbFile, "entity", "deprecate", "REQ-001",
36-
"--reason", "replaced by REQ-002", "--actor", "dev-2")
37-
if r.exitCode != 0 {
38-
t.Fatalf("entity deprecate failed: exit=%d stderr=%s", r.exitCode, r.stderr)
39-
}
40-
})
41-
42-
t.Run("history_entity_three_entries", func(t *testing.T) {
43-
r := runCLI(t, dir, "--db", dbFile, "history", "entity", "REQ-001")
44-
if r.exitCode != 0 {
45-
t.Fatalf("history entity failed: exit=%d stderr=%s", r.exitCode, r.stderr)
46-
}
47-
48-
var resp jsoncontract.EntityHistoryResponse
49-
if err := json.Unmarshal([]byte(r.stdout), &resp); err != nil {
50-
t.Fatalf("unmarshal: %v\nraw: %s", err, r.stdout)
51-
}
52-
if resp.EntityID != "REQ-001" {
53-
t.Errorf("entity_id = %q; want REQ-001", resp.EntityID)
54-
}
55-
if resp.Count != 3 {
56-
t.Fatalf("count = %d; want 3", resp.Count)
57-
}
58-
if len(resp.Entries) != 3 {
59-
t.Fatalf("len(entries) = %d; want 3", len(resp.Entries))
60-
}
61-
62-
actionMap := map[string]jsoncontract.EntityHistoryEntry{}
63-
for _, e := range resp.Entries {
64-
actionMap[e.Action] = e
65-
}
66-
67-
if _, ok := actionMap["create"]; !ok {
68-
t.Fatal("missing create entry")
69-
}
70-
71-
if _, ok := actionMap["update"]; !ok {
72-
t.Fatal("missing update entry")
73-
}
74-
75-
if _, ok := actionMap["deprecate"]; !ok {
76-
t.Fatal("missing deprecate entry")
77-
}
78-
})
79-
80-
t.Run("history_changeset_deprecated", func(t *testing.T) {
81-
r := runCLI(t, dir, "--db", dbFile, "history", "changeset", "CHG-1")
82-
if r.exitCode != 3 {
83-
t.Fatalf("history changeset should fail: exit=%d stdout=%s", r.exitCode, r.stdout)
84-
}
85-
86-
var errResp jsoncontract.ErrorResponse
87-
if err := json.Unmarshal([]byte(r.stderr), &errResp); err != nil {
88-
t.Fatalf("unmarshal stderr: %v\nraw: %s", err, r.stderr)
89-
}
90-
if errResp.Error.Code != "DEPRECATED" {
91-
t.Errorf("code = %q; want DEPRECATED", errResp.Error.Code)
92-
}
93-
})
94-
95-
t.Run("relation_create_and_delete", func(t *testing.T) {
96-
r := runCLI(t, dir, "--db", dbFile, "entity", "add",
97-
"--type", "interface", "--id", "API-001", "--title", "Auth API")
98-
if r.exitCode != 0 {
99-
t.Fatalf("entity add API-001 failed: exit=%d stderr=%s", r.exitCode, r.stderr)
100-
}
101-
102-
r = runCLI(t, dir, "--db", dbFile, "relation", "add",
103-
"--from", "API-001", "--to", "REQ-001", "--type", "implements",
104-
"--reason", "API implements auth", "--actor", "dev-1")
105-
if r.exitCode != 0 {
106-
t.Fatalf("relation add failed: exit=%d stderr=%s", r.exitCode, r.stderr)
107-
}
108-
109-
r = runCLI(t, dir, "--db", dbFile, "relation", "delete",
110-
"--from", "API-001", "--to", "REQ-001", "--type", "implements",
111-
"--reason", "removing link", "--actor", "dev-2")
112-
if r.exitCode != 0 {
113-
t.Fatalf("relation delete failed: exit=%d stderr=%s", r.exitCode, r.stderr)
114-
}
115-
})
116-
117-
t.Run("history_relation_two_entries", func(t *testing.T) {
118-
r := runCLI(t, dir, "--db", dbFile, "history", "relation", "API-001:REQ-001:implements")
119-
if r.exitCode != 0 {
120-
t.Fatalf("history relation failed: exit=%d stderr=%s", r.exitCode, r.stderr)
121-
}
122-
123-
var resp jsoncontract.RelationHistoryResponse
124-
if err := json.Unmarshal([]byte(r.stdout), &resp); err != nil {
125-
t.Fatalf("unmarshal: %v\nraw: %s", err, r.stdout)
126-
}
127-
if resp.RelationKey != "API-001:REQ-001:implements" {
128-
t.Errorf("relation_key = %q; want API-001:REQ-001:implements", resp.RelationKey)
129-
}
130-
if resp.Count != 2 {
131-
t.Fatalf("count = %d; want 2", resp.Count)
132-
}
133-
if len(resp.Entries) != 2 {
134-
t.Fatalf("len(entries) = %d; want 2", len(resp.Entries))
135-
}
136-
})
137-
}
138-
13912
func TestE2E_V04_BootstrapPipeline(t *testing.T) {
14013
dbFile := initTestProject(t)
14114
dir := t.TempDir()
@@ -287,31 +160,4 @@ REQ-010 depends on DEC-010
287160
t.Errorf("entity count = %d; want 2", resp.Count)
288161
}
289162
})
290-
291-
t.Run("history_bootstrap_entity", func(t *testing.T) {
292-
r := runCLI(t, dir, "--db", dbFile, "history", "entity", "REQ-010")
293-
if r.exitCode != 0 {
294-
t.Fatalf("history entity failed: exit=%d stderr=%s", r.exitCode, r.stderr)
295-
}
296-
297-
var resp jsoncontract.EntityHistoryResponse
298-
if err := json.Unmarshal([]byte(r.stdout), &resp); err != nil {
299-
t.Fatalf("unmarshal: %v\nraw: %s", err, r.stdout)
300-
}
301-
if resp.Count < 1 {
302-
t.Fatalf("count = %d; want >= 1", resp.Count)
303-
}
304-
if len(resp.Entries) < 1 {
305-
t.Fatalf("len(entries) = %d; want >= 1", len(resp.Entries))
306-
}
307-
if resp.Entries[0].Action != "create" {
308-
t.Errorf("action = %q; want create", resp.Entries[0].Action)
309-
}
310-
if resp.Entries[0].Reason != "bootstrap import" {
311-
t.Errorf("reason = %q; want 'bootstrap import'", resp.Entries[0].Reason)
312-
}
313-
if resp.Entries[0].Detail != "bootstrap" {
314-
t.Errorf("detail = %q; want 'bootstrap'", resp.Entries[0].Detail)
315-
}
316-
})
317163
}

internal/cli/entity.go

Lines changed: 1 addition & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -85,20 +85,6 @@ var entityAddCmd = &cobra.Command{
8585
handleError(cmd, fmt.Errorf("write entity: %w", err))
8686
}
8787

88-
reason, _ := cmd.Flags().GetString("reason")
89-
actor, _ := cmd.Flags().GetString("actor")
90-
source, _ := cmd.Flags().GetString("source")
91-
92-
if err := tomlStore.AppendHistory(id, spectoml.HistoryEntry{
93-
Action: model.ActionCreate,
94-
Reason: reason,
95-
Actor: actor,
96-
Detail: source,
97-
Timestamp: time.Now(),
98-
}); err != nil {
99-
handleError(cmd, fmt.Errorf("append history: %w", err))
100-
}
101-
10288
entity, err := ef.ToEntity()
10389
if err != nil {
10490
handleError(cmd, fmt.Errorf("convert entity: %w", err))
@@ -257,10 +243,6 @@ var entityUpdateCmd = &cobra.Command{
257243

258244
if report.Blocked {
259245
if forceFlag {
260-
reason, _ := cmd.Flags().GetString("reason")
261-
if reason == "" {
262-
handleError(cmd, &model.ErrInvalidInput{Message: "--force requires --reason"})
263-
}
264246
if len(report.Warnings) > 0 || len(report.BlockingIssues) > 0 {
265247
allIssues := append(report.BlockingIssues, report.Warnings...)
266248
warningOutput := make([]jsoncontract.ValidateIssue, len(allIssues))
@@ -324,25 +306,6 @@ var entityUpdateCmd = &cobra.Command{
324306
handleError(cmd, fmt.Errorf("write entity: %w", err))
325307
}
326308

327-
reason, _ := cmd.Flags().GetString("reason")
328-
actor, _ := cmd.Flags().GetString("actor")
329-
source, _ := cmd.Flags().GetString("source")
330-
331-
detail := source
332-
if forceFlag {
333-
detail = "force=true; " + detail
334-
}
335-
336-
if err := tomlStore.AppendHistory(id, spectoml.HistoryEntry{
337-
Action: model.ActionUpdate,
338-
Reason: reason,
339-
Actor: actor,
340-
Detail: detail,
341-
Timestamp: time.Now(),
342-
}); err != nil {
343-
handleError(cmd, fmt.Errorf("append history: %w", err))
344-
}
345-
346309
entity, err := ef.ToEntity()
347310
if err != nil {
348311
handleError(cmd, fmt.Errorf("convert entity %q: %w", id, err))
@@ -371,20 +334,6 @@ var entityDeprecateCmd = &cobra.Command{
371334
handleError(cmd, fmt.Errorf("write entity: %w", err))
372335
}
373336

374-
reason, _ := cmd.Flags().GetString("reason")
375-
actor, _ := cmd.Flags().GetString("actor")
376-
source, _ := cmd.Flags().GetString("source")
377-
378-
if err := tomlStore.AppendHistory(id, spectoml.HistoryEntry{
379-
Action: model.ActionDeprecate,
380-
Reason: reason,
381-
Actor: actor,
382-
Detail: source,
383-
Timestamp: time.Now(),
384-
}); err != nil {
385-
handleError(cmd, fmt.Errorf("append history: %w", err))
386-
}
387-
388337
entity, _ := ef.ToEntity()
389338
writeJSON(cmd, jsoncontract.EntityResponse{Entity: entity})
390339
return nil
@@ -417,8 +366,6 @@ var entityDeleteCmd = &cobra.Command{
417366
handleError(cmd, fmt.Errorf("delete entity: %w", err))
418367
}
419368

420-
_ = tomlStore.DeleteHistory(id)
421-
422369
writeJSON(cmd, jsoncontract.DeleteResponse{Deleted: id})
423370
return nil
424371
},
@@ -452,10 +399,6 @@ var entityImportCmd = &cobra.Command{
452399
handleError(cmd, &model.ErrInvalidInput{Message: "parse input file: " + err.Error()})
453400
}
454401

455-
reason, _ := cmd.Flags().GetString("reason")
456-
actor, _ := cmd.Flags().GetString("actor")
457-
source, _ := cmd.Flags().GetString("source")
458-
459402
var created []string
460403
var skipped []jsoncontract.BootstrapSkippedItem
461404
var errors []jsoncontract.BootstrapErrorItem
@@ -514,16 +457,6 @@ var entityImportCmd = &cobra.Command{
514457
continue
515458
}
516459

517-
if err := tomlStore.AppendHistory(item.ID, spectoml.HistoryEntry{
518-
Action: model.ActionCreate,
519-
Reason: reason,
520-
Actor: actor,
521-
Detail: source,
522-
Timestamp: time.Now(),
523-
}); err != nil {
524-
fmt.Fprintf(os.Stderr, "warning: failed to write history for %s: %v\n", item.ID, err)
525-
}
526-
527460
created = append(created, item.ID)
528461
}
529462

@@ -544,9 +477,6 @@ func init() {
544477
entityAddCmd.Flags().String("metadata", "", "entity metadata as JSON string")
545478
entityAddCmd.Flags().String("metadata-file", "", "path to JSON file containing metadata (mutually exclusive with --metadata)")
546479
entityAddCmd.Flags().String("status", "", "entity status")
547-
entityAddCmd.Flags().String("reason", "", "reason for creating this entity")
548-
entityAddCmd.Flags().String("actor", "", "actor performing the change")
549-
entityAddCmd.Flags().String("source", "", "source of the change")
550480

551481
entityListCmd.Flags().String("type", "", "filter by entity type")
552482
entityListCmd.Flags().String("status", "", "filter by entity status")
@@ -556,23 +486,9 @@ func init() {
556486
entityUpdateCmd.Flags().String("status", "", "new status")
557487
entityUpdateCmd.Flags().String("metadata", "", "new metadata as JSON string")
558488
entityUpdateCmd.Flags().String("metadata-file", "", "path to JSON file containing metadata (mutually exclusive with --metadata)")
559-
entityUpdateCmd.Flags().String("reason", "", "reason for update")
560-
entityUpdateCmd.Flags().String("actor", "", "actor performing the change")
561-
entityUpdateCmd.Flags().String("source", "", "source of the change")
562-
entityUpdateCmd.Flags().Bool("force", false, "bypass gate checks (requires --reason)")
563-
564-
entityDeprecateCmd.Flags().String("reason", "", "reason for deprecation")
565-
entityDeprecateCmd.Flags().String("actor", "", "actor performing the change")
566-
entityDeprecateCmd.Flags().String("source", "", "source of the change")
567-
568-
entityDeleteCmd.Flags().String("reason", "", "reason for deletion")
569-
entityDeleteCmd.Flags().String("actor", "", "actor performing the change")
570-
entityDeleteCmd.Flags().String("source", "", "source of the change")
489+
entityUpdateCmd.Flags().Bool("force", false, "bypass gate checks")
571490

572491
entityImportCmd.Flags().String("input", "", "path to JSON file containing entity array (required)")
573-
entityImportCmd.Flags().String("reason", "", "reason for import")
574-
entityImportCmd.Flags().String("actor", "", "actor performing the import")
575-
entityImportCmd.Flags().String("source", "", "source of the import")
576492

577493
entityCmd.AddCommand(entityAddCmd)
578494
entityCmd.AddCommand(entityGetCmd)

internal/cli/entity_test.go

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -664,7 +664,7 @@ func TestGateForceBypassWithReason(t *testing.T) {
664664
setupGatedPhaseWithUnresolvedQuestion(t, dir, dbFile)
665665

666666
r := runCLI(t, dir, "--db", dbFile, "entity", "update", "PHS-001",
667-
"--status", "resolved", "--force", "--reason", "override for testing")
667+
"--status", "resolved", "--force")
668668
if r.exitCode != 0 {
669669
t.Fatalf("expected exit 0 (force bypass), got %d; stdout=%s stderr=%s", r.exitCode, r.stdout, r.stderr)
670670
}
@@ -692,26 +692,6 @@ func TestGateForceBypassWithReason(t *testing.T) {
692692
}
693693
}
694694

695-
func TestGateForceWithoutReasonFails(t *testing.T) {
696-
dbFile := initTestProject(t)
697-
dir := t.TempDir()
698-
setupGatedPhaseWithUnresolvedQuestion(t, dir, dbFile)
699-
700-
r := runCLI(t, dir, "--db", dbFile, "entity", "update", "PHS-001",
701-
"--status", "resolved", "--force")
702-
if r.exitCode != 3 {
703-
t.Fatalf("expected exit 3 (INVALID_INPUT), got %d; stdout=%s stderr=%s", r.exitCode, r.stdout, r.stderr)
704-
}
705-
706-
var errResp jsoncontract.ErrorResponse
707-
if err := json.Unmarshal([]byte(r.stderr), &errResp); err != nil {
708-
t.Fatalf("unmarshal error response: %v\nraw: %s", err, r.stderr)
709-
}
710-
if errResp.Error.Code != "INVALID_INPUT" {
711-
t.Errorf("error.code = %q; want INVALID_INPUT", errResp.Error.Code)
712-
}
713-
}
714-
715695
func TestGateNotAppliedForNonResolvedTransition(t *testing.T) {
716696
dbFile := initTestProject(t)
717697
dir := t.TempDir()

0 commit comments

Comments
 (0)