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.
- package/CHANGELOG.md +12 -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 -17
- package/pack/.claude/skills/rite-build/reference/afk-discipline.md +1 -1
- package/pack/.claude/skills/rite-build/reference/parallel-batch.md +101 -39
- 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 -17
- package/pack/generated/claude/skills/rite-build/reference/afk-discipline.md +1 -1
- package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +101 -39
- 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 -17
- package/pack/generated/codex/skills/rite-build/reference/afk-discipline.md +1 -1
- package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +101 -39
- 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 -17
- package/pack/generated/omp/skills/rite-build/reference/afk-discipline.md +1 -1
- package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +101 -39
- 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,142 @@
|
|
|
1
|
+
package lib
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"os"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"strings"
|
|
8
|
+
"testing"
|
|
9
|
+
|
|
10
|
+
"github.com/devrites/devrites/internal/state"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
const budgetTasks = `# Tasks
|
|
14
|
+
|
|
15
|
+
Current checkpoint: intro text.
|
|
16
|
+
|
|
17
|
+
## SLICE-001 First
|
|
18
|
+
Goal: one
|
|
19
|
+
Dependencies: none
|
|
20
|
+
depends_on: []
|
|
21
|
+
|
|
22
|
+
### Notes
|
|
23
|
+
nested heading stays inside the slice
|
|
24
|
+
|
|
25
|
+
## SLICE-002 Second
|
|
26
|
+
Goal: two
|
|
27
|
+
Dependencies: SLICE-001
|
|
28
|
+
depends_on: [SLICE-001]
|
|
29
|
+
|
|
30
|
+
## Trailing section
|
|
31
|
+
not a slice
|
|
32
|
+
`
|
|
33
|
+
|
|
34
|
+
func writeBudgetWorkspace(t *testing.T, files map[string]string) (root, workspace string) {
|
|
35
|
+
t.Helper()
|
|
36
|
+
root = filepath.Join(t.TempDir(), ".devrites")
|
|
37
|
+
workspace = filepath.Join(root, "work", "feature")
|
|
38
|
+
if err := os.MkdirAll(workspace, 0o755); err != nil {
|
|
39
|
+
t.Fatal(err)
|
|
40
|
+
}
|
|
41
|
+
for name, body := range files {
|
|
42
|
+
if err := os.WriteFile(filepath.Join(workspace, name), []byte(body), 0o644); err != nil {
|
|
43
|
+
t.Fatal(err)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return root, workspace
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func TestExtractTaskSliceStopsAtNextH2AndKeepsH3(t *testing.T) {
|
|
50
|
+
block, ok := state.ExtractTaskSlice([]byte(budgetTasks), "SLICE-001")
|
|
51
|
+
if !ok {
|
|
52
|
+
t.Fatal("expected SLICE-001")
|
|
53
|
+
}
|
|
54
|
+
if !strings.HasPrefix(block, "## SLICE-001 First\n") || !strings.Contains(block, "### Notes") {
|
|
55
|
+
t.Fatalf("block=%q", block)
|
|
56
|
+
}
|
|
57
|
+
if strings.Contains(block, "SLICE-002") || strings.Contains(block, "Trailing") {
|
|
58
|
+
t.Fatalf("block leaked past the next heading: %q", block)
|
|
59
|
+
}
|
|
60
|
+
last, ok := state.ExtractTaskSlice([]byte(budgetTasks), "SLICE-002")
|
|
61
|
+
if !ok || strings.Contains(last, "Trailing section") {
|
|
62
|
+
t.Fatalf("last=%q ok=%v", last, ok)
|
|
63
|
+
}
|
|
64
|
+
if _, ok := state.ExtractTaskSlice([]byte(budgetTasks), "SLICE-003"); ok {
|
|
65
|
+
t.Fatal("SLICE-003 must not resolve")
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
func TestRunObserveSlicePrintsOneSection(t *testing.T) {
|
|
70
|
+
root, _ := writeBudgetWorkspace(t, map[string]string{"tasks.md": budgetTasks})
|
|
71
|
+
var stdout, stderr bytes.Buffer
|
|
72
|
+
if code := RunObserveSlice(root, "feature", "SLICE-002", &stdout, &stderr); code != 0 {
|
|
73
|
+
t.Fatalf("code=%d stderr=%s", code, stderr.String())
|
|
74
|
+
}
|
|
75
|
+
if got := stdout.String(); !strings.HasPrefix(got, "## SLICE-002 Second\n") || strings.Contains(got, "SLICE-001 First") {
|
|
76
|
+
t.Fatalf("stdout=%q", got)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
stdout.Reset()
|
|
80
|
+
if code := RunObserveSlice(root, "feature", "SLICE-009", &stdout, &stderr); code != 3 || !strings.Contains(stdout.String(), "BLOCKED") {
|
|
81
|
+
t.Fatalf("code=%d stdout=%q", code, stdout.String())
|
|
82
|
+
}
|
|
83
|
+
if code := RunObserveSlice(root, "feature", "slice-1", &stdout, &stderr); code != 2 {
|
|
84
|
+
t.Fatalf("code=%d for malformed id", code)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func TestObserveSummaryReportsBudgetsAndBulkFiles(t *testing.T) {
|
|
89
|
+
longLine := strings.Repeat("x", 60_000) + "\n"
|
|
90
|
+
root, _ := writeBudgetWorkspace(t, map[string]string{
|
|
91
|
+
"state.md": "| phase | build |\n| schema | 3 |\n" + longLine,
|
|
92
|
+
"tasks.md": budgetTasks,
|
|
93
|
+
"vet-review-input-1.json": strings.Repeat("{}", 40_000),
|
|
94
|
+
"small-note.json": "{}",
|
|
95
|
+
})
|
|
96
|
+
summary, err := ObserveSummaryFor(root, "feature")
|
|
97
|
+
if err != nil {
|
|
98
|
+
t.Fatal(err)
|
|
99
|
+
}
|
|
100
|
+
byFile := map[string]ArtifactBudget{}
|
|
101
|
+
for _, b := range summary.ArtifactBudgets {
|
|
102
|
+
byFile[b.File] = b
|
|
103
|
+
}
|
|
104
|
+
if st, ok := byFile["state.md"]; !ok || !st.Over || st.Lines != 3 || st.ByteBudget != 120*state.BudgetBytesPerLine {
|
|
105
|
+
t.Fatalf("state.md budget=%+v ok=%v (long line must trip the byte budget)", st, ok)
|
|
106
|
+
}
|
|
107
|
+
if tk, ok := byFile["tasks.md"]; !ok || tk.Over {
|
|
108
|
+
t.Fatalf("tasks.md budget=%+v ok=%v", tk, ok)
|
|
109
|
+
}
|
|
110
|
+
if summary.TaskGraph == nil || len(summary.TaskGraph.Slices) != 2 ||
|
|
111
|
+
summary.TaskGraph.Slices[0].ID != "SLICE-001" || len(summary.TaskGraph.Slices[0].DependsOn) != 0 ||
|
|
112
|
+
summary.TaskGraph.Slices[1].DependsOn[0] != "SLICE-001" {
|
|
113
|
+
t.Fatalf("task_graph.slices=%+v", summary.TaskGraph)
|
|
114
|
+
}
|
|
115
|
+
if len(summary.BulkFiles) != 1 || summary.BulkFiles[0].File != "vet-review-input-1.json" {
|
|
116
|
+
t.Fatalf("bulk_files=%+v", summary.BulkFiles)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
func TestObserveSummaryReportsSequenceCursor(t *testing.T) {
|
|
121
|
+
root, _ := writeBudgetWorkspace(t, map[string]string{
|
|
122
|
+
"state.md": "| phase | build |\n| schema | 3 |\n| sequence_parent | feat-1 |\n| sequence_position | 2 |\n| sequence_workspaces_remaining | 3 |\n",
|
|
123
|
+
})
|
|
124
|
+
summary, err := ObserveSummaryFor(root, "feature")
|
|
125
|
+
if err != nil {
|
|
126
|
+
t.Fatal(err)
|
|
127
|
+
}
|
|
128
|
+
seq := summary.Sequence
|
|
129
|
+
if seq == nil || seq.Parent != "feat-1" || seq.Position != "2" || seq.WorkspacesRemaining != "3" {
|
|
130
|
+
t.Fatalf("sequence=%+v", seq)
|
|
131
|
+
}
|
|
132
|
+
root, _ = writeBudgetWorkspace(t, map[string]string{
|
|
133
|
+
"state.md": "| phase | build |\n| schema | 3 |\n",
|
|
134
|
+
})
|
|
135
|
+
summary, err = ObserveSummaryFor(root, "feature")
|
|
136
|
+
if err != nil {
|
|
137
|
+
t.Fatal(err)
|
|
138
|
+
}
|
|
139
|
+
if summary.Sequence != nil {
|
|
140
|
+
t.Fatalf("sequence must be absent without cursor fields: %+v", summary.Sequence)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -2,30 +2,50 @@ package lib
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"encoding/json"
|
|
5
|
+
"fmt"
|
|
5
6
|
"io"
|
|
6
7
|
|
|
8
|
+
"github.com/devrites/devrites/internal/devritespaths"
|
|
7
9
|
"github.com/devrites/devrites/internal/state"
|
|
8
10
|
)
|
|
9
11
|
|
|
12
|
+
// ObserveSlice is one slice's identity and ordering edges, enough for a root to pick
|
|
13
|
+
// the next buildable slice without loading tasks.md.
|
|
14
|
+
type ObserveSlice struct {
|
|
15
|
+
ID string `json:"id"`
|
|
16
|
+
DependsOn []string `json:"depends_on"`
|
|
17
|
+
}
|
|
18
|
+
|
|
10
19
|
// ObserveTaskGraph is the slice-graph subset of ObserveSummary.
|
|
11
20
|
type ObserveTaskGraph struct {
|
|
12
|
-
SliceCount int
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
21
|
+
SliceCount int `json:"slice_count"`
|
|
22
|
+
Slices []ObserveSlice `json:"slices,omitempty"`
|
|
23
|
+
Cycle []string `json:"cycle,omitempty"`
|
|
24
|
+
Unknown []string `json:"unknown_dependencies,omitempty"`
|
|
25
|
+
Problems []string `json:"problems,omitempty"`
|
|
26
|
+
OK bool `json:"ok"`
|
|
17
27
|
}
|
|
18
28
|
|
|
19
29
|
// ObserveSummary is a sanitized, machine-readable workspace snapshot.
|
|
30
|
+
type ObserveSequenceCursor struct {
|
|
31
|
+
Parent string `json:"parent"`
|
|
32
|
+
Position string `json:"position"`
|
|
33
|
+
WorkspacesRemaining string `json:"workspaces_remaining"`
|
|
34
|
+
Role string `json:"role,omitempty"`
|
|
35
|
+
}
|
|
36
|
+
|
|
20
37
|
type ObserveSummary struct {
|
|
21
|
-
Slug string
|
|
22
|
-
Phase string
|
|
23
|
-
Status string
|
|
24
|
-
NextAction string
|
|
25
|
-
MissingSections []string
|
|
26
|
-
MissingFiles []string
|
|
27
|
-
PrinciplesPresent bool
|
|
28
|
-
TaskGraph *ObserveTaskGraph
|
|
38
|
+
Slug string `json:"slug"`
|
|
39
|
+
Phase string `json:"phase,omitempty"`
|
|
40
|
+
Status string `json:"status,omitempty"`
|
|
41
|
+
NextAction string `json:"next_action,omitempty"`
|
|
42
|
+
MissingSections []string `json:"missing_sections,omitempty"`
|
|
43
|
+
MissingFiles []string `json:"missing_files,omitempty"`
|
|
44
|
+
PrinciplesPresent bool `json:"principles_present"`
|
|
45
|
+
TaskGraph *ObserveTaskGraph `json:"task_graph,omitempty"`
|
|
46
|
+
ArtifactBudgets []ArtifactBudget `json:"artifact_budgets,omitempty"`
|
|
47
|
+
BulkFiles []BulkFile `json:"bulk_files,omitempty"`
|
|
48
|
+
Sequence *ObserveSequenceCursor `json:"sequence,omitempty"`
|
|
29
49
|
}
|
|
30
50
|
|
|
31
51
|
// ObserveSummaryFor builds a summary for one feature slug.
|
|
@@ -45,17 +65,58 @@ func ObserveSummaryFor(root, slug string) (ObserveSummary, error) {
|
|
|
45
65
|
}
|
|
46
66
|
|
|
47
67
|
if graph, graphErr := CheckTaskGraph(root, slug); graphErr == nil && (len(graph.Slices) > 0 || len(graph.Problems) > 0) {
|
|
68
|
+
slices := make([]ObserveSlice, 0, len(graph.Slices))
|
|
69
|
+
for _, slice := range graph.Slices {
|
|
70
|
+
deps := make([]string, 0, len(slice.Dependencies))
|
|
71
|
+
deps = append(deps, slice.Dependencies...)
|
|
72
|
+
slices = append(slices, ObserveSlice{ID: slice.ID, DependsOn: deps})
|
|
73
|
+
}
|
|
48
74
|
summary.TaskGraph = &ObserveTaskGraph{
|
|
49
75
|
SliceCount: len(graph.Slices),
|
|
76
|
+
Slices: slices,
|
|
50
77
|
Cycle: append([]string(nil), graph.Cycle...),
|
|
51
78
|
Unknown: append([]string(nil), graph.Unknown...),
|
|
52
79
|
Problems: append([]string(nil), graph.Problems...),
|
|
53
80
|
OK: len(graph.Problems) == 0,
|
|
54
81
|
}
|
|
55
82
|
}
|
|
83
|
+
if report.SequenceParent != "" || report.SequencePosition != "" || report.SequenceWorkspacesRemaining != "" || report.SequenceRole != "" {
|
|
84
|
+
summary.Sequence = &ObserveSequenceCursor{
|
|
85
|
+
Parent: report.SequenceParent,
|
|
86
|
+
Position: report.SequencePosition,
|
|
87
|
+
WorkspacesRemaining: report.SequenceWorkspacesRemaining,
|
|
88
|
+
Role: report.SequenceRole,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if workspace, wsErr := devritespaths.ExistingFeatureDirChecked(root, slug); wsErr == nil {
|
|
92
|
+
if budgets, bulk, budgetErr := observeArtifactBudgets(workspace); budgetErr == nil {
|
|
93
|
+
summary.ArtifactBudgets = budgets
|
|
94
|
+
summary.BulkFiles = bulk
|
|
95
|
+
}
|
|
96
|
+
}
|
|
56
97
|
return summary, nil
|
|
57
98
|
}
|
|
58
99
|
|
|
100
|
+
// RunObserveSlice prints one SLICE-### section of tasks.md for a slug.
|
|
101
|
+
func RunObserveSlice(root, slug, id string, stdout, stderr io.Writer) int {
|
|
102
|
+
if !state.ValidSliceID(id) {
|
|
103
|
+
fmt.Fprintf(stderr, "observe slice: %q is not a SLICE-### id\n", id)
|
|
104
|
+
return 2
|
|
105
|
+
}
|
|
106
|
+
raw, err := readWorkspaceArtifact(root, slug, "tasks.md")
|
|
107
|
+
if err != nil {
|
|
108
|
+
fmt.Fprintf(stderr, "observe slice: %v\n", err)
|
|
109
|
+
return 2
|
|
110
|
+
}
|
|
111
|
+
block, ok := state.ExtractTaskSlice(raw, id)
|
|
112
|
+
if !ok {
|
|
113
|
+
fmt.Fprintf(stdout, "observe slice: BLOCKED: %s not found in tasks.md\n", id)
|
|
114
|
+
return 3
|
|
115
|
+
}
|
|
116
|
+
fmt.Fprint(stdout, block)
|
|
117
|
+
return 0
|
|
118
|
+
}
|
|
119
|
+
|
|
59
120
|
func missingSectionNames(sections []state.Section) []string {
|
|
60
121
|
if len(sections) == 0 {
|
|
61
122
|
return nil
|
|
@@ -8,23 +8,28 @@ import (
|
|
|
8
8
|
"github.com/devrites/devrites/internal/devritespaths"
|
|
9
9
|
)
|
|
10
10
|
|
|
11
|
+
// workspaceArtifactLimit bounds one artifact read. Slice extraction must keep
|
|
12
|
+
// working on a workspace whose tasks.md is over budget: the budget gate flags the
|
|
13
|
+
// file, while Build still reads its own slice instead of the whole document.
|
|
14
|
+
const workspaceArtifactLimit = 16 << 20
|
|
15
|
+
|
|
11
16
|
func readWorkspaceArtifact(root, slug, name string) ([]byte, error) {
|
|
12
17
|
workspace, err := devritespaths.ExistingFeatureDirChecked(root, slug)
|
|
13
18
|
if err != nil {
|
|
14
|
-
return nil, fmt.Errorf("
|
|
19
|
+
return nil, fmt.Errorf("workspace artifact: workspace unavailable: %w", err)
|
|
15
20
|
}
|
|
16
21
|
path := filepath.Join(workspace, name)
|
|
17
22
|
info, err := os.Stat(path)
|
|
18
23
|
if err != nil {
|
|
19
|
-
return nil, fmt.Errorf("
|
|
24
|
+
return nil, fmt.Errorf("workspace artifact: %s unavailable: %w", name, err)
|
|
20
25
|
}
|
|
21
|
-
if info.Size() >
|
|
22
|
-
return nil, fmt.Errorf("
|
|
26
|
+
if info.Size() > workspaceArtifactLimit {
|
|
27
|
+
return nil, fmt.Errorf("workspace artifact: %s exceeds %d MiB limit", name, workspaceArtifactLimit>>20)
|
|
23
28
|
}
|
|
24
|
-
// #nosec G304 -- workspace
|
|
29
|
+
// #nosec G304 -- workspace artifact; size cap checked above
|
|
25
30
|
raw, err := os.ReadFile(path)
|
|
26
31
|
if err != nil {
|
|
27
|
-
return nil, fmt.Errorf("
|
|
32
|
+
return nil, fmt.Errorf("workspace artifact: cannot read %s: %w", name, err)
|
|
28
33
|
}
|
|
29
34
|
return raw, nil
|
|
30
35
|
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
package state
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"strings"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
// Artifact budgets mirror workspace-artifact-schema.md "What each file owns".
|
|
9
|
+
// A line budget also bounds bytes at BudgetBytesPerLine per budgeted line so a
|
|
10
|
+
// few very long lines cannot evade it.
|
|
11
|
+
const (
|
|
12
|
+
BudgetBytesPerLine = 400
|
|
13
|
+
// BudgetBlockFactor is the material-overshoot threshold. At or above this
|
|
14
|
+
// multiple of a line or byte budget a gate blocks unless the artifact
|
|
15
|
+
// records a structural `Budget override:` line; smaller overshoots are
|
|
16
|
+
// reported by `orient` and stay advisory.
|
|
17
|
+
BudgetBlockFactor = 1.5
|
|
18
|
+
// MaxArtifactBytes is the per-artifact read cap. A larger artifact cannot be
|
|
19
|
+
// observed and blocks as file_too_large, so callers that explain a size
|
|
20
|
+
// problem quote the same number the observer enforced.
|
|
21
|
+
MaxArtifactBytes = maxArtifactBytes
|
|
22
|
+
// PacketMaxBytes is the serialized-packet ceiling from context-hygiene.md:
|
|
23
|
+
// "A serialized packet stays <= 64 KiB; larger means the scope is wrong".
|
|
24
|
+
// A non-canonical workspace-root file over it is unsanctioned placement.
|
|
25
|
+
PacketMaxBytes = 64 << 10
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
var artifactLineBudgets = map[string]int{
|
|
29
|
+
"README.md": 120,
|
|
30
|
+
"brief.md": 80,
|
|
31
|
+
"spec.md": 260,
|
|
32
|
+
"decision-coverage.md": 200,
|
|
33
|
+
"strategy.md": 180,
|
|
34
|
+
"architecture.md": 180,
|
|
35
|
+
"flows.md": 160,
|
|
36
|
+
"decisions.md": 200,
|
|
37
|
+
"assumptions.md": 160,
|
|
38
|
+
"questions.md": 180,
|
|
39
|
+
"plan.md": 220,
|
|
40
|
+
"tasks.md": 280,
|
|
41
|
+
"traceability.md": 220,
|
|
42
|
+
"eng-review.md": 240,
|
|
43
|
+
"test-plan.md": 260,
|
|
44
|
+
"state.md": 120,
|
|
45
|
+
"evidence.md": 280,
|
|
46
|
+
"browser-evidence.md": 220,
|
|
47
|
+
"drift.md": 160,
|
|
48
|
+
"touched-files.md": 160,
|
|
49
|
+
"design-brief.md": 160,
|
|
50
|
+
"handoff.md": 120,
|
|
51
|
+
"polish-report.md": 200,
|
|
52
|
+
"ship.md": 120,
|
|
53
|
+
"review.md": 240,
|
|
54
|
+
"seal.md": 200,
|
|
55
|
+
"ai-spec.md": 160,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// advisoryOnlyArtifacts are append-only proof ledgers: they scale with proof
|
|
59
|
+
// volume, not with planning surface, so their budget stays advisory in `orient`
|
|
60
|
+
// and never blocks a gate. touched-files.md keeps its own deterministic
|
|
61
|
+
// manifest limits.
|
|
62
|
+
var advisoryOnlyArtifacts = map[string]bool{
|
|
63
|
+
"evidence.md": true,
|
|
64
|
+
"browser-evidence.md": true,
|
|
65
|
+
"touched-files.md": true,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ArtifactLineBudget returns the documented line budget for a canonical artifact.
|
|
69
|
+
func ArtifactLineBudget(name string) (int, bool) {
|
|
70
|
+
budget, ok := artifactLineBudgets[name]
|
|
71
|
+
return budget, ok
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// sanctionedRootFiles are workspace-root files the schema places there without a
|
|
75
|
+
// line budget: conditional artifacts (`references.md`, `investigation-map.md`,
|
|
76
|
+
// `dogfood.md`) and the engine-owned parallel lease. Anything else in the root
|
|
77
|
+
// is unsanctioned placement — `packets/` and `history/` own by-reference
|
|
78
|
+
// packets and relocated narrative (workspace-artifact-schema.md "Required by
|
|
79
|
+
// phase").
|
|
80
|
+
var sanctionedRootFiles = map[string]bool{
|
|
81
|
+
"references.md": true,
|
|
82
|
+
"investigation-map.md": true,
|
|
83
|
+
"dogfood.md": true,
|
|
84
|
+
"parallel-lease.md": true,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// SanctionedRootFile reports whether name is a legitimate workspace-root file:
|
|
88
|
+
// a budgeted canonical artifact or a documented unbudgeted one.
|
|
89
|
+
func SanctionedRootFile(name string) bool {
|
|
90
|
+
if _, ok := artifactLineBudgets[name]; ok {
|
|
91
|
+
return true
|
|
92
|
+
}
|
|
93
|
+
return sanctionedRootFiles[name]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ArtifactBudgetGateApplies reports whether the deterministic material-overshoot
|
|
97
|
+
// gate covers this artifact (canonical and not an advisory-only proof ledger).
|
|
98
|
+
func ArtifactBudgetGateApplies(name string) bool {
|
|
99
|
+
_, ok := artifactLineBudgets[name]
|
|
100
|
+
return ok && !advisoryOnlyArtifacts[name]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ArtifactBudgetStatus is one canonical artifact measured against its budget.
|
|
104
|
+
type ArtifactBudgetStatus struct {
|
|
105
|
+
File string
|
|
106
|
+
Lines int
|
|
107
|
+
Bytes int64
|
|
108
|
+
LineBudget int
|
|
109
|
+
ByteBudget int64
|
|
110
|
+
Over bool
|
|
111
|
+
Material bool
|
|
112
|
+
Override bool
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ArtifactBudget measures raw against the named artifact's budget. ok is false
|
|
116
|
+
// for a non-canonical file name.
|
|
117
|
+
func ArtifactBudget(name string, raw []byte) (ArtifactBudgetStatus, bool) {
|
|
118
|
+
lineBudget, ok := artifactLineBudgets[name]
|
|
119
|
+
if !ok {
|
|
120
|
+
return ArtifactBudgetStatus{}, false
|
|
121
|
+
}
|
|
122
|
+
status := ArtifactBudgetStatus{
|
|
123
|
+
File: name,
|
|
124
|
+
Lines: CountLines(raw),
|
|
125
|
+
Bytes: int64(len(raw)),
|
|
126
|
+
LineBudget: lineBudget,
|
|
127
|
+
ByteBudget: int64(lineBudget) * BudgetBytesPerLine,
|
|
128
|
+
}
|
|
129
|
+
status.Over = status.Lines > lineBudget || status.Bytes > status.ByteBudget
|
|
130
|
+
status.Material = float64(status.Lines) >= float64(lineBudget)*BudgetBlockFactor ||
|
|
131
|
+
float64(status.Bytes) >= float64(status.ByteBudget)*BudgetBlockFactor
|
|
132
|
+
status.Override = HasBudgetOverride(raw)
|
|
133
|
+
return status, true
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// CountLines counts newline-terminated lines plus a final unterminated line.
|
|
137
|
+
func CountLines(raw []byte) int {
|
|
138
|
+
if len(raw) == 0 {
|
|
139
|
+
return 0
|
|
140
|
+
}
|
|
141
|
+
lines := bytes.Count(raw, []byte{'\n'})
|
|
142
|
+
if raw[len(raw)-1] != '\n' {
|
|
143
|
+
lines++
|
|
144
|
+
}
|
|
145
|
+
return lines
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// HasBudgetOverride reports a structural `Budget override:` line outside fenced
|
|
149
|
+
// code blocks; a fenced example never counts.
|
|
150
|
+
func HasBudgetOverride(raw []byte) bool {
|
|
151
|
+
fenced := false
|
|
152
|
+
for _, line := range strings.Split(string(raw), "\n") {
|
|
153
|
+
trimmed := strings.TrimSpace(line)
|
|
154
|
+
if strings.HasPrefix(trimmed, "```") {
|
|
155
|
+
fenced = !fenced
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
if !fenced && strings.HasPrefix(trimmed, "Budget override:") {
|
|
159
|
+
return true
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return false
|
|
163
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
package state
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"strings"
|
|
5
|
+
"testing"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
func TestArtifactBudgetMeasuresLinesBytesAndMateriality(t *testing.T) {
|
|
9
|
+
if _, ok := ArtifactBudget("notes.json", []byte("x")); ok {
|
|
10
|
+
t.Fatal("non-canonical files have no budget")
|
|
11
|
+
}
|
|
12
|
+
within, ok := ArtifactBudget("tasks.md", []byte("# Tasks\n## SLICE-001 A\nDependencies: none\n"))
|
|
13
|
+
if !ok || within.Over || within.Material {
|
|
14
|
+
t.Fatalf("within=%+v", within)
|
|
15
|
+
}
|
|
16
|
+
over, _ := ArtifactBudget("tasks.md", []byte(strings.Repeat("x\n", 300)))
|
|
17
|
+
if !over.Over || over.Material {
|
|
18
|
+
t.Fatalf("300 lines against a 280-line budget must be over but not material: %+v", over)
|
|
19
|
+
}
|
|
20
|
+
material, _ := ArtifactBudget("tasks.md", []byte(strings.Repeat("x\n", 500)))
|
|
21
|
+
if !material.Material {
|
|
22
|
+
t.Fatalf("500 lines must be material: %+v", material)
|
|
23
|
+
}
|
|
24
|
+
longLine, _ := ArtifactBudget("state.md", []byte(strings.Repeat("y", 100_000)+"\n"))
|
|
25
|
+
if !longLine.Material || longLine.Lines != 1 {
|
|
26
|
+
t.Fatalf("one very long line must trip the byte budget: %+v", longLine)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func TestHasBudgetOverrideIgnoresFencedExamples(t *testing.T) {
|
|
31
|
+
if HasBudgetOverride([]byte("plain text\nBudget override: relocation pending\n")) != true {
|
|
32
|
+
t.Fatal("structural override line must be recognized")
|
|
33
|
+
}
|
|
34
|
+
fenced := "See the schema:\n\n```markdown\nBudget override: <reason>\n```\n\nNothing recorded.\n"
|
|
35
|
+
if HasBudgetOverride([]byte(fenced)) {
|
|
36
|
+
t.Fatal("a fenced example is not an override")
|
|
37
|
+
}
|
|
38
|
+
indented := " Budget override: table form is required\n"
|
|
39
|
+
if !HasBudgetOverride([]byte(indented)) {
|
|
40
|
+
t.Fatal("leading whitespace must not hide a real override line")
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -12,15 +12,19 @@ import (
|
|
|
12
12
|
// remain local to the cursor package; normalizeCursorKey still accepts legacy
|
|
13
13
|
// presentation aliases at the text boundary.
|
|
14
14
|
const (
|
|
15
|
-
CursorPhase
|
|
16
|
-
CursorStatus
|
|
17
|
-
CursorNextAction
|
|
18
|
-
CursorQuestionID
|
|
19
|
-
CursorActiveSlice
|
|
20
|
-
CursorAFKSlicesRemaining
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
CursorPhase = "phase"
|
|
16
|
+
CursorStatus = "status"
|
|
17
|
+
CursorNextAction = "next_action"
|
|
18
|
+
CursorQuestionID = "question_id"
|
|
19
|
+
CursorActiveSlice = "active_slice"
|
|
20
|
+
CursorAFKSlicesRemaining = "afk_slices_remaining"
|
|
21
|
+
CursorSequenceParent = "sequence_parent"
|
|
22
|
+
CursorSequencePosition = "sequence_position"
|
|
23
|
+
CursorSequenceWorkspacesRemain = "sequence_workspaces_remaining"
|
|
24
|
+
CursorSequenceRole = "sequence_role"
|
|
25
|
+
CursorReturnPhase = "return_phase"
|
|
26
|
+
CursorReturnNextAction = "return_next_action"
|
|
27
|
+
CursorSchema = "schema"
|
|
24
28
|
)
|
|
25
29
|
|
|
26
30
|
var cursorKeyAliases = map[string]string{
|
|
@@ -273,7 +277,9 @@ func isMigratableCursorKey(key string) bool {
|
|
|
273
277
|
func canonicalCursorSpelling(key string) (string, bool) {
|
|
274
278
|
normalized := normalizeCursorKey(key)
|
|
275
279
|
canonicals := []string{CursorPhase, CursorStatus, CursorNextAction, CursorQuestionID,
|
|
276
|
-
CursorActiveSlice, CursorAFKSlicesRemaining,
|
|
280
|
+
CursorActiveSlice, CursorAFKSlicesRemaining, CursorSequenceParent,
|
|
281
|
+
CursorSequencePosition, CursorSequenceWorkspacesRemain, CursorSequenceRole,
|
|
282
|
+
CursorReturnPhase,
|
|
277
283
|
CursorReturnNextAction, CursorSchema}
|
|
278
284
|
for _, canonical := range canonicals {
|
|
279
285
|
if normalized == normalizeCursorKey(canonical) {
|
|
@@ -8,6 +8,7 @@ import (
|
|
|
8
8
|
"os"
|
|
9
9
|
"path"
|
|
10
10
|
"path/filepath"
|
|
11
|
+
"sort"
|
|
11
12
|
"strings"
|
|
12
13
|
|
|
13
14
|
"github.com/devrites/devrites/internal/devritespaths"
|
|
@@ -90,9 +91,22 @@ func (f ArtifactFact) Diagnostic() (ArtifactDiagnostic, bool) {
|
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
type WorkspaceObservation struct {
|
|
93
|
-
slug
|
|
94
|
-
facts
|
|
95
|
-
index
|
|
94
|
+
slug string
|
|
95
|
+
facts []ArtifactFact
|
|
96
|
+
index map[ArtifactPath]int
|
|
97
|
+
rootFiles []RootFile
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// RootFile is one regular file directly inside the workspace root.
|
|
101
|
+
type RootFile struct {
|
|
102
|
+
Name string
|
|
103
|
+
Bytes int64
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// RootFiles lists the workspace root's regular files so a consumer can judge
|
|
107
|
+
// placement without acquiring the directory itself.
|
|
108
|
+
func (o *WorkspaceObservation) RootFiles() []RootFile {
|
|
109
|
+
return append([]RootFile(nil), o.rootFiles...)
|
|
96
110
|
}
|
|
97
111
|
|
|
98
112
|
func (o *WorkspaceObservation) Slug() string { return o.slug }
|
|
@@ -296,7 +310,39 @@ func observeWorkspace(root, slug string, callback observationCallback) (*Workspa
|
|
|
296
310
|
for i, fact := range facts {
|
|
297
311
|
index[fact.path] = i
|
|
298
312
|
}
|
|
299
|
-
|
|
313
|
+
rootFiles, err := readRootFiles(roots.workspace)
|
|
314
|
+
if err != nil {
|
|
315
|
+
return nil, ObservationWorkspaceInvalid
|
|
316
|
+
}
|
|
317
|
+
return &WorkspaceObservation{slug: slug, facts: facts, index: index, rootFiles: rootFiles}, nil
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// readRootFiles lists the workspace directory's regular entries through the
|
|
321
|
+
// already-verified root handle, so the list describes the same snapshot as the
|
|
322
|
+
// artifact facts.
|
|
323
|
+
func readRootFiles(root *os.Root) ([]RootFile, error) {
|
|
324
|
+
dir, err := root.Open(".")
|
|
325
|
+
if err != nil {
|
|
326
|
+
return nil, err
|
|
327
|
+
}
|
|
328
|
+
defer func() { _ = dir.Close() }()
|
|
329
|
+
entries, err := dir.ReadDir(-1)
|
|
330
|
+
if err != nil {
|
|
331
|
+
return nil, err
|
|
332
|
+
}
|
|
333
|
+
files := make([]RootFile, 0, len(entries))
|
|
334
|
+
for _, entry := range entries {
|
|
335
|
+
if !entry.Type().IsRegular() {
|
|
336
|
+
continue
|
|
337
|
+
}
|
|
338
|
+
info, err := entry.Info()
|
|
339
|
+
if err != nil {
|
|
340
|
+
return nil, err
|
|
341
|
+
}
|
|
342
|
+
files = append(files, RootFile{Name: entry.Name(), Bytes: info.Size()})
|
|
343
|
+
}
|
|
344
|
+
sort.Slice(files, func(i, j int) bool { return files[i].Name < files[j].Name })
|
|
345
|
+
return files, nil
|
|
300
346
|
}
|
|
301
347
|
|
|
302
348
|
func observationWorkspaceOverrideValid(root, slug string) bool {
|