devrites 5.3.0 → 5.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/docs/command-map.md +2 -2
  4. package/docs/engine/state-schema.md +12 -0
  5. package/docs/flow.md +4 -0
  6. package/docs/orchestration.md +5 -2
  7. package/engine/commands.go +2 -2
  8. package/engine/internal/devritespaths/paths.go +15 -0
  9. package/engine/internal/gate/gate.go +78 -0
  10. package/engine/internal/gate/gate_test.go +118 -0
  11. package/engine/internal/lib/candidate.go +7 -3
  12. package/engine/internal/lib/manifest_union.go +326 -0
  13. package/engine/internal/lib/manifest_union_test.go +214 -0
  14. package/engine/internal/lib/observe_budgets.go +73 -0
  15. package/engine/internal/lib/observe_budgets_test.go +142 -0
  16. package/engine/internal/lib/observe_summary.go +74 -13
  17. package/engine/internal/lib/workspace_read.go +11 -6
  18. package/engine/internal/state/budgets.go +163 -0
  19. package/engine/internal/state/budgets_test.go +42 -0
  20. package/engine/internal/state/cursor.go +16 -10
  21. package/engine/internal/state/observation.go +50 -4
  22. package/engine/internal/state/status.go +38 -26
  23. package/engine/internal/state/taskgraph.go +23 -0
  24. package/engine/main.go +32 -13
  25. package/engine/root_routing_test.go +3 -1
  26. package/pack/.claude/agents/devrites-code-reviewer.md +9 -6
  27. package/pack/.claude/agents/devrites-doubt-reviewer.md +6 -0
  28. package/pack/.claude/agents/devrites-plan-reviewer.md +7 -1
  29. package/pack/.claude/agents/devrites-slice-wright.md +42 -46
  30. package/pack/.claude/agents/devrites-test-analyst.md +9 -4
  31. package/pack/.claude/agents/devrites-upgrade-planner.md +9 -1
  32. package/pack/.claude/settings.json +17 -0
  33. package/pack/.claude/skills/devrites-api-interface/SKILL.md +2 -2
  34. package/pack/.claude/skills/devrites-doubt/SKILL.md +9 -17
  35. package/pack/.claude/skills/devrites-lib/reference/candidate-integrity.md +34 -35
  36. package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +11 -12
  37. package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +104 -51
  38. package/pack/.claude/skills/devrites-lib/reference/standards/agents.md +1 -0
  39. package/pack/.claude/skills/devrites-lib/reference/standards/context-hygiene.md +45 -25
  40. package/pack/.claude/skills/devrites-lib/reference/standards/core.md +1 -1
  41. package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +80 -41
  42. package/pack/.claude/skills/rite-autocomplete/SKILL.md +19 -8
  43. package/pack/.claude/skills/rite-autocomplete/reference/loop.md +33 -1
  44. package/pack/.claude/skills/rite-build/SKILL.md +26 -17
  45. package/pack/.claude/skills/rite-build/reference/afk-discipline.md +1 -1
  46. package/pack/.claude/skills/rite-build/reference/parallel-batch.md +101 -39
  47. package/pack/.claude/skills/rite-build/reference/phase-contract.md +79 -59
  48. package/pack/.claude/skills/rite-build/reference/spec-drift-guard.md +14 -15
  49. package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +8 -8
  50. package/pack/.claude/skills/rite-define/SKILL.md +4 -1
  51. package/pack/.claude/skills/rite-plan/SKILL.md +4 -1
  52. package/pack/.claude/skills/rite-plan/reference/replan-and-repair.md +21 -0
  53. package/pack/.claude/skills/rite-plan/reference/slicing.md +117 -15
  54. package/pack/.claude/skills/rite-prove/SKILL.md +20 -22
  55. package/pack/.claude/skills/rite-review/SKILL.md +23 -16
  56. package/pack/.claude/skills/rite-seal/SKILL.md +8 -8
  57. package/pack/.claude/skills/rite-seal/reference/final-evidence.md +6 -5
  58. package/pack/.claude/skills/rite-seal/reference/phase-contract.md +13 -13
  59. package/pack/.claude/skills/rite-ship/SKILL.md +3 -0
  60. package/pack/.claude/skills/rite-ship/reference/close-out.md +16 -2
  61. package/pack/.claude/skills/rite-ship/reference/git-ship.md +14 -5
  62. package/pack/.claude/skills/rite-spec/SKILL.md +6 -0
  63. package/pack/.claude/skills/rite-spec/reference/state-workspace.md +6 -0
  64. package/pack/.claude/skills/rite-status/SKILL.md +4 -1
  65. package/pack/.claude/skills/rite-temper/SKILL.md +14 -11
  66. package/pack/.claude/skills/rite-temper/reference/anti-patterns.md +14 -13
  67. package/pack/.claude/skills/rite-temper/reference/review-dimensions.md +3 -4
  68. package/pack/.claude/skills/rite-upgrade/SKILL.md +11 -2
  69. package/pack/.claude/skills/rite-vet/SKILL.md +22 -15
  70. package/pack/.claude/skills/rite-vet/reference/anti-patterns.md +11 -9
  71. package/pack/.claude/skills/rite-vet/reference/depth.md +6 -7
  72. package/pack/.claude/skills/rite-vet/reference/review-axes.md +39 -14
  73. package/pack/generated/claude/agents/devrites-code-reviewer.md +9 -6
  74. package/pack/generated/claude/agents/devrites-doubt-reviewer.md +6 -0
  75. package/pack/generated/claude/agents/devrites-plan-reviewer.md +7 -1
  76. package/pack/generated/claude/agents/devrites-slice-wright.md +42 -46
  77. package/pack/generated/claude/agents/devrites-test-analyst.md +9 -4
  78. package/pack/generated/claude/agents/devrites-upgrade-planner.md +9 -1
  79. package/pack/generated/claude/settings.json +17 -0
  80. package/pack/generated/claude/skills/devrites-api-interface/SKILL.md +2 -2
  81. package/pack/generated/claude/skills/devrites-doubt/SKILL.md +9 -17
  82. package/pack/generated/claude/skills/devrites-lib/reference/candidate-integrity.md +34 -35
  83. package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +11 -12
  84. package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +104 -51
  85. package/pack/generated/claude/skills/devrites-lib/reference/standards/agents.md +1 -0
  86. package/pack/generated/claude/skills/devrites-lib/reference/standards/context-hygiene.md +45 -25
  87. package/pack/generated/claude/skills/devrites-lib/reference/standards/core.md +1 -1
  88. package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +80 -41
  89. package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +19 -8
  90. package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +33 -1
  91. package/pack/generated/claude/skills/rite-build/SKILL.md +26 -17
  92. package/pack/generated/claude/skills/rite-build/reference/afk-discipline.md +1 -1
  93. package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +101 -39
  94. package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +79 -59
  95. package/pack/generated/claude/skills/rite-build/reference/spec-drift-guard.md +14 -15
  96. package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +8 -8
  97. package/pack/generated/claude/skills/rite-define/SKILL.md +4 -1
  98. package/pack/generated/claude/skills/rite-plan/SKILL.md +4 -1
  99. package/pack/generated/claude/skills/rite-plan/reference/replan-and-repair.md +21 -0
  100. package/pack/generated/claude/skills/rite-plan/reference/slicing.md +117 -15
  101. package/pack/generated/claude/skills/rite-prove/SKILL.md +20 -22
  102. package/pack/generated/claude/skills/rite-review/SKILL.md +23 -16
  103. package/pack/generated/claude/skills/rite-seal/SKILL.md +8 -8
  104. package/pack/generated/claude/skills/rite-seal/reference/final-evidence.md +6 -5
  105. package/pack/generated/claude/skills/rite-seal/reference/phase-contract.md +13 -13
  106. package/pack/generated/claude/skills/rite-ship/SKILL.md +3 -0
  107. package/pack/generated/claude/skills/rite-ship/reference/close-out.md +16 -2
  108. package/pack/generated/claude/skills/rite-ship/reference/git-ship.md +14 -5
  109. package/pack/generated/claude/skills/rite-spec/SKILL.md +6 -0
  110. package/pack/generated/claude/skills/rite-spec/reference/state-workspace.md +6 -0
  111. package/pack/generated/claude/skills/rite-status/SKILL.md +4 -1
  112. package/pack/generated/claude/skills/rite-temper/SKILL.md +14 -11
  113. package/pack/generated/claude/skills/rite-temper/reference/anti-patterns.md +14 -13
  114. package/pack/generated/claude/skills/rite-temper/reference/review-dimensions.md +3 -4
  115. package/pack/generated/claude/skills/rite-upgrade/SKILL.md +11 -2
  116. package/pack/generated/claude/skills/rite-vet/SKILL.md +22 -15
  117. package/pack/generated/claude/skills/rite-vet/reference/anti-patterns.md +11 -9
  118. package/pack/generated/claude/skills/rite-vet/reference/depth.md +6 -7
  119. package/pack/generated/claude/skills/rite-vet/reference/review-axes.md +39 -14
  120. package/pack/generated/codex/agents/devrites-code-reviewer.toml +9 -6
  121. package/pack/generated/codex/agents/devrites-doubt-reviewer.toml +6 -0
  122. package/pack/generated/codex/agents/devrites-plan-reviewer.toml +7 -1
  123. package/pack/generated/codex/agents/devrites-slice-wright.toml +42 -46
  124. package/pack/generated/codex/agents/devrites-test-analyst.toml +9 -4
  125. package/pack/generated/codex/agents/devrites-upgrade-planner.toml +9 -1
  126. package/pack/generated/codex/skills/devrites-api-interface/SKILL.md +2 -2
  127. package/pack/generated/codex/skills/devrites-doubt/SKILL.md +9 -17
  128. package/pack/generated/codex/skills/devrites-lib/reference/candidate-integrity.md +34 -35
  129. package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +11 -12
  130. package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +104 -51
  131. package/pack/generated/codex/skills/devrites-lib/reference/standards/agents.md +1 -0
  132. package/pack/generated/codex/skills/devrites-lib/reference/standards/context-hygiene.md +45 -25
  133. package/pack/generated/codex/skills/devrites-lib/reference/standards/core.md +1 -1
  134. package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +80 -41
  135. package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +19 -8
  136. package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +33 -1
  137. package/pack/generated/codex/skills/rite-build/SKILL.md +26 -17
  138. package/pack/generated/codex/skills/rite-build/reference/afk-discipline.md +1 -1
  139. package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +101 -39
  140. package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +79 -59
  141. package/pack/generated/codex/skills/rite-build/reference/spec-drift-guard.md +14 -15
  142. package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +8 -8
  143. package/pack/generated/codex/skills/rite-define/SKILL.md +4 -1
  144. package/pack/generated/codex/skills/rite-plan/SKILL.md +4 -1
  145. package/pack/generated/codex/skills/rite-plan/reference/replan-and-repair.md +21 -0
  146. package/pack/generated/codex/skills/rite-plan/reference/slicing.md +117 -15
  147. package/pack/generated/codex/skills/rite-prove/SKILL.md +20 -22
  148. package/pack/generated/codex/skills/rite-review/SKILL.md +23 -16
  149. package/pack/generated/codex/skills/rite-seal/SKILL.md +8 -8
  150. package/pack/generated/codex/skills/rite-seal/reference/final-evidence.md +6 -5
  151. package/pack/generated/codex/skills/rite-seal/reference/phase-contract.md +13 -13
  152. package/pack/generated/codex/skills/rite-ship/SKILL.md +3 -0
  153. package/pack/generated/codex/skills/rite-ship/reference/close-out.md +16 -2
  154. package/pack/generated/codex/skills/rite-ship/reference/git-ship.md +14 -5
  155. package/pack/generated/codex/skills/rite-spec/SKILL.md +6 -0
  156. package/pack/generated/codex/skills/rite-spec/reference/state-workspace.md +6 -0
  157. package/pack/generated/codex/skills/rite-status/SKILL.md +4 -1
  158. package/pack/generated/codex/skills/rite-temper/SKILL.md +14 -11
  159. package/pack/generated/codex/skills/rite-temper/reference/anti-patterns.md +14 -13
  160. package/pack/generated/codex/skills/rite-temper/reference/review-dimensions.md +3 -4
  161. package/pack/generated/codex/skills/rite-upgrade/SKILL.md +11 -2
  162. package/pack/generated/codex/skills/rite-vet/SKILL.md +22 -15
  163. package/pack/generated/codex/skills/rite-vet/reference/anti-patterns.md +11 -9
  164. package/pack/generated/codex/skills/rite-vet/reference/depth.md +6 -7
  165. package/pack/generated/codex/skills/rite-vet/reference/review-axes.md +39 -14
  166. package/pack/generated/omp/agents/devrites-code-reviewer.md +9 -6
  167. package/pack/generated/omp/agents/devrites-doubt-reviewer.md +6 -0
  168. package/pack/generated/omp/agents/devrites-plan-reviewer.md +7 -1
  169. package/pack/generated/omp/agents/devrites-slice-wright.md +42 -46
  170. package/pack/generated/omp/agents/devrites-test-analyst.md +9 -4
  171. package/pack/generated/omp/agents/devrites-upgrade-planner.md +9 -1
  172. package/pack/generated/omp/skills/devrites-api-interface/SKILL.md +2 -2
  173. package/pack/generated/omp/skills/devrites-doubt/SKILL.md +9 -17
  174. package/pack/generated/omp/skills/devrites-lib/reference/candidate-integrity.md +34 -35
  175. package/pack/generated/omp/skills/devrites-lib/reference/parallel-dispatch.md +11 -12
  176. package/pack/generated/omp/skills/devrites-lib/reference/standards/afk-hitl.md +104 -51
  177. package/pack/generated/omp/skills/devrites-lib/reference/standards/agents.md +1 -0
  178. package/pack/generated/omp/skills/devrites-lib/reference/standards/context-hygiene.md +45 -25
  179. package/pack/generated/omp/skills/devrites-lib/reference/standards/core.md +1 -1
  180. package/pack/generated/omp/skills/devrites-lib/reference/workspace-artifact-schema.md +80 -41
  181. package/pack/generated/omp/skills/rite-autocomplete/SKILL.md +19 -8
  182. package/pack/generated/omp/skills/rite-autocomplete/reference/loop.md +33 -1
  183. package/pack/generated/omp/skills/rite-build/SKILL.md +26 -17
  184. package/pack/generated/omp/skills/rite-build/reference/afk-discipline.md +1 -1
  185. package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +101 -39
  186. package/pack/generated/omp/skills/rite-build/reference/phase-contract.md +79 -59
  187. package/pack/generated/omp/skills/rite-build/reference/spec-drift-guard.md +14 -15
  188. package/pack/generated/omp/skills/rite-build/reference/wright-dispatch.md +8 -8
  189. package/pack/generated/omp/skills/rite-define/SKILL.md +4 -1
  190. package/pack/generated/omp/skills/rite-plan/SKILL.md +4 -1
  191. package/pack/generated/omp/skills/rite-plan/reference/replan-and-repair.md +21 -0
  192. package/pack/generated/omp/skills/rite-plan/reference/slicing.md +117 -15
  193. package/pack/generated/omp/skills/rite-prove/SKILL.md +20 -22
  194. package/pack/generated/omp/skills/rite-review/SKILL.md +23 -16
  195. package/pack/generated/omp/skills/rite-seal/SKILL.md +8 -8
  196. package/pack/generated/omp/skills/rite-seal/reference/final-evidence.md +6 -5
  197. package/pack/generated/omp/skills/rite-seal/reference/phase-contract.md +13 -13
  198. package/pack/generated/omp/skills/rite-ship/SKILL.md +3 -0
  199. package/pack/generated/omp/skills/rite-ship/reference/close-out.md +16 -2
  200. package/pack/generated/omp/skills/rite-ship/reference/git-ship.md +14 -5
  201. package/pack/generated/omp/skills/rite-spec/SKILL.md +6 -0
  202. package/pack/generated/omp/skills/rite-spec/reference/state-workspace.md +6 -0
  203. package/pack/generated/omp/skills/rite-status/SKILL.md +4 -1
  204. package/pack/generated/omp/skills/rite-temper/SKILL.md +14 -11
  205. package/pack/generated/omp/skills/rite-temper/reference/anti-patterns.md +14 -13
  206. package/pack/generated/omp/skills/rite-temper/reference/review-dimensions.md +3 -4
  207. package/pack/generated/omp/skills/rite-upgrade/SKILL.md +11 -2
  208. package/pack/generated/omp/skills/rite-vet/SKILL.md +22 -15
  209. package/pack/generated/omp/skills/rite-vet/reference/anti-patterns.md +11 -9
  210. package/pack/generated/omp/skills/rite-vet/reference/depth.md +6 -7
  211. package/pack/generated/omp/skills/rite-vet/reference/review-axes.md +39 -14
  212. package/package.json +1 -1
  213. package/scripts/codex-generate.sh +6 -0
@@ -0,0 +1,326 @@
1
+ package lib
2
+
3
+ import (
4
+ "errors"
5
+ "fmt"
6
+ "io"
7
+ "os"
8
+ "path/filepath"
9
+ "sort"
10
+ "strings"
11
+
12
+ "github.com/devrites/devrites/internal/devritespaths"
13
+ "github.com/devrites/devrites/internal/state"
14
+ )
15
+
16
+ // maxSequenceChain bounds the sequence_parent walk so a malformed cycle cannot
17
+ // loop forever; real chains are far smaller.
18
+ const maxSequenceChain = 64
19
+
20
+ // RunMergeManifest folds predecessor candidate manifests into the release
21
+ // workspace's `## Candidate manifest` so a release milestone's prove/review/seal
22
+ // binds the union surface instead of only its own slices. On a path collision
23
+ // the later sequence position's row wins (it records the most recent touch).
24
+ // Everything outside the manifest section of the release workspace's
25
+ // touched-files.md is preserved.
26
+ //
27
+ // args is `<slug>` to walk the recorded `sequence_parent` chain, or
28
+ // `<slug> <predecessor> [<predecessor>...]` for an explicit ordered list —
29
+ // required for chains recorded before the sequence cursor fields existed.
30
+ // Exit codes:
31
+ //
32
+ // 0 merged and written
33
+ // 2 usage error
34
+ // 3 blocked: a workspace, manifest, or chain check failed
35
+ // 1 internal/IO error
36
+ func RunMergeManifest(root string, args []string, stdout, stderr io.Writer) int {
37
+ if len(args) < 1 {
38
+ fmt.Fprintln(stderr, "usage: devrites-engine state merge-manifest <slug> [<predecessor>...]")
39
+ return 2
40
+ }
41
+ slug := args[0]
42
+ workspace, err := devritespaths.ExistingFeatureDirChecked(root, slug)
43
+ if err != nil {
44
+ fmt.Fprintf(stderr, "merge-manifest: BLOCKED: workspace for %s: %v\n", slug, err)
45
+ return 3
46
+ }
47
+ if err := state.RequireWorkspaceSchema(root, slug); err != nil {
48
+ fmt.Fprintf(stderr, "merge-manifest: BLOCKED: %v\n", err)
49
+ return 3
50
+ }
51
+ manifestPath := filepath.Join(workspace, "touched-files.md")
52
+ releaseRaw, err := readBoundedRegularFile(manifestPath, maxCandidateManifestBytes)
53
+ if err != nil {
54
+ fmt.Fprintf(stderr, "merge-manifest: BLOCKED: candidate manifest %s: %v\n", manifestPath, err)
55
+ return 3
56
+ }
57
+
58
+ var manifests [][]candidateRow
59
+ if len(args) > 1 {
60
+ manifests, err = explicitManifests(root, slug, args[1:])
61
+ } else {
62
+ // The walked chain is validated inside walkSequenceChain; only its
63
+ // manifests feed the union.
64
+ _, manifests, err = walkSequenceChain(root, slug, workspace)
65
+ }
66
+ if err != nil {
67
+ fmt.Fprintf(stderr, "merge-manifest: BLOCKED: %v\n", err)
68
+ return 3
69
+ }
70
+
71
+ merged := make(map[string]candidateRow)
72
+ collisions := 0
73
+ fold := func(rows []candidateRow) {
74
+ for _, row := range rows {
75
+ if _, dup := merged[row.path]; dup {
76
+ collisions++
77
+ }
78
+ merged[row.path] = row
79
+ }
80
+ }
81
+ for _, rows := range manifests {
82
+ fold(rows)
83
+ }
84
+ releaseRows, err := parseCandidateManifest(releaseRaw)
85
+ if err != nil {
86
+ fmt.Fprintf(stderr, "merge-manifest: BLOCKED: candidate manifest %s: %v\n", manifestPath, err)
87
+ return 3
88
+ }
89
+ fold(releaseRows)
90
+
91
+ paths := make([]string, 0, len(merged))
92
+ for path := range merged {
93
+ paths = append(paths, path)
94
+ }
95
+ sort.Strings(paths)
96
+ body := make([]string, 0, len(paths)+2)
97
+ if len(paths) == 0 {
98
+ body = append(body, "No project files.")
99
+ } else {
100
+ body = append(body, "| State | File | Slice | Reason |", "| --- | --- | --- | --- |")
101
+ for _, path := range paths {
102
+ row := merged[path]
103
+ body = append(body, fmt.Sprintf("| %s | `%s` | %s | %s |", row.state, row.path, row.slice, row.reason))
104
+ }
105
+ }
106
+ updated, err := replaceManifestSectionBody(releaseRaw, body)
107
+ if err != nil {
108
+ fmt.Fprintf(stderr, "merge-manifest: BLOCKED: candidate manifest %s: %v\n", manifestPath, err)
109
+ return 3
110
+ }
111
+ if err := state.AtomicWrite(manifestPath, updated, 0o644); err != nil {
112
+ fmt.Fprintf(stderr, "merge-manifest: cannot write %s: %v\n", manifestPath, err)
113
+ return 1
114
+ }
115
+ fmt.Fprintf(stdout, "merge-manifest: merged %d manifest(s) into %d row(s)", len(manifests)+1, len(paths))
116
+ if collisions > 0 {
117
+ fmt.Fprintf(stdout, " (%d path collision(s), later position kept)", collisions)
118
+ }
119
+ fmt.Fprintln(stdout)
120
+ return 0
121
+ }
122
+
123
+ // explicitManifests parses the caller-ordered predecessor list. When the
124
+ // workspace carries a sequence_parent cursor, the last predecessor must be the
125
+ // recorded parent; a mismatch means the list does not describe this chain.
126
+ func explicitManifests(root, slug string, predecessors []string) ([][]candidateRow, error) {
127
+ seen := make(map[string]struct{}, len(predecessors))
128
+ for _, pred := range predecessors {
129
+ if pred == slug {
130
+ return nil, fmt.Errorf("predecessor %q names the target workspace", pred)
131
+ }
132
+ if _, dup := seen[pred]; dup {
133
+ return nil, fmt.Errorf("duplicate predecessor %q", pred)
134
+ }
135
+ seen[pred] = struct{}{}
136
+ }
137
+ if parent := sequenceParent(root, slug); parent != "" && parent != predecessors[len(predecessors)-1] {
138
+ return nil, fmt.Errorf("%s records sequence_parent %q; pass the full ordered predecessor list ending with it (or no list to walk the recorded chain)", slug, parent)
139
+ }
140
+ manifests := make([][]candidateRow, 0, len(predecessors))
141
+ for _, pred := range predecessors {
142
+ rows, err := predecessorManifestRows(root, pred)
143
+ if err != nil {
144
+ return nil, fmt.Errorf("predecessor %s: %w", pred, err)
145
+ }
146
+ manifests = append(manifests, rows)
147
+ }
148
+ return manifests, nil
149
+ }
150
+
151
+ // walkSequenceChain follows the target workspace's sequence_parent links to the
152
+ // chain head, returning predecessors oldest-first with their manifests. Every
153
+ // link must resolve to a live or archived workspace carrying the cursor field;
154
+ // a broken link refuses the merge rather than producing a partial union.
155
+ func walkSequenceChain(root, slug, workspace string) ([]string, [][]candidateRow, error) {
156
+ visited := map[string]struct{}{slug: {}}
157
+ chain := []string{}
158
+ manifests := [][]candidateRow{}
159
+ dir := workspace
160
+ for len(chain) < maxSequenceChain {
161
+ raw, err := readBoundedRegularFile(filepath.Join(dir, "state.md"), maxCandidateManifestBytes)
162
+ if err != nil {
163
+ return nil, nil, fmt.Errorf("%s state.md: %w", filepath.Base(dir), err)
164
+ }
165
+ parent, _ := state.CursorField(strings.Split(string(raw), "\n"), state.CursorSequenceParent)
166
+ if parent == "" {
167
+ break
168
+ }
169
+ if _, dup := visited[parent]; dup {
170
+ return nil, nil, fmt.Errorf("sequence_parent cycle at %q", parent)
171
+ }
172
+ visited[parent] = struct{}{}
173
+ rows, err := predecessorManifestRows(root, parent)
174
+ if err != nil {
175
+ return nil, nil, fmt.Errorf("sequence_parent %s: %w", parent, err)
176
+ }
177
+ dir, err = devritespaths.ExistingFeatureDirChecked(root, parent)
178
+ if errors.Is(err, os.ErrNotExist) {
179
+ dir, err = devritespaths.ExistingArchivedFeatureDirChecked(root, parent)
180
+ }
181
+ if err != nil {
182
+ return nil, nil, fmt.Errorf("sequence_parent %s: %w", parent, err)
183
+ }
184
+ chain = append(chain, parent)
185
+ manifests = append(manifests, rows)
186
+ }
187
+ if len(chain) == 0 {
188
+ return nil, nil, fmt.Errorf("%s has no sequence_parent; pass an explicit ordered predecessor list", slug)
189
+ }
190
+ for i, j := 0, len(chain)-1; i < j; i, j = i+1, j-1 {
191
+ chain[i], chain[j] = chain[j], chain[i]
192
+ manifests[i], manifests[j] = manifests[j], manifests[i]
193
+ }
194
+ return chain, manifests, nil
195
+ }
196
+
197
+ func sequenceParent(root, slug string) string {
198
+ dir, err := devritespaths.ExistingFeatureDirChecked(root, slug)
199
+ if err != nil {
200
+ return ""
201
+ }
202
+ raw, err := readBoundedRegularFile(filepath.Join(dir, "state.md"), maxCandidateManifestBytes)
203
+ if err != nil {
204
+ return ""
205
+ }
206
+ parent, _ := state.CursorField(strings.Split(string(raw), "\n"), state.CursorSequenceParent)
207
+ return parent
208
+ }
209
+
210
+ // VerifyReleaseUnion enforces the release-milestone contract at the candidate
211
+ // and seal gates: a workspace whose cursor declares `sequence_role: release`
212
+ // must carry a candidate manifest covering every predecessor in its recorded
213
+ // `sequence_parent` chain. Workspaces without the role are unaffected.
214
+ func VerifyReleaseUnion(root, slug string) error {
215
+ dir, err := devritespaths.ExistingFeatureDirChecked(root, slug)
216
+ if err != nil {
217
+ return fmt.Errorf("workspace for %s: %w", slug, err)
218
+ }
219
+ raw, err := readBoundedRegularFile(filepath.Join(dir, "state.md"), maxCandidateManifestBytes)
220
+ if errors.Is(err, os.ErrNotExist) {
221
+ return nil // no state.md: nothing declares the release role
222
+ }
223
+ if err != nil {
224
+ return fmt.Errorf("%s state.md: %w", slug, err)
225
+ }
226
+ role, _ := state.CursorField(strings.Split(string(raw), "\n"), state.CursorSequenceRole)
227
+ if role != "release" {
228
+ return nil
229
+ }
230
+ _, manifests, err := walkSequenceChain(root, slug, dir)
231
+ if err != nil {
232
+ return err
233
+ }
234
+ manifestRaw, err := readBoundedRegularFile(filepath.Join(dir, "touched-files.md"), maxCandidateManifestBytes)
235
+ if err != nil {
236
+ return fmt.Errorf("candidate manifest: %w", err)
237
+ }
238
+ rows, err := parseCandidateManifest(manifestRaw)
239
+ if err != nil {
240
+ return fmt.Errorf("candidate manifest: %w", err)
241
+ }
242
+ declared := make(map[string]struct{}, len(rows))
243
+ for _, row := range rows {
244
+ declared[row.path] = struct{}{}
245
+ }
246
+ var missing []string
247
+ for _, ancestor := range manifests {
248
+ for _, row := range ancestor {
249
+ if _, ok := declared[row.path]; !ok {
250
+ missing = append(missing, row.path)
251
+ }
252
+ }
253
+ }
254
+ if len(missing) > 0 {
255
+ sort.Strings(missing)
256
+ const show = 8
257
+ list := missing
258
+ if len(list) > show {
259
+ list = append(append([]string(nil), missing[:show]...), fmt.Sprintf("…and %d more", len(missing)-show))
260
+ }
261
+ return fmt.Errorf("release manifest does not cover the recorded sequence chain; run `devrites-engine state merge-manifest %s`. Missing: %s", slug, strings.Join(list, ", "))
262
+ }
263
+ return nil
264
+ }
265
+
266
+ // predecessorManifestRows loads the candidate manifest of a sealed or archived
267
+ // predecessor workspace. Live workspaces win over archive entries so an
268
+ // unshipped sequence member is always found first.
269
+ func predecessorManifestRows(root, slug string) ([]candidateRow, error) {
270
+ dir, err := devritespaths.ExistingFeatureDirChecked(root, slug)
271
+ if errors.Is(err, os.ErrNotExist) {
272
+ dir, err = devritespaths.ExistingArchivedFeatureDirChecked(root, slug)
273
+ }
274
+ if err != nil {
275
+ return nil, fmt.Errorf("no live or archived workspace: %w", err)
276
+ }
277
+ raw, err := readBoundedRegularFile(filepath.Join(dir, "touched-files.md"), maxCandidateManifestBytes)
278
+ if err != nil {
279
+ return nil, fmt.Errorf("candidate manifest: %w", err)
280
+ }
281
+ rows, err := parseCandidateManifest(raw)
282
+ if err != nil {
283
+ return nil, fmt.Errorf("candidate manifest: %w", err)
284
+ }
285
+ return rows, nil
286
+ }
287
+
288
+ // replaceManifestSectionBody rewrites the body of the single
289
+ // `## Candidate manifest` section, keeping every other byte of the file —
290
+ // headings, `## Touched files` narrative, and `## Review trail` — intact.
291
+ func replaceManifestSectionBody(raw []byte, body []string) ([]byte, error) {
292
+ text := string(raw)
293
+ crlf := strings.Contains(text, "\r\n")
294
+ lines := strings.Split(text, "\n")
295
+ start, count := -1, 0
296
+ for i, line := range lines {
297
+ if strings.TrimSpace(line) == "## Candidate manifest" {
298
+ start = i + 1
299
+ count++
300
+ }
301
+ }
302
+ if count != 1 {
303
+ return nil, errors.New("requires exactly one ## Candidate manifest section")
304
+ }
305
+ end := len(lines)
306
+ for i := start; i < len(lines); i++ {
307
+ if strings.HasPrefix(strings.TrimSpace(lines[i]), "## ") {
308
+ end = i
309
+ break
310
+ }
311
+ }
312
+ emitted := append([]string(nil), body...)
313
+ if crlf {
314
+ for i := range emitted {
315
+ emitted[i] += "\r"
316
+ }
317
+ }
318
+ if end < len(lines) {
319
+ emitted = append(emitted, "")
320
+ if crlf {
321
+ emitted[len(emitted)-1] = "\r"
322
+ }
323
+ }
324
+ lines = append(lines[:start], append(emitted, lines[end:]...)...)
325
+ return []byte(strings.Join(lines, "\n")), nil
326
+ }
@@ -0,0 +1,214 @@
1
+ package lib
2
+
3
+ import (
4
+ "bytes"
5
+ "os"
6
+ "path/filepath"
7
+ "strings"
8
+ "testing"
9
+ )
10
+
11
+ const unionManifest = `# Touched files: SLUG
12
+
13
+ ## Touched files
14
+ SLUG scope.
15
+
16
+ ## Candidate manifest
17
+ | State | File | Slice | Reason |
18
+ | --- | --- | --- | --- |
19
+ ROWS
20
+ ## Review trail
21
+ SLUG trail.
22
+ `
23
+
24
+ func manifestBody(rows ...string) string {
25
+ return strings.Join(rows, "\n") + "\n"
26
+ }
27
+
28
+ func unionWorkspace(t *testing.T, root, slug, rows string, archived bool) {
29
+ t.Helper()
30
+ parent := "work"
31
+ if archived {
32
+ parent = "archive"
33
+ }
34
+ dir := filepath.Join(root, parent, slug)
35
+ if err := os.MkdirAll(dir, 0o755); err != nil {
36
+ t.Fatal(err)
37
+ }
38
+ stateBody := "| phase | seal |\n| schema | 3 |\n"
39
+ if err := os.WriteFile(filepath.Join(dir, "state.md"), []byte(stateBody), 0o644); err != nil {
40
+ t.Fatal(err)
41
+ }
42
+ manifest := strings.ReplaceAll(unionManifest, "SLUG", slug)
43
+ manifest = strings.Replace(manifest, "ROWS\n", rows, 1)
44
+ if err := os.WriteFile(filepath.Join(dir, "touched-files.md"), []byte(manifest), 0o644); err != nil {
45
+ t.Fatal(err)
46
+ }
47
+ }
48
+
49
+ func unionRoot(t *testing.T) string {
50
+ root := filepath.Join(t.TempDir(), ".devrites")
51
+ if err := os.MkdirAll(filepath.Join(root, "work"), 0o755); err != nil {
52
+ t.Fatal(err)
53
+ }
54
+ return root
55
+ }
56
+
57
+ func TestMergeManifestUnionsOrderedPredecessors(t *testing.T) {
58
+ root := unionRoot(t)
59
+ unionWorkspace(t, root, "feat-1", manifestBody(
60
+ "| present | `src/a.ts` | SLICE-001 | Added. |",
61
+ "| present | `src/shared.ts` | SLICE-001 | First write. |",
62
+ ), false)
63
+ unionWorkspace(t, root, "feat-2", manifestBody(
64
+ "| present | `src/b.ts` | SLICE-002 | Added. |",
65
+ "| present | `src/shared.ts` | SLICE-002 | Rewritten. |",
66
+ ), true) // archived predecessor still resolves
67
+ unionWorkspace(t, root, "release", manifestBody(
68
+ "| present | `src/r.ts` | SLICE-001 | Release wiring. |",
69
+ ), false)
70
+
71
+ var stdout, stderr bytes.Buffer
72
+ code := RunMergeManifest(root, []string{"release", "feat-1", "feat-2"}, &stdout, &stderr)
73
+ if code != 0 {
74
+ t.Fatalf("code=%d stderr=%s", code, stderr.String())
75
+ }
76
+ raw, err := os.ReadFile(filepath.Join(root, "work", "release", "touched-files.md"))
77
+ if err != nil {
78
+ t.Fatal(err)
79
+ }
80
+ text := string(raw)
81
+ want := []string{
82
+ "| present | `src/a.ts` | SLICE-001 | Added. |",
83
+ "| present | `src/b.ts` | SLICE-002 | Added. |",
84
+ "| present | `src/r.ts` | SLICE-001 | Release wiring. |",
85
+ "| present | `src/shared.ts` | SLICE-002 | Rewritten. |", // later position wins
86
+ }
87
+ for _, row := range want {
88
+ if !strings.Contains(text, row) {
89
+ t.Fatalf("merged manifest missing %q:\n%s", row, text)
90
+ }
91
+ }
92
+ if strings.Contains(text, "First write.") {
93
+ t.Fatalf("losing collision row survived:\n%s", text)
94
+ }
95
+ if !strings.Contains(text, "## Touched files\nrelease scope.") || !strings.Contains(text, "## Review trail\nrelease trail.") {
96
+ t.Fatalf("non-manifest sections were not preserved:\n%s", text)
97
+ }
98
+ if !strings.Contains(stdout.String(), "collision") {
99
+ t.Fatalf("stdout should report the resolved collision: %q", stdout.String())
100
+ }
101
+ }
102
+
103
+ func TestMergeManifestWalksSequenceChain(t *testing.T) {
104
+ root := unionRoot(t)
105
+ unionWorkspace(t, root, "feat-1", manifestBody("| present | `src/a.ts` | SLICE-001 | Added. |"), false)
106
+ unionWorkspace(t, root, "feat-2", manifestBody(
107
+ "| present | `src/a.ts` | SLICE-002 | Touched again. |",
108
+ "| present | `src/b.ts` | SLICE-002 | Added. |",
109
+ ), false)
110
+ unionWorkspace(t, root, "release", manifestBody("| present | `src/r.ts` | SLICE-001 | Release. |"), false)
111
+ for slug, parent := range map[string]string{"feat-2": "feat-1", "release": "feat-2"} {
112
+ statePath := filepath.Join(root, "work", slug, "state.md")
113
+ raw, _ := os.ReadFile(statePath)
114
+ raw = append(raw, []byte("| sequence_parent | "+parent+" |\n")...)
115
+ if err := os.WriteFile(statePath, raw, 0o644); err != nil {
116
+ t.Fatal(err)
117
+ }
118
+ }
119
+
120
+ var stdout, stderr bytes.Buffer
121
+ if code := RunMergeManifest(root, []string{"release"}, &stdout, &stderr); code != 0 {
122
+ t.Fatalf("chain walk: code=%d stderr=%s", code, stderr.String())
123
+ }
124
+ raw, err := os.ReadFile(filepath.Join(root, "work", "release", "touched-files.md"))
125
+ if err != nil {
126
+ t.Fatal(err)
127
+ }
128
+ text := string(raw)
129
+ for _, row := range []string{
130
+ "| present | `src/a.ts` | SLICE-002 | Touched again. |",
131
+ "| present | `src/b.ts` | SLICE-002 | Added. |",
132
+ "| present | `src/r.ts` | SLICE-001 | Release. |",
133
+ } {
134
+ if !strings.Contains(text, row) {
135
+ t.Fatalf("chain-merged manifest missing %q:\n%s", row, text)
136
+ }
137
+ }
138
+
139
+ // A workspace outside the chain has nothing to walk.
140
+ stderr.Reset()
141
+ unionWorkspace(t, root, "lone", manifestBody("| present | `src/x.ts` | SLICE-001 | Lone. |"), false)
142
+ if code := RunMergeManifest(root, []string{"lone"}, &stdout, &stderr); code != 3 {
143
+ t.Fatalf("no chain: code=%d stderr=%s", code, stderr.String())
144
+ }
145
+ }
146
+
147
+ func TestVerifyReleaseUnionEnforcesChainCoverage(t *testing.T) {
148
+ root := unionRoot(t)
149
+ unionWorkspace(t, root, "feat-1", manifestBody("| present | `src/a.ts` | SLICE-001 | Added. |"), false)
150
+ unionWorkspace(t, root, "release", manifestBody("| present | `src/r.ts` | SLICE-001 | Release. |"), false)
151
+ for slug, extra := range map[string]string{
152
+ "release": "| sequence_parent | feat-1 |\n| sequence_role | release |\n",
153
+ "feat-1": "",
154
+ } {
155
+ if extra == "" {
156
+ continue
157
+ }
158
+ statePath := filepath.Join(root, "work", slug, "state.md")
159
+ raw, _ := os.ReadFile(statePath)
160
+ if err := os.WriteFile(statePath, append(raw, []byte(extra)...), 0o644); err != nil {
161
+ t.Fatal(err)
162
+ }
163
+ }
164
+
165
+ // A release workspace whose manifest misses a predecessor path is blocked.
166
+ if err := VerifyReleaseUnion(root, "release"); err == nil || !strings.Contains(err.Error(), "merge-manifest") {
167
+ t.Fatalf("expected union gap, got %v", err)
168
+ }
169
+ // Workspaces without the role are not checked.
170
+ if err := VerifyReleaseUnion(root, "feat-1"); err != nil {
171
+ t.Fatalf("non-release workspace: %v", err)
172
+ }
173
+
174
+ // After the merge, coverage holds.
175
+ var stdout, stderr bytes.Buffer
176
+ if code := RunMergeManifest(root, []string{"release"}, &stdout, &stderr); code != 0 {
177
+ t.Fatalf("merge: code=%d stderr=%s", code, stderr.String())
178
+ }
179
+ if err := VerifyReleaseUnion(root, "release"); err != nil {
180
+ t.Fatalf("post-merge: %v", err)
181
+ }
182
+ }
183
+
184
+ func TestMergeManifestRejectsParentMismatchAndMissingPredecessor(t *testing.T) {
185
+ root := unionRoot(t)
186
+ unionWorkspace(t, root, "feat-1", manifestBody("| present | `src/a.ts` | SLICE-001 | Added. |"), false)
187
+ unionWorkspace(t, root, "release", manifestBody("| present | `src/r.ts` | SLICE-001 | Release. |"), false)
188
+ statePath := filepath.Join(root, "work", "release", "state.md")
189
+ raw, _ := os.ReadFile(statePath)
190
+ raw = append(raw, []byte("| sequence_parent | feat-1 |\n")...)
191
+ if err := os.WriteFile(statePath, raw, 0o644); err != nil {
192
+ t.Fatal(err)
193
+ }
194
+
195
+ var stdout, stderr bytes.Buffer
196
+ // The recorded parent must be the last predecessor; omitting it blocks.
197
+ if code := RunMergeManifest(root, []string{"release", "other"}, &stdout, &stderr); code != 3 {
198
+ t.Fatalf("parent mismatch: code=%d stderr=%s", code, stderr.String())
199
+ }
200
+ if !strings.Contains(stderr.String(), "sequence_parent") {
201
+ t.Fatalf("stderr=%q", stderr.String())
202
+ }
203
+ stderr.Reset()
204
+ if code := RunMergeManifest(root, []string{"release", "feat-1", "ghost"}, &stdout, &stderr); code != 3 {
205
+ t.Fatalf("missing predecessor: code=%d stderr=%s", code, stderr.String())
206
+ }
207
+ stderr.Reset()
208
+ if code := RunMergeManifest(root, []string{"release", "release", "feat-1"}, &stdout, &stderr); code != 3 {
209
+ t.Fatalf("self predecessor: code=%d", code)
210
+ }
211
+ if code := RunMergeManifest(root, []string{}, &stdout, &stderr); code != 2 {
212
+ t.Fatalf("missing args: code=%d", code)
213
+ }
214
+ }
@@ -0,0 +1,73 @@
1
+ package lib
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "sort"
7
+
8
+ "github.com/devrites/devrites/internal/state"
9
+ )
10
+
11
+ const bulkFileThreshold = 64 << 10
12
+
13
+ // ArtifactBudget reports one canonical workspace artifact against its budget.
14
+ type ArtifactBudget struct {
15
+ File string `json:"file"`
16
+ Lines int `json:"lines"`
17
+ Bytes int64 `json:"bytes"`
18
+ LineBudget int `json:"line_budget"`
19
+ ByteBudget int64 `json:"byte_budget"`
20
+ Over bool `json:"over"`
21
+ Override bool `json:"override,omitempty"`
22
+ }
23
+
24
+ // BulkFile is a non-canonical regular file in the workspace root above bulkFileThreshold.
25
+ type BulkFile struct {
26
+ File string `json:"file"`
27
+ Bytes int64 `json:"bytes"`
28
+ }
29
+
30
+ // observeArtifactBudgets lists canonical artifacts present in the workspace root with
31
+ // their budget status, plus non-canonical files large enough to hurt a reader.
32
+ func observeArtifactBudgets(workspace string) ([]ArtifactBudget, []BulkFile, error) {
33
+ entries, err := os.ReadDir(workspace)
34
+ if err != nil {
35
+ return nil, nil, err
36
+ }
37
+ var budgets []ArtifactBudget
38
+ var bulk []BulkFile
39
+ for _, entry := range entries {
40
+ if !entry.Type().IsRegular() {
41
+ continue
42
+ }
43
+ info, err := entry.Info()
44
+ if err != nil {
45
+ return nil, nil, err
46
+ }
47
+ name := entry.Name()
48
+ if _, canonical := state.ArtifactLineBudget(name); !canonical {
49
+ if info.Size() > bulkFileThreshold {
50
+ bulk = append(bulk, BulkFile{File: name, Bytes: info.Size()})
51
+ }
52
+ continue
53
+ }
54
+ // #nosec G304 -- canonical workspace artifact name from a fixed table
55
+ raw, err := os.ReadFile(filepath.Join(workspace, name))
56
+ if err != nil {
57
+ return nil, nil, err
58
+ }
59
+ measured, _ := state.ArtifactBudget(name, raw)
60
+ budgets = append(budgets, ArtifactBudget{
61
+ File: name,
62
+ Lines: measured.Lines,
63
+ Bytes: measured.Bytes,
64
+ LineBudget: measured.LineBudget,
65
+ ByteBudget: measured.ByteBudget,
66
+ Over: measured.Over,
67
+ Override: measured.Override,
68
+ })
69
+ }
70
+ sort.Slice(budgets, func(i, j int) bool { return budgets[i].File < budgets[j].File })
71
+ sort.Slice(bulk, func(i, j int) bool { return bulk[i].File < bulk[j].File })
72
+ return budgets, bulk, nil
73
+ }