devrites 5.4.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.
- package/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/docs/command-map.md +2 -2
- package/docs/engine/state-schema.md +12 -0
- package/docs/flow.md +4 -0
- package/docs/orchestration.md +5 -2
- package/engine/commands.go +2 -2
- package/engine/internal/devritespaths/paths.go +15 -0
- package/engine/internal/gate/gate.go +78 -0
- package/engine/internal/gate/gate_test.go +118 -0
- package/engine/internal/lib/candidate.go +7 -3
- package/engine/internal/lib/manifest_union.go +326 -0
- package/engine/internal/lib/manifest_union_test.go +214 -0
- package/engine/internal/lib/observe_budgets.go +73 -0
- package/engine/internal/lib/observe_budgets_test.go +142 -0
- package/engine/internal/lib/observe_summary.go +74 -13
- package/engine/internal/lib/workspace_read.go +11 -6
- package/engine/internal/state/budgets.go +163 -0
- package/engine/internal/state/budgets_test.go +42 -0
- package/engine/internal/state/cursor.go +16 -10
- package/engine/internal/state/observation.go +50 -4
- package/engine/internal/state/status.go +38 -26
- package/engine/internal/state/taskgraph.go +23 -0
- package/engine/main.go +32 -13
- package/engine/root_routing_test.go +3 -1
- package/pack/.claude/agents/devrites-code-reviewer.md +9 -6
- package/pack/.claude/agents/devrites-doubt-reviewer.md +6 -0
- package/pack/.claude/agents/devrites-plan-reviewer.md +7 -1
- package/pack/.claude/agents/devrites-slice-wright.md +42 -46
- package/pack/.claude/agents/devrites-test-analyst.md +9 -4
- package/pack/.claude/agents/devrites-upgrade-planner.md +9 -1
- package/pack/.claude/settings.json +17 -0
- package/pack/.claude/skills/devrites-api-interface/SKILL.md +2 -2
- package/pack/.claude/skills/devrites-doubt/SKILL.md +9 -17
- package/pack/.claude/skills/devrites-lib/reference/candidate-integrity.md +34 -35
- package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +11 -12
- package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +104 -51
- package/pack/.claude/skills/devrites-lib/reference/standards/agents.md +1 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/context-hygiene.md +45 -25
- package/pack/.claude/skills/devrites-lib/reference/standards/core.md +1 -1
- package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +80 -41
- package/pack/.claude/skills/rite-autocomplete/SKILL.md +19 -8
- package/pack/.claude/skills/rite-autocomplete/reference/loop.md +33 -1
- package/pack/.claude/skills/rite-build/SKILL.md +26 -19
- package/pack/.claude/skills/rite-build/reference/afk-discipline.md +1 -1
- package/pack/.claude/skills/rite-build/reference/parallel-batch.md +90 -40
- package/pack/.claude/skills/rite-build/reference/phase-contract.md +79 -59
- package/pack/.claude/skills/rite-build/reference/spec-drift-guard.md +14 -15
- package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +8 -8
- package/pack/.claude/skills/rite-define/SKILL.md +4 -1
- package/pack/.claude/skills/rite-plan/SKILL.md +4 -1
- package/pack/.claude/skills/rite-plan/reference/replan-and-repair.md +21 -0
- package/pack/.claude/skills/rite-plan/reference/slicing.md +117 -15
- package/pack/.claude/skills/rite-prove/SKILL.md +20 -22
- package/pack/.claude/skills/rite-review/SKILL.md +23 -16
- package/pack/.claude/skills/rite-seal/SKILL.md +8 -8
- package/pack/.claude/skills/rite-seal/reference/final-evidence.md +6 -5
- package/pack/.claude/skills/rite-seal/reference/phase-contract.md +13 -13
- package/pack/.claude/skills/rite-ship/SKILL.md +3 -0
- package/pack/.claude/skills/rite-ship/reference/close-out.md +16 -2
- package/pack/.claude/skills/rite-ship/reference/git-ship.md +14 -5
- package/pack/.claude/skills/rite-spec/SKILL.md +6 -0
- package/pack/.claude/skills/rite-spec/reference/state-workspace.md +6 -0
- package/pack/.claude/skills/rite-status/SKILL.md +4 -1
- package/pack/.claude/skills/rite-temper/SKILL.md +14 -11
- package/pack/.claude/skills/rite-temper/reference/anti-patterns.md +14 -13
- package/pack/.claude/skills/rite-temper/reference/review-dimensions.md +3 -4
- package/pack/.claude/skills/rite-upgrade/SKILL.md +11 -2
- package/pack/.claude/skills/rite-vet/SKILL.md +22 -15
- package/pack/.claude/skills/rite-vet/reference/anti-patterns.md +11 -9
- package/pack/.claude/skills/rite-vet/reference/depth.md +6 -7
- package/pack/.claude/skills/rite-vet/reference/review-axes.md +39 -14
- package/pack/generated/claude/agents/devrites-code-reviewer.md +9 -6
- package/pack/generated/claude/agents/devrites-doubt-reviewer.md +6 -0
- package/pack/generated/claude/agents/devrites-plan-reviewer.md +7 -1
- package/pack/generated/claude/agents/devrites-slice-wright.md +42 -46
- package/pack/generated/claude/agents/devrites-test-analyst.md +9 -4
- package/pack/generated/claude/agents/devrites-upgrade-planner.md +9 -1
- package/pack/generated/claude/settings.json +17 -0
- package/pack/generated/claude/skills/devrites-api-interface/SKILL.md +2 -2
- package/pack/generated/claude/skills/devrites-doubt/SKILL.md +9 -17
- package/pack/generated/claude/skills/devrites-lib/reference/candidate-integrity.md +34 -35
- package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +11 -12
- package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +104 -51
- package/pack/generated/claude/skills/devrites-lib/reference/standards/agents.md +1 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/context-hygiene.md +45 -25
- package/pack/generated/claude/skills/devrites-lib/reference/standards/core.md +1 -1
- package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +80 -41
- package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +19 -8
- package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +33 -1
- package/pack/generated/claude/skills/rite-build/SKILL.md +26 -19
- package/pack/generated/claude/skills/rite-build/reference/afk-discipline.md +1 -1
- package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +90 -40
- package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +79 -59
- package/pack/generated/claude/skills/rite-build/reference/spec-drift-guard.md +14 -15
- package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +8 -8
- package/pack/generated/claude/skills/rite-define/SKILL.md +4 -1
- package/pack/generated/claude/skills/rite-plan/SKILL.md +4 -1
- package/pack/generated/claude/skills/rite-plan/reference/replan-and-repair.md +21 -0
- package/pack/generated/claude/skills/rite-plan/reference/slicing.md +117 -15
- package/pack/generated/claude/skills/rite-prove/SKILL.md +20 -22
- package/pack/generated/claude/skills/rite-review/SKILL.md +23 -16
- package/pack/generated/claude/skills/rite-seal/SKILL.md +8 -8
- package/pack/generated/claude/skills/rite-seal/reference/final-evidence.md +6 -5
- package/pack/generated/claude/skills/rite-seal/reference/phase-contract.md +13 -13
- package/pack/generated/claude/skills/rite-ship/SKILL.md +3 -0
- package/pack/generated/claude/skills/rite-ship/reference/close-out.md +16 -2
- package/pack/generated/claude/skills/rite-ship/reference/git-ship.md +14 -5
- package/pack/generated/claude/skills/rite-spec/SKILL.md +6 -0
- package/pack/generated/claude/skills/rite-spec/reference/state-workspace.md +6 -0
- package/pack/generated/claude/skills/rite-status/SKILL.md +4 -1
- package/pack/generated/claude/skills/rite-temper/SKILL.md +14 -11
- package/pack/generated/claude/skills/rite-temper/reference/anti-patterns.md +14 -13
- package/pack/generated/claude/skills/rite-temper/reference/review-dimensions.md +3 -4
- package/pack/generated/claude/skills/rite-upgrade/SKILL.md +11 -2
- package/pack/generated/claude/skills/rite-vet/SKILL.md +22 -15
- package/pack/generated/claude/skills/rite-vet/reference/anti-patterns.md +11 -9
- package/pack/generated/claude/skills/rite-vet/reference/depth.md +6 -7
- package/pack/generated/claude/skills/rite-vet/reference/review-axes.md +39 -14
- package/pack/generated/codex/agents/devrites-code-reviewer.toml +9 -6
- package/pack/generated/codex/agents/devrites-doubt-reviewer.toml +6 -0
- package/pack/generated/codex/agents/devrites-plan-reviewer.toml +7 -1
- package/pack/generated/codex/agents/devrites-slice-wright.toml +42 -46
- package/pack/generated/codex/agents/devrites-test-analyst.toml +9 -4
- package/pack/generated/codex/agents/devrites-upgrade-planner.toml +9 -1
- package/pack/generated/codex/skills/devrites-api-interface/SKILL.md +2 -2
- package/pack/generated/codex/skills/devrites-doubt/SKILL.md +9 -17
- package/pack/generated/codex/skills/devrites-lib/reference/candidate-integrity.md +34 -35
- package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +11 -12
- package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +104 -51
- package/pack/generated/codex/skills/devrites-lib/reference/standards/agents.md +1 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/context-hygiene.md +45 -25
- package/pack/generated/codex/skills/devrites-lib/reference/standards/core.md +1 -1
- package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +80 -41
- package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +19 -8
- package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +33 -1
- package/pack/generated/codex/skills/rite-build/SKILL.md +26 -19
- package/pack/generated/codex/skills/rite-build/reference/afk-discipline.md +1 -1
- package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +90 -40
- package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +79 -59
- package/pack/generated/codex/skills/rite-build/reference/spec-drift-guard.md +14 -15
- package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +8 -8
- package/pack/generated/codex/skills/rite-define/SKILL.md +4 -1
- package/pack/generated/codex/skills/rite-plan/SKILL.md +4 -1
- package/pack/generated/codex/skills/rite-plan/reference/replan-and-repair.md +21 -0
- package/pack/generated/codex/skills/rite-plan/reference/slicing.md +117 -15
- package/pack/generated/codex/skills/rite-prove/SKILL.md +20 -22
- package/pack/generated/codex/skills/rite-review/SKILL.md +23 -16
- package/pack/generated/codex/skills/rite-seal/SKILL.md +8 -8
- package/pack/generated/codex/skills/rite-seal/reference/final-evidence.md +6 -5
- package/pack/generated/codex/skills/rite-seal/reference/phase-contract.md +13 -13
- package/pack/generated/codex/skills/rite-ship/SKILL.md +3 -0
- package/pack/generated/codex/skills/rite-ship/reference/close-out.md +16 -2
- package/pack/generated/codex/skills/rite-ship/reference/git-ship.md +14 -5
- package/pack/generated/codex/skills/rite-spec/SKILL.md +6 -0
- package/pack/generated/codex/skills/rite-spec/reference/state-workspace.md +6 -0
- package/pack/generated/codex/skills/rite-status/SKILL.md +4 -1
- package/pack/generated/codex/skills/rite-temper/SKILL.md +14 -11
- package/pack/generated/codex/skills/rite-temper/reference/anti-patterns.md +14 -13
- package/pack/generated/codex/skills/rite-temper/reference/review-dimensions.md +3 -4
- package/pack/generated/codex/skills/rite-upgrade/SKILL.md +11 -2
- package/pack/generated/codex/skills/rite-vet/SKILL.md +22 -15
- package/pack/generated/codex/skills/rite-vet/reference/anti-patterns.md +11 -9
- package/pack/generated/codex/skills/rite-vet/reference/depth.md +6 -7
- package/pack/generated/codex/skills/rite-vet/reference/review-axes.md +39 -14
- package/pack/generated/omp/agents/devrites-code-reviewer.md +9 -6
- package/pack/generated/omp/agents/devrites-doubt-reviewer.md +6 -0
- package/pack/generated/omp/agents/devrites-plan-reviewer.md +7 -1
- package/pack/generated/omp/agents/devrites-slice-wright.md +42 -46
- package/pack/generated/omp/agents/devrites-test-analyst.md +9 -4
- package/pack/generated/omp/agents/devrites-upgrade-planner.md +9 -1
- package/pack/generated/omp/skills/devrites-api-interface/SKILL.md +2 -2
- package/pack/generated/omp/skills/devrites-doubt/SKILL.md +9 -17
- package/pack/generated/omp/skills/devrites-lib/reference/candidate-integrity.md +34 -35
- package/pack/generated/omp/skills/devrites-lib/reference/parallel-dispatch.md +11 -12
- package/pack/generated/omp/skills/devrites-lib/reference/standards/afk-hitl.md +104 -51
- package/pack/generated/omp/skills/devrites-lib/reference/standards/agents.md +1 -0
- package/pack/generated/omp/skills/devrites-lib/reference/standards/context-hygiene.md +45 -25
- package/pack/generated/omp/skills/devrites-lib/reference/standards/core.md +1 -1
- package/pack/generated/omp/skills/devrites-lib/reference/workspace-artifact-schema.md +80 -41
- package/pack/generated/omp/skills/rite-autocomplete/SKILL.md +19 -8
- package/pack/generated/omp/skills/rite-autocomplete/reference/loop.md +33 -1
- package/pack/generated/omp/skills/rite-build/SKILL.md +26 -19
- package/pack/generated/omp/skills/rite-build/reference/afk-discipline.md +1 -1
- package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +90 -40
- package/pack/generated/omp/skills/rite-build/reference/phase-contract.md +79 -59
- package/pack/generated/omp/skills/rite-build/reference/spec-drift-guard.md +14 -15
- package/pack/generated/omp/skills/rite-build/reference/wright-dispatch.md +8 -8
- package/pack/generated/omp/skills/rite-define/SKILL.md +4 -1
- package/pack/generated/omp/skills/rite-plan/SKILL.md +4 -1
- package/pack/generated/omp/skills/rite-plan/reference/replan-and-repair.md +21 -0
- package/pack/generated/omp/skills/rite-plan/reference/slicing.md +117 -15
- package/pack/generated/omp/skills/rite-prove/SKILL.md +20 -22
- package/pack/generated/omp/skills/rite-review/SKILL.md +23 -16
- package/pack/generated/omp/skills/rite-seal/SKILL.md +8 -8
- package/pack/generated/omp/skills/rite-seal/reference/final-evidence.md +6 -5
- package/pack/generated/omp/skills/rite-seal/reference/phase-contract.md +13 -13
- package/pack/generated/omp/skills/rite-ship/SKILL.md +3 -0
- package/pack/generated/omp/skills/rite-ship/reference/close-out.md +16 -2
- package/pack/generated/omp/skills/rite-ship/reference/git-ship.md +14 -5
- package/pack/generated/omp/skills/rite-spec/SKILL.md +6 -0
- package/pack/generated/omp/skills/rite-spec/reference/state-workspace.md +6 -0
- package/pack/generated/omp/skills/rite-status/SKILL.md +4 -1
- package/pack/generated/omp/skills/rite-temper/SKILL.md +14 -11
- package/pack/generated/omp/skills/rite-temper/reference/anti-patterns.md +14 -13
- package/pack/generated/omp/skills/rite-temper/reference/review-dimensions.md +3 -4
- package/pack/generated/omp/skills/rite-upgrade/SKILL.md +11 -2
- package/pack/generated/omp/skills/rite-vet/SKILL.md +22 -15
- package/pack/generated/omp/skills/rite-vet/reference/anti-patterns.md +11 -9
- package/pack/generated/omp/skills/rite-vet/reference/depth.md +6 -7
- package/pack/generated/omp/skills/rite-vet/reference/review-axes.md +39 -14
- package/package.json +1 -1
- 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
|
+
}
|