devrites 4.1.0 → 4.2.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/engine/commands.md +65 -6
- package/engine/go.mod +1 -1
- package/engine/internal/gate/gate.go +154 -46
- package/engine/internal/gate/gate_test.go +290 -2
- package/engine/internal/gate/readiness_binding.go +66 -100
- package/engine/internal/gate/readiness_binding_test.go +126 -3
- package/engine/internal/lib/resolve.go +6 -6
- package/engine/internal/state/cmd/workflowmanifest/main.go +32 -3
- package/engine/internal/state/feature.go +1 -100
- package/engine/internal/state/observation.go +584 -0
- package/engine/internal/state/observation_open_other.go +9 -0
- package/engine/internal/state/observation_open_unix.go +12 -0
- package/engine/internal/state/observation_test.go +960 -0
- package/engine/internal/state/observation_unix_test.go +68 -0
- package/engine/internal/state/schema.go +60 -160
- package/engine/internal/state/state_test.go +489 -101
- package/engine/internal/state/status.go +67 -48
- package/engine/tests/adr_0004_required_by_phase_test.go +16 -13
- package/engine/tests/adr_0011_define_plan_test.go +14 -12
- package/engine/tests/gate_test.go +403 -28
- package/engine/tests/workspace_observation_migration_test.go +1065 -0
- package/pack/.claude/skills/devrites-debug-recovery/SKILL.md +1 -10
- package/pack/.claude/skills/devrites-lib/reference/standards/acceptance-preserving-reslice.md +30 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +2 -9
- package/pack/.claude/skills/devrites-lib/reference/standards/one-shot-actions.md +1 -6
- package/pack/.claude/skills/devrites-lib/reference/standards/workflow-artifacts.md +411 -102
- package/pack/.claude/skills/rite-autocomplete/SKILL.md +98 -146
- package/pack/.claude/skills/rite-autocomplete/reference/decision-policy.md +11 -5
- package/pack/.claude/skills/rite-autocomplete/reference/loop.md +96 -134
- package/pack/.claude/skills/rite-autocomplete/reference/stop-conditions.md +92 -131
- package/pack/.claude/skills/rite-build/SKILL.md +51 -79
- package/pack/.claude/skills/rite-build/reference/phase-contract.md +3 -17
- package/pack/.claude/skills/rite-plan/SKILL.md +17 -17
- package/pack/.claude/skills/rite-plan/reference/anti-patterns.md +11 -1
- package/pack/.claude/skills/rite-plan/reference/replan-and-repair.md +10 -3
- package/pack/.claude/skills/rite-prove/SKILL.md +95 -153
- package/pack/.claude/skills/rite-vet/SKILL.md +121 -174
- package/pack/.claude/skills/rite-vet/reference/anti-patterns.md +14 -2
- package/pack/.claude/skills/rite-vet/reference/artifacts.md +10 -6
- package/pack/.claude/skills/rite-vet/reference/depth.md +10 -3
- package/pack/.claude/skills/rite-vet/reference/review-axes.md +12 -4
- package/pack/generated/claude/skills/devrites-debug-recovery/SKILL.md +1 -10
- package/pack/generated/claude/skills/devrites-lib/reference/standards/acceptance-preserving-reslice.md +30 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +2 -9
- package/pack/generated/claude/skills/devrites-lib/reference/standards/one-shot-actions.md +1 -6
- package/pack/generated/claude/skills/devrites-lib/reference/standards/workflow-artifacts.md +411 -102
- package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +98 -146
- package/pack/generated/claude/skills/rite-autocomplete/reference/decision-policy.md +11 -5
- package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +96 -134
- package/pack/generated/claude/skills/rite-autocomplete/reference/stop-conditions.md +92 -131
- package/pack/generated/claude/skills/rite-build/SKILL.md +51 -79
- package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +3 -17
- package/pack/generated/claude/skills/rite-plan/SKILL.md +17 -17
- package/pack/generated/claude/skills/rite-plan/reference/anti-patterns.md +11 -1
- package/pack/generated/claude/skills/rite-plan/reference/replan-and-repair.md +10 -3
- package/pack/generated/claude/skills/rite-prove/SKILL.md +95 -153
- package/pack/generated/claude/skills/rite-vet/SKILL.md +121 -174
- package/pack/generated/claude/skills/rite-vet/reference/anti-patterns.md +14 -2
- package/pack/generated/claude/skills/rite-vet/reference/artifacts.md +10 -6
- package/pack/generated/claude/skills/rite-vet/reference/depth.md +10 -3
- package/pack/generated/claude/skills/rite-vet/reference/review-axes.md +12 -4
- package/pack/generated/codex/skills/devrites-debug-recovery/SKILL.md +1 -10
- package/pack/generated/codex/skills/devrites-lib/reference/standards/acceptance-preserving-reslice.md +30 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +2 -9
- package/pack/generated/codex/skills/devrites-lib/reference/standards/one-shot-actions.md +1 -6
- package/pack/generated/codex/skills/devrites-lib/reference/standards/workflow-artifacts.md +411 -102
- package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +98 -146
- package/pack/generated/codex/skills/rite-autocomplete/reference/decision-policy.md +11 -5
- package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +96 -134
- package/pack/generated/codex/skills/rite-autocomplete/reference/stop-conditions.md +92 -131
- package/pack/generated/codex/skills/rite-build/SKILL.md +51 -79
- package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +3 -17
- package/pack/generated/codex/skills/rite-plan/SKILL.md +17 -17
- package/pack/generated/codex/skills/rite-plan/reference/anti-patterns.md +11 -1
- package/pack/generated/codex/skills/rite-plan/reference/replan-and-repair.md +10 -3
- package/pack/generated/codex/skills/rite-prove/SKILL.md +95 -153
- package/pack/generated/codex/skills/rite-vet/SKILL.md +121 -174
- package/pack/generated/codex/skills/rite-vet/reference/anti-patterns.md +14 -2
- package/pack/generated/codex/skills/rite-vet/reference/artifacts.md +10 -6
- package/pack/generated/codex/skills/rite-vet/reference/depth.md +10 -3
- package/pack/generated/codex/skills/rite-vet/reference/review-axes.md +12 -4
- package/package.json +1 -1
|
@@ -0,0 +1,1065 @@
|
|
|
1
|
+
package main_test
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"go/ast"
|
|
6
|
+
"go/parser"
|
|
7
|
+
"go/token"
|
|
8
|
+
"os"
|
|
9
|
+
"path/filepath"
|
|
10
|
+
"sort"
|
|
11
|
+
"strconv"
|
|
12
|
+
"strings"
|
|
13
|
+
"testing"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
func TestWorkspaceObservationMigrationGuards(t *testing.T) {
|
|
17
|
+
engineRoot := filepath.Clean("..")
|
|
18
|
+
|
|
19
|
+
t.Run("superseded readers are absent", func(t *testing.T) {
|
|
20
|
+
program := parseMigrationProgram(t, engineRoot)
|
|
21
|
+
for _, problem := range obsoleteSurfaceProblems(program) {
|
|
22
|
+
t.Error(problem)
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
t.Run("acquisition calls are exact", func(t *testing.T) {
|
|
27
|
+
program := parseMigrationProgram(t, engineRoot)
|
|
28
|
+
for _, problem := range acquisitionProblems(program) {
|
|
29
|
+
t.Error(problem)
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
t.Run("consumer reachability stays retained", func(t *testing.T) {
|
|
34
|
+
program := parseMigrationProgram(t, engineRoot)
|
|
35
|
+
for _, problem := range consumerReachabilityProblems(program, migrationConsumerEntries()) {
|
|
36
|
+
t.Error(problem)
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
t.Run("command documentation names closed contract", func(t *testing.T) {
|
|
41
|
+
path := filepath.Join(engineRoot, "..", "docs", "engine", "commands.md")
|
|
42
|
+
content, err := os.ReadFile(path)
|
|
43
|
+
if err != nil {
|
|
44
|
+
t.Fatal(err)
|
|
45
|
+
}
|
|
46
|
+
text := strings.Join(strings.Fields(string(content)), " ")
|
|
47
|
+
for _, want := range []string{
|
|
48
|
+
"`absent`", "`empty`", "`malformed`", "`unsafe`", "`unreadable`", "`present`",
|
|
49
|
+
"`malformed_markdown`", "`parent_symlink`", "`final_symlink`", "`non_regular`",
|
|
50
|
+
"`file_too_large`", "`permission_denied`", "`read_failure`",
|
|
51
|
+
"next: repair <logical-path>: replace invalid Markdown with valid Markdown; required artifacts need substantive content",
|
|
52
|
+
"next: repair <logical-path>: replace the symlinked parent with a real directory",
|
|
53
|
+
"next: repair <logical-path>: replace the symlink with a regular file",
|
|
54
|
+
"next: repair <logical-path>: replace the non-regular entry with a regular file",
|
|
55
|
+
"next: repair <logical-path>: reduce the file to at most 1 MiB",
|
|
56
|
+
"next: repair <logical-path>: grant read permission",
|
|
57
|
+
"next: repair <logical-path>: restore a readable regular file",
|
|
58
|
+
"; optional readiness input may instead be removed",
|
|
59
|
+
"readiness input <logical-path> is malformed (malformed_markdown); replace invalid Markdown with valid Markdown",
|
|
60
|
+
"readiness input <logical-path> is unsafe (parent_symlink); replace the symlinked parent with a real directory",
|
|
61
|
+
"readiness input <logical-path> is unsafe (final_symlink); replace the symlink with a regular file",
|
|
62
|
+
"readiness input <logical-path> is unsafe (non_regular); replace the non-regular entry with a regular file",
|
|
63
|
+
"readiness input <logical-path> is unsafe (file_too_large); reduce the file to at most 1 MiB",
|
|
64
|
+
"readiness input <logical-path> is unreadable (permission_denied); grant read permission",
|
|
65
|
+
"readiness input <logical-path> is unreadable (read_failure); restore a readable regular file",
|
|
66
|
+
"`workspace_invalid`", "`aggregate_too_large`", "`concurrent_change`",
|
|
67
|
+
"workspace observation: workspace_invalid: workspace is unavailable; verify the selected logical workspace and canonical workspace override, then retry",
|
|
68
|
+
"workspace observation: aggregate_too_large: retained content exceeds the 8 MiB aggregate limit; reduce retained Markdown below 8 MiB, then retry",
|
|
69
|
+
"add real content to state.md and retry", "repair state.md and retry",
|
|
70
|
+
"record phase in state.md and retry", "record a known phase in state.md and retry",
|
|
71
|
+
"1 MiB per file", "8 MiB aggregate",
|
|
72
|
+
"artifact: <logical-path>: <state> (<code>)",
|
|
73
|
+
"Status emits diagnostics without recovery or `next:` lines",
|
|
74
|
+
"Gate emits diagnostics after `reason` and before recovery, `invariant`, and `retry` lines",
|
|
75
|
+
"Whole observation failures use stderr, exit `2`, and no lifecycle result or reason on stdout",
|
|
76
|
+
"Standalone readiness-binding failures use one stderr line, exit `3`, and empty stdout",
|
|
77
|
+
"These seven codes are the closed Workspace Observation classification and recovery mapping outcomes. A selected public consumer emits only a code reachable for its consumed fixed logical path. Invalid workspace ancestry is `workspace_invalid`, not an artifact `parent_symlink` diagnostic.",
|
|
78
|
+
} {
|
|
79
|
+
if !strings.Contains(text, want) {
|
|
80
|
+
t.Errorf("%s missing %q", path, want)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
func TestWorkspaceObservationMigrationGuardFixtures(t *testing.T) {
|
|
87
|
+
t.Run("unrelated Present is accepted", func(t *testing.T) {
|
|
88
|
+
program := parseMigrationFixture(t, map[string]string{
|
|
89
|
+
"internal/state/snapshot.go": `package state
|
|
90
|
+
type Snapshot struct {
|
|
91
|
+
Present bool
|
|
92
|
+
}
|
|
93
|
+
func Present() bool { return Snapshot{Present: true}.Present }
|
|
94
|
+
`,
|
|
95
|
+
})
|
|
96
|
+
if problems := obsoleteSurfaceProblems(program); len(problems) != 0 {
|
|
97
|
+
t.Fatalf("unrelated Present identifier was rejected: %v", problems)
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
t.Run("obsolete State and Gate surfaces are rejected", func(t *testing.T) {
|
|
102
|
+
program := parseMigrationFixture(t, map[string]string{
|
|
103
|
+
"internal/state/feature.go": `package state
|
|
104
|
+
type Feature struct {
|
|
105
|
+
Present map[string]bool
|
|
106
|
+
PresentFiles map[string]bool
|
|
107
|
+
}
|
|
108
|
+
func LoadFeature() *Feature { return nil }
|
|
109
|
+
func declaredPhaseFromLedger() {}
|
|
110
|
+
`,
|
|
111
|
+
"internal/gate/gate.go": `package gate
|
|
112
|
+
import lifecycle "github.com/devrites/devrites/internal/state"
|
|
113
|
+
func Check() { _ = lifecycle.LoadFeature }
|
|
114
|
+
`,
|
|
115
|
+
})
|
|
116
|
+
problems := obsoleteSurfaceProblems(program)
|
|
117
|
+
for _, want := range []string{"state type Feature", "Feature.Present", "Feature.PresentFiles", "state function LoadFeature", "state function declaredPhaseFromLedger", "state.LoadFeature"} {
|
|
118
|
+
if !migrationProblemContains(problems, want) {
|
|
119
|
+
t.Errorf("obsolete surface %q was not rejected: %v", want, problems)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
t.Run("reachable helper reopen", func(t *testing.T) {
|
|
125
|
+
program := parseMigrationFixture(t, map[string]string{
|
|
126
|
+
"internal/state/status.go": `package state
|
|
127
|
+
func Status() { statusWithCallback() }
|
|
128
|
+
func statusWithCallback() { reopen().Read() }
|
|
129
|
+
`,
|
|
130
|
+
"internal/state/reopen.go": `package state
|
|
131
|
+
import "os"
|
|
132
|
+
func reopen() any {
|
|
133
|
+
_, _ = os.ReadFile("state.md")
|
|
134
|
+
return nil
|
|
135
|
+
}
|
|
136
|
+
`,
|
|
137
|
+
})
|
|
138
|
+
problems := consumerReachabilityProblems(program, []migrationEntry{{directory: "internal/state", function: "Status"}})
|
|
139
|
+
if !migrationProblemContains(problems, "internal/state/reopen.go", `filesystem content-reader package "os"`) {
|
|
140
|
+
t.Fatalf("reachable helper reopen was not rejected: %v", problems)
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
t.Run("reachable receiver method reopen", func(t *testing.T) {
|
|
145
|
+
program := parseMigrationFixture(t, map[string]string{
|
|
146
|
+
"internal/state/status.go": `package state
|
|
147
|
+
type retainedConsumer struct{}
|
|
148
|
+
func Status() {
|
|
149
|
+
var consumer retainedConsumer
|
|
150
|
+
consumer.reopen()
|
|
151
|
+
}
|
|
152
|
+
`,
|
|
153
|
+
"internal/state/reopen.go": `package state
|
|
154
|
+
import "os"
|
|
155
|
+
func (retainedConsumer) reopen() {
|
|
156
|
+
_, _ = os.ReadFile("state.md")
|
|
157
|
+
}
|
|
158
|
+
`,
|
|
159
|
+
})
|
|
160
|
+
problems := consumerReachabilityProblems(program, []migrationEntry{{directory: "internal/state", function: "Status"}})
|
|
161
|
+
if !migrationProblemContains(problems, "internal/state/reopen.go", `filesystem content-reader package "os"`) {
|
|
162
|
+
t.Fatalf("reachable receiver-method reopen was not rejected: %v", problems)
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
for _, test := range []struct {
|
|
167
|
+
name string
|
|
168
|
+
body string
|
|
169
|
+
}{
|
|
170
|
+
{name: "direct constructor result receiver", body: "newRetainedConsumer().reopen()"},
|
|
171
|
+
{name: "assigned constructor result receiver", body: "consumer := newRetainedConsumer()\nconsumer.reopen()"},
|
|
172
|
+
} {
|
|
173
|
+
t.Run(test.name, func(t *testing.T) {
|
|
174
|
+
program := parseMigrationFixture(t, map[string]string{
|
|
175
|
+
"internal/state/status.go": fmt.Sprintf(`package state
|
|
176
|
+
type retainedConsumer struct{}
|
|
177
|
+
func newRetainedConsumer() *retainedConsumer { return &retainedConsumer{} }
|
|
178
|
+
func Status() {
|
|
179
|
+
%s
|
|
180
|
+
}
|
|
181
|
+
`, test.body),
|
|
182
|
+
"internal/state/reopen.go": `package state
|
|
183
|
+
import "os"
|
|
184
|
+
func (*retainedConsumer) reopen() {
|
|
185
|
+
_, _ = os.ReadFile("state.md")
|
|
186
|
+
}
|
|
187
|
+
`,
|
|
188
|
+
})
|
|
189
|
+
problems := consumerReachabilityProblems(program, []migrationEntry{{directory: "internal/state", function: "Status"}})
|
|
190
|
+
if !migrationProblemContains(problems, "internal/state/reopen.go", `filesystem content-reader package "os"`) {
|
|
191
|
+
t.Fatalf("constructor-result receiver method was not rejected: %v", problems)
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
t.Run("reachable internal helper package", func(t *testing.T) {
|
|
197
|
+
program := parseMigrationFixture(t, map[string]string{
|
|
198
|
+
"internal/gate/gate.go": `package gate
|
|
199
|
+
import "github.com/devrites/devrites/internal/workspacereader"
|
|
200
|
+
func Check() { workspacereader.Read() }
|
|
201
|
+
`,
|
|
202
|
+
})
|
|
203
|
+
problems := consumerReachabilityProblems(program, []migrationEntry{{directory: "internal/gate", function: "Check"}})
|
|
204
|
+
if !migrationProblemContains(problems, "internal/gate/gate.go", `unapproved internal package "github.com/devrites/devrites/internal/workspacereader"`) {
|
|
205
|
+
t.Fatalf("reachable internal helper package was not rejected: %v", problems)
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
t.Run("aliased helper second capture", func(t *testing.T) {
|
|
210
|
+
program := parseMigrationFixture(t, map[string]string{
|
|
211
|
+
"internal/gate/gate.go": `package gate
|
|
212
|
+
import observed "github.com/devrites/devrites/internal/state"
|
|
213
|
+
func Check() { _, _ = observed.ObserveWorkspace("", "") }
|
|
214
|
+
func helper() {
|
|
215
|
+
capture := observed.ObserveWorkspace
|
|
216
|
+
_, _ = capture("", "")
|
|
217
|
+
}
|
|
218
|
+
`,
|
|
219
|
+
})
|
|
220
|
+
problems := acquisitionProblems(program)
|
|
221
|
+
if !migrationProblemContains(problems, "internal/gate/gate.go:helper", "state.ObserveWorkspace") {
|
|
222
|
+
t.Fatalf("aliased helper second capture was not rejected: %v", problems)
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
t.Run("alias assigned outside loop and called in loop is rejected", func(t *testing.T) {
|
|
227
|
+
program := parseCompleteMigrationAcquisitionFixture(t, `capture := state.ObserveWorkspace
|
|
228
|
+
for range []int{1, 2} {
|
|
229
|
+
capture()
|
|
230
|
+
}`)
|
|
231
|
+
problems := acquisitionProblems(program)
|
|
232
|
+
if !migrationProblemContains(problems, "internal/gate/gate.go:Check", "acquisition function reference", "state.ObserveWorkspace") {
|
|
233
|
+
t.Fatalf("loop-called acquisition alias was not rejected: %v", problems)
|
|
234
|
+
}
|
|
235
|
+
if !migrationProblemContains(problems, "internal/gate/gate.go:Check", "calls state.ObserveWorkspace 0 times, want 1") {
|
|
236
|
+
t.Fatalf("acquisition alias satisfied direct-call cardinality: %v", problems)
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
t.Run("anonymous closure assigned outside loop and called in loop is rejected", func(t *testing.T) {
|
|
241
|
+
program := parseCompleteMigrationAcquisitionFixture(t, `capture := func() { state.ObserveWorkspace() }
|
|
242
|
+
for range []int{1, 2} {
|
|
243
|
+
capture()
|
|
244
|
+
}`)
|
|
245
|
+
problems := acquisitionProblems(program)
|
|
246
|
+
if !migrationProblemContains(problems, "internal/gate/gate.go:Check", "acquisition function reference", "state.ObserveWorkspace") {
|
|
247
|
+
t.Fatalf("loop-called acquisition closure was not rejected: %v", problems)
|
|
248
|
+
}
|
|
249
|
+
if !migrationProblemContains(problems, "internal/gate/gate.go:Check", "calls state.ObserveWorkspace 0 times, want 1") {
|
|
250
|
+
t.Fatalf("acquisition closure satisfied direct-call cardinality: %v", problems)
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
t.Run("uncalled function value is rejected without satisfying call count", func(t *testing.T) {
|
|
255
|
+
program := parseCompleteMigrationAcquisitionFixture(t, `_ = state.ObserveWorkspace`)
|
|
256
|
+
problems := acquisitionProblems(program)
|
|
257
|
+
if !migrationProblemContains(problems, "internal/gate/gate.go:Check", "acquisition function reference", "state.ObserveWorkspace") {
|
|
258
|
+
t.Fatalf("uncalled acquisition function value was not rejected: %v", problems)
|
|
259
|
+
}
|
|
260
|
+
if !migrationProblemContains(problems, "internal/gate/gate.go:Check", "calls state.ObserveWorkspace 0 times, want 1") {
|
|
261
|
+
t.Fatalf("uncalled acquisition function value satisfied direct-call cardinality: %v", problems)
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
t.Run("dot-import direct call is accepted", func(t *testing.T) {
|
|
266
|
+
program := parseMigrationFixture(t, map[string]string{
|
|
267
|
+
"internal/state/observation.go": `package state
|
|
268
|
+
func ObserveWorkspace() { observeWorkspace() }
|
|
269
|
+
func observeWorkspace() {}
|
|
270
|
+
`,
|
|
271
|
+
"internal/state/status.go": `package state
|
|
272
|
+
func Status() { statusWithCallback() }
|
|
273
|
+
func statusWithCallback() { observeWorkspace() }
|
|
274
|
+
`,
|
|
275
|
+
"internal/gate/gate.go": fmt.Sprintf(`package gate
|
|
276
|
+
import . %q
|
|
277
|
+
func Check() { ObserveWorkspace() }
|
|
278
|
+
`, migrationStateImport),
|
|
279
|
+
"internal/gate/readiness_binding.go": fmt.Sprintf(`package gate
|
|
280
|
+
import %q
|
|
281
|
+
func ReadinessBinding() { state.ObserveWorkspace() }
|
|
282
|
+
`, migrationStateImport),
|
|
283
|
+
})
|
|
284
|
+
if problems := acquisitionProblems(program); len(problems) != 0 {
|
|
285
|
+
t.Fatalf("dot-import acquisition call was rejected: %v", problems)
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
for _, test := range []struct {
|
|
290
|
+
name string
|
|
291
|
+
checkBody string
|
|
292
|
+
wantLoop string
|
|
293
|
+
wantAccept bool
|
|
294
|
+
}{
|
|
295
|
+
{name: "direct call in for initializer is accepted", checkBody: `for state.ObserveWorkspace(); ; {
|
|
296
|
+
break
|
|
297
|
+
}`, wantAccept: true},
|
|
298
|
+
{name: "direct call in for condition is rejected", checkBody: `for ; state.ObserveWorkspace(); {
|
|
299
|
+
break
|
|
300
|
+
}`, wantLoop: "for"},
|
|
301
|
+
{name: "direct call in for post is rejected", checkBody: `for ; false; state.ObserveWorkspace() {}`, wantLoop: "for"},
|
|
302
|
+
{name: "direct call in range expression is accepted", checkBody: `for range state.ObserveWorkspace() {}`, wantAccept: true},
|
|
303
|
+
{name: "nested loop initializer remains repeat capable", checkBody: `for range []int{1, 2} {
|
|
304
|
+
for state.ObserveWorkspace(); ; {
|
|
305
|
+
break
|
|
306
|
+
}
|
|
307
|
+
}`, wantLoop: "range"},
|
|
308
|
+
} {
|
|
309
|
+
t.Run(test.name, func(t *testing.T) {
|
|
310
|
+
program := parseCompleteMigrationAcquisitionFixture(t, test.checkBody)
|
|
311
|
+
problems := acquisitionProblems(program)
|
|
312
|
+
if test.wantAccept {
|
|
313
|
+
if len(problems) != 0 {
|
|
314
|
+
t.Fatalf("one-shot acquisition call was rejected: %v", problems)
|
|
315
|
+
}
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
if !migrationProblemContains(problems, "internal/gate/gate.go:Check", "calls state.ObserveWorkspace", test.wantLoop+" loop", "repeated acquisition risk") {
|
|
319
|
+
t.Fatalf("repeat-capable acquisition call was not rejected: %v", problems)
|
|
320
|
+
}
|
|
321
|
+
})
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
loops := []struct {
|
|
325
|
+
name string
|
|
326
|
+
body func(string) string
|
|
327
|
+
}{
|
|
328
|
+
{
|
|
329
|
+
name: "for",
|
|
330
|
+
body: func(call string) string {
|
|
331
|
+
return "for i := 0; i < 2; i++ {\n" + call + "\n}"
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: "range",
|
|
336
|
+
body: func(call string) string {
|
|
337
|
+
return "for range []int{1, 2} {\n" + call + "\n}"
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
}
|
|
341
|
+
sites := []struct {
|
|
342
|
+
name string
|
|
343
|
+
file string
|
|
344
|
+
function string
|
|
345
|
+
target string
|
|
346
|
+
}{
|
|
347
|
+
{name: "state ObserveWorkspace wrapper", file: "internal/state/observation.go", function: "ObserveWorkspace", target: "observeWorkspace"},
|
|
348
|
+
{name: "state statusWithCallback", file: "internal/state/status.go", function: "statusWithCallback", target: "observeWorkspace"},
|
|
349
|
+
{name: "gate Check", file: "internal/gate/gate.go", function: "Check", target: "state.ObserveWorkspace"},
|
|
350
|
+
{name: "gate ReadinessBinding", file: "internal/gate/readiness_binding.go", function: "ReadinessBinding", target: "state.ObserveWorkspace"},
|
|
351
|
+
}
|
|
352
|
+
for _, loop := range loops {
|
|
353
|
+
for _, site := range sites {
|
|
354
|
+
t.Run(loop.name+" around "+site.name, func(t *testing.T) {
|
|
355
|
+
observationBody := "observeWorkspace()"
|
|
356
|
+
statusBody := "observeWorkspace()"
|
|
357
|
+
checkBody := "state.ObserveWorkspace()"
|
|
358
|
+
bindingBody := "state.ObserveWorkspace()"
|
|
359
|
+
switch site.file {
|
|
360
|
+
case "internal/state/observation.go":
|
|
361
|
+
observationBody = loop.body(observationBody)
|
|
362
|
+
case "internal/state/status.go":
|
|
363
|
+
statusBody = loop.body(statusBody)
|
|
364
|
+
case "internal/gate/gate.go":
|
|
365
|
+
checkBody = loop.body(checkBody)
|
|
366
|
+
case "internal/gate/readiness_binding.go":
|
|
367
|
+
bindingBody = loop.body(bindingBody)
|
|
368
|
+
}
|
|
369
|
+
program := parseMigrationFixture(t, map[string]string{
|
|
370
|
+
"internal/state/observation.go": fmt.Sprintf("package state\nfunc ObserveWorkspace() {\n%s\n}\nfunc observeWorkspace() {}\n", observationBody),
|
|
371
|
+
"internal/state/status.go": fmt.Sprintf("package state\nfunc Status() { statusWithCallback() }\nfunc statusWithCallback() {\n%s\n}\n", statusBody),
|
|
372
|
+
"internal/gate/gate.go": fmt.Sprintf("package gate\nimport %q\nfunc Check() {\n%s\n}\n", migrationStateImport, checkBody),
|
|
373
|
+
"internal/gate/readiness_binding.go": fmt.Sprintf("package gate\nimport %q\nfunc ReadinessBinding() {\n%s\n}\n", migrationStateImport, bindingBody),
|
|
374
|
+
})
|
|
375
|
+
problems := acquisitionProblems(program)
|
|
376
|
+
if !migrationProblemContains(problems, site.file+":"+site.function, site.target, loop.name+" loop", "repeated acquisition risk") {
|
|
377
|
+
t.Fatalf("looped acquisition reference was not rejected: %v", problems)
|
|
378
|
+
}
|
|
379
|
+
})
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const migrationStateImport = "github.com/devrites/devrites/internal/state"
|
|
385
|
+
|
|
386
|
+
type migrationEntry struct {
|
|
387
|
+
directory string
|
|
388
|
+
receiver string
|
|
389
|
+
function string
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
type migrationFile struct {
|
|
393
|
+
name string
|
|
394
|
+
directory string
|
|
395
|
+
packageName string
|
|
396
|
+
syntax *ast.File
|
|
397
|
+
imports map[string]string
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
type migrationFunction struct {
|
|
401
|
+
file *migrationFile
|
|
402
|
+
decl *ast.FuncDecl
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
type migrationProgram struct {
|
|
406
|
+
files []*migrationFile
|
|
407
|
+
functions map[migrationEntry]*migrationFunction
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
type migrationAcquisitionUse struct {
|
|
411
|
+
file string
|
|
412
|
+
function string
|
|
413
|
+
target string
|
|
414
|
+
loop string
|
|
415
|
+
functionValue bool
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
type migrationAcquisitionVisitor struct {
|
|
419
|
+
file *migrationFile
|
|
420
|
+
function string
|
|
421
|
+
loop string
|
|
422
|
+
functionValue bool
|
|
423
|
+
uses *[]migrationAcquisitionUse
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
func (v migrationAcquisitionVisitor) Visit(node ast.Node) ast.Visitor {
|
|
427
|
+
if node == nil {
|
|
428
|
+
return nil
|
|
429
|
+
}
|
|
430
|
+
switch node := node.(type) {
|
|
431
|
+
case *ast.FuncLit:
|
|
432
|
+
v.functionValue = true
|
|
433
|
+
v.walk(node.Body)
|
|
434
|
+
return nil
|
|
435
|
+
case *ast.ForStmt:
|
|
436
|
+
v.walk(node.Init)
|
|
437
|
+
repeated := v.repeated("for")
|
|
438
|
+
repeated.walk(node.Cond)
|
|
439
|
+
repeated.walk(node.Post)
|
|
440
|
+
repeated.walk(node.Body)
|
|
441
|
+
return nil
|
|
442
|
+
case *ast.RangeStmt:
|
|
443
|
+
v.walk(node.Key)
|
|
444
|
+
v.walk(node.Value)
|
|
445
|
+
v.walk(node.X)
|
|
446
|
+
v.repeated("range").walk(node.Body)
|
|
447
|
+
return nil
|
|
448
|
+
case *ast.CallExpr:
|
|
449
|
+
if target := migrationAcquisitionTarget(v.file, node.Fun); target != "" {
|
|
450
|
+
*v.uses = append(*v.uses, migrationAcquisitionUse{file: v.file.name, function: v.function, target: target, loop: v.loop, functionValue: v.functionValue})
|
|
451
|
+
} else {
|
|
452
|
+
v.walk(node.Fun)
|
|
453
|
+
}
|
|
454
|
+
for _, argument := range node.Args {
|
|
455
|
+
v.walk(argument)
|
|
456
|
+
}
|
|
457
|
+
return nil
|
|
458
|
+
case *ast.SelectorExpr:
|
|
459
|
+
if target := migrationAcquisitionTarget(v.file, node); target != "" {
|
|
460
|
+
*v.uses = append(*v.uses, migrationAcquisitionUse{file: v.file.name, function: v.function, target: target, loop: v.loop, functionValue: true})
|
|
461
|
+
}
|
|
462
|
+
v.walk(node.X)
|
|
463
|
+
return nil
|
|
464
|
+
case *ast.Ident:
|
|
465
|
+
if target := migrationAcquisitionTarget(v.file, node); target != "" {
|
|
466
|
+
*v.uses = append(*v.uses, migrationAcquisitionUse{file: v.file.name, function: v.function, target: target, loop: v.loop, functionValue: true})
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return v
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
func (v migrationAcquisitionVisitor) walk(node ast.Node) {
|
|
473
|
+
if node != nil {
|
|
474
|
+
ast.Walk(v, node)
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
func (v migrationAcquisitionVisitor) repeated(loop string) migrationAcquisitionVisitor {
|
|
479
|
+
if v.loop == "" {
|
|
480
|
+
v.loop = loop
|
|
481
|
+
}
|
|
482
|
+
return v
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
func migrationAcquisitionTarget(file *migrationFile, expression ast.Expr) string {
|
|
486
|
+
switch expression := expression.(type) {
|
|
487
|
+
case *ast.ParenExpr:
|
|
488
|
+
return migrationAcquisitionTarget(file, expression.X)
|
|
489
|
+
case *ast.SelectorExpr:
|
|
490
|
+
receiver, ok := expression.X.(*ast.Ident)
|
|
491
|
+
if ok && expression.Sel.Name == "ObserveWorkspace" && file.imports[receiver.Name] == migrationStateImport {
|
|
492
|
+
return "state.ObserveWorkspace"
|
|
493
|
+
}
|
|
494
|
+
case *ast.Ident:
|
|
495
|
+
if expression.Name == "observeWorkspace" && file.packageName == "state" {
|
|
496
|
+
return "observeWorkspace"
|
|
497
|
+
}
|
|
498
|
+
if expression.Name == "ObserveWorkspace" && file.imports["."] == migrationStateImport {
|
|
499
|
+
return "state.ObserveWorkspace"
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return ""
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
func parseMigrationProgram(t *testing.T, engineRoot string) *migrationProgram {
|
|
506
|
+
t.Helper()
|
|
507
|
+
program := newMigrationProgram()
|
|
508
|
+
for _, name := range productionGoFiles(t, engineRoot) {
|
|
509
|
+
program.addFile(t, name, filepath.Join(engineRoot, name), nil)
|
|
510
|
+
}
|
|
511
|
+
return program
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
func parseMigrationFixture(t *testing.T, sources map[string]string) *migrationProgram {
|
|
515
|
+
t.Helper()
|
|
516
|
+
names := make([]string, 0, len(sources))
|
|
517
|
+
for name := range sources {
|
|
518
|
+
names = append(names, name)
|
|
519
|
+
}
|
|
520
|
+
sort.Strings(names)
|
|
521
|
+
program := newMigrationProgram()
|
|
522
|
+
for _, name := range names {
|
|
523
|
+
program.addFile(t, name, name, []byte(sources[name]))
|
|
524
|
+
}
|
|
525
|
+
return program
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
func parseCompleteMigrationAcquisitionFixture(t *testing.T, checkBody string) *migrationProgram {
|
|
529
|
+
t.Helper()
|
|
530
|
+
return parseMigrationFixture(t, map[string]string{
|
|
531
|
+
"internal/state/observation.go": `package state
|
|
532
|
+
func ObserveWorkspace() { observeWorkspace() }
|
|
533
|
+
func observeWorkspace() {}
|
|
534
|
+
`,
|
|
535
|
+
"internal/state/status.go": `package state
|
|
536
|
+
func Status() { statusWithCallback() }
|
|
537
|
+
func statusWithCallback() { observeWorkspace() }
|
|
538
|
+
`,
|
|
539
|
+
"internal/gate/gate.go": fmt.Sprintf(`package gate
|
|
540
|
+
import %q
|
|
541
|
+
func Check() {
|
|
542
|
+
%s
|
|
543
|
+
}
|
|
544
|
+
`, migrationStateImport, checkBody),
|
|
545
|
+
"internal/gate/readiness_binding.go": fmt.Sprintf(`package gate
|
|
546
|
+
import %q
|
|
547
|
+
func ReadinessBinding() { state.ObserveWorkspace() }
|
|
548
|
+
`, migrationStateImport),
|
|
549
|
+
})
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
func newMigrationProgram() *migrationProgram {
|
|
553
|
+
return &migrationProgram{functions: make(map[migrationEntry]*migrationFunction)}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
func (p *migrationProgram) addFile(t *testing.T, name, parsePath string, source []byte) {
|
|
557
|
+
t.Helper()
|
|
558
|
+
var parserSource any
|
|
559
|
+
if source != nil {
|
|
560
|
+
parserSource = source
|
|
561
|
+
}
|
|
562
|
+
file, err := parser.ParseFile(token.NewFileSet(), parsePath, parserSource, 0)
|
|
563
|
+
if err != nil {
|
|
564
|
+
t.Fatal(err)
|
|
565
|
+
}
|
|
566
|
+
name = filepath.ToSlash(name)
|
|
567
|
+
parsed := &migrationFile{
|
|
568
|
+
name: name,
|
|
569
|
+
directory: filepath.ToSlash(filepath.Dir(name)),
|
|
570
|
+
packageName: file.Name.Name,
|
|
571
|
+
syntax: file,
|
|
572
|
+
imports: make(map[string]string),
|
|
573
|
+
}
|
|
574
|
+
for _, spec := range file.Imports {
|
|
575
|
+
importPath, err := strconv.Unquote(spec.Path.Value)
|
|
576
|
+
if err != nil {
|
|
577
|
+
t.Fatal(err)
|
|
578
|
+
}
|
|
579
|
+
alias := filepath.Base(importPath)
|
|
580
|
+
if spec.Name != nil {
|
|
581
|
+
alias = spec.Name.Name
|
|
582
|
+
}
|
|
583
|
+
parsed.imports[alias] = importPath
|
|
584
|
+
}
|
|
585
|
+
p.files = append(p.files, parsed)
|
|
586
|
+
for _, declaration := range file.Decls {
|
|
587
|
+
function, ok := declaration.(*ast.FuncDecl)
|
|
588
|
+
if !ok || function.Body == nil {
|
|
589
|
+
continue
|
|
590
|
+
}
|
|
591
|
+
entry := migrationEntry{
|
|
592
|
+
directory: parsed.directory,
|
|
593
|
+
receiver: migrationReceiverName(function),
|
|
594
|
+
function: function.Name.Name,
|
|
595
|
+
}
|
|
596
|
+
p.functions[entry] = &migrationFunction{file: parsed, decl: function}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
func migrationReceiverName(function *ast.FuncDecl) string {
|
|
601
|
+
if function.Recv == nil || len(function.Recv.List) == 0 {
|
|
602
|
+
return ""
|
|
603
|
+
}
|
|
604
|
+
return migrationTypeName(function.Recv.List[0].Type)
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
func migrationTypeName(expression ast.Expr) string {
|
|
608
|
+
switch expression := expression.(type) {
|
|
609
|
+
case *ast.Ident:
|
|
610
|
+
return expression.Name
|
|
611
|
+
case *ast.StarExpr:
|
|
612
|
+
return migrationTypeName(expression.X)
|
|
613
|
+
case *ast.IndexExpr:
|
|
614
|
+
return migrationTypeName(expression.X)
|
|
615
|
+
case *ast.IndexListExpr:
|
|
616
|
+
return migrationTypeName(expression.X)
|
|
617
|
+
default:
|
|
618
|
+
return ""
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
func obsoleteSurfaceProblems(program *migrationProgram) []string {
|
|
623
|
+
stateFunctions := map[string]bool{
|
|
624
|
+
"LoadFeature": true,
|
|
625
|
+
"MissingFor": true,
|
|
626
|
+
"MissingWorkspaceFiles": true,
|
|
627
|
+
"NewReport": true,
|
|
628
|
+
}
|
|
629
|
+
stateReaders := map[string]bool{
|
|
630
|
+
"declaredPhaseFromLedger": true,
|
|
631
|
+
"sectionPresentAny": true,
|
|
632
|
+
"sectionPresent": true,
|
|
633
|
+
}
|
|
634
|
+
gateReaders := map[string]bool{
|
|
635
|
+
"openHumanGates": true,
|
|
636
|
+
"readReadinessInput": true,
|
|
637
|
+
}
|
|
638
|
+
gateConstants := map[string]bool{
|
|
639
|
+
"maxReadinessInputBytes": true,
|
|
640
|
+
"maxReadinessTotalBytes": true,
|
|
641
|
+
}
|
|
642
|
+
stateSelectors := map[string]bool{
|
|
643
|
+
"Feature": true,
|
|
644
|
+
"LoadFeature": true,
|
|
645
|
+
"MissingFor": true,
|
|
646
|
+
"MissingWorkspaceFiles": true,
|
|
647
|
+
"NewReport": true,
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
seen := make(map[string]bool)
|
|
651
|
+
var problems []string
|
|
652
|
+
addProblem := func(problem string) {
|
|
653
|
+
if !seen[problem] {
|
|
654
|
+
seen[problem] = true
|
|
655
|
+
problems = append(problems, problem)
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
for _, file := range program.files {
|
|
659
|
+
for _, declaration := range file.syntax.Decls {
|
|
660
|
+
switch declaration := declaration.(type) {
|
|
661
|
+
case *ast.FuncDecl:
|
|
662
|
+
receiver := migrationReceiverName(declaration)
|
|
663
|
+
switch {
|
|
664
|
+
case file.directory == "internal/state" && receiver == "" && (stateFunctions[declaration.Name.Name] || stateReaders[declaration.Name.Name]):
|
|
665
|
+
addProblem(fmt.Sprintf("%s defines superseded state function %s", file.name, declaration.Name.Name))
|
|
666
|
+
case file.directory == "internal/state" && receiver == "WorkspaceObservation" && declaration.Name.Name == "MissingSections":
|
|
667
|
+
addProblem(fmt.Sprintf("%s defines superseded WorkspaceObservation.MissingSections", file.name))
|
|
668
|
+
case file.directory == "internal/gate" && receiver == "" && gateReaders[declaration.Name.Name]:
|
|
669
|
+
addProblem(fmt.Sprintf("%s defines superseded gate reader %s", file.name, declaration.Name.Name))
|
|
670
|
+
}
|
|
671
|
+
entry := migrationEntry{directory: file.directory, receiver: receiver, function: declaration.Name.Name}
|
|
672
|
+
function := program.functions[entry]
|
|
673
|
+
if function == nil {
|
|
674
|
+
continue
|
|
675
|
+
}
|
|
676
|
+
variableTypes := migrationVariableTypes(program, function)
|
|
677
|
+
ast.Inspect(declaration.Body, func(node ast.Node) bool {
|
|
678
|
+
switch node := node.(type) {
|
|
679
|
+
case *ast.CallExpr:
|
|
680
|
+
identifier, ok := node.Fun.(*ast.Ident)
|
|
681
|
+
if !ok {
|
|
682
|
+
return true
|
|
683
|
+
}
|
|
684
|
+
if file.directory == "internal/state" && (stateFunctions[identifier.Name] || stateReaders[identifier.Name]) {
|
|
685
|
+
addProblem(fmt.Sprintf("%s:%s calls superseded state function %s", file.name, declaration.Name.Name, identifier.Name))
|
|
686
|
+
}
|
|
687
|
+
if file.directory == "internal/gate" && gateReaders[identifier.Name] {
|
|
688
|
+
addProblem(fmt.Sprintf("%s:%s calls superseded gate reader %s", file.name, declaration.Name.Name, identifier.Name))
|
|
689
|
+
}
|
|
690
|
+
case *ast.SelectorExpr:
|
|
691
|
+
receiverType := migrationExpressionType(program, function.file.directory, node.X, variableTypes)
|
|
692
|
+
if file.directory == "internal/state" && receiverType == "Feature" && (node.Sel.Name == "Present" || node.Sel.Name == "PresentFiles") {
|
|
693
|
+
addProblem(fmt.Sprintf("%s:%s selects superseded Feature.%s", file.name, declaration.Name.Name, node.Sel.Name))
|
|
694
|
+
}
|
|
695
|
+
if file.directory == "internal/state" && receiverType == "WorkspaceObservation" && node.Sel.Name == "MissingSections" {
|
|
696
|
+
addProblem(fmt.Sprintf("%s:%s calls superseded WorkspaceObservation.MissingSections", file.name, declaration.Name.Name))
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return true
|
|
700
|
+
})
|
|
701
|
+
case *ast.GenDecl:
|
|
702
|
+
for _, spec := range declaration.Specs {
|
|
703
|
+
switch spec := spec.(type) {
|
|
704
|
+
case *ast.TypeSpec:
|
|
705
|
+
if file.directory == "internal/state" && spec.Name.Name == "Feature" {
|
|
706
|
+
addProblem(fmt.Sprintf("%s defines superseded state type Feature", file.name))
|
|
707
|
+
if structure, ok := spec.Type.(*ast.StructType); ok {
|
|
708
|
+
for _, field := range structure.Fields.List {
|
|
709
|
+
for _, name := range field.Names {
|
|
710
|
+
if name.Name == "Present" || name.Name == "PresentFiles" {
|
|
711
|
+
addProblem(fmt.Sprintf("%s defines superseded Feature.%s", file.name, name.Name))
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
case *ast.ValueSpec:
|
|
718
|
+
if file.directory == "internal/gate" {
|
|
719
|
+
for _, name := range spec.Names {
|
|
720
|
+
if gateConstants[name.Name] {
|
|
721
|
+
addProblem(fmt.Sprintf("%s defines superseded gate constant %s", file.name, name.Name))
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
ast.Inspect(file.syntax, func(node ast.Node) bool {
|
|
730
|
+
selector, ok := node.(*ast.SelectorExpr)
|
|
731
|
+
if !ok {
|
|
732
|
+
return true
|
|
733
|
+
}
|
|
734
|
+
receiver, ok := selector.X.(*ast.Ident)
|
|
735
|
+
if ok && file.imports[receiver.Name] == migrationStateImport && stateSelectors[selector.Sel.Name] {
|
|
736
|
+
addProblem(fmt.Sprintf("%s selects superseded state.%s", file.name, selector.Sel.Name))
|
|
737
|
+
}
|
|
738
|
+
return true
|
|
739
|
+
})
|
|
740
|
+
}
|
|
741
|
+
sort.Strings(problems)
|
|
742
|
+
return problems
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
func acquisitionProblems(program *migrationProgram) []string {
|
|
746
|
+
allowed := map[migrationAcquisitionUse]int{
|
|
747
|
+
{file: "internal/state/observation.go", function: "ObserveWorkspace", target: "observeWorkspace"}: 1,
|
|
748
|
+
{file: "internal/state/status.go", function: "statusWithCallback", target: "observeWorkspace"}: 1,
|
|
749
|
+
{file: "internal/gate/gate.go", function: "Check", target: "state.ObserveWorkspace"}: 1,
|
|
750
|
+
{file: "internal/gate/readiness_binding.go", function: "ReadinessBinding", target: "state.ObserveWorkspace"}: 1,
|
|
751
|
+
}
|
|
752
|
+
counts := make(map[migrationAcquisitionUse]int)
|
|
753
|
+
var problems []string
|
|
754
|
+
for _, use := range migrationAcquisitionUses(program) {
|
|
755
|
+
if use.functionValue {
|
|
756
|
+
problems = append(problems, fmt.Sprintf("%s:%s retains acquisition function reference %s; only direct calls are approved", use.file, use.function, use.target))
|
|
757
|
+
continue
|
|
758
|
+
}
|
|
759
|
+
key := use
|
|
760
|
+
key.loop = ""
|
|
761
|
+
counts[key]++
|
|
762
|
+
if _, ok := allowed[key]; !ok {
|
|
763
|
+
problems = append(problems, fmt.Sprintf("%s:%s retains unapproved acquisition call %s", use.file, use.function, use.target))
|
|
764
|
+
} else if use.loop != "" {
|
|
765
|
+
problems = append(problems, fmt.Sprintf("%s:%s calls %s inside a %s loop; repeated acquisition risk", use.file, use.function, use.target, use.loop))
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
for acquisition, want := range allowed {
|
|
769
|
+
if got := counts[acquisition]; got != want {
|
|
770
|
+
problems = append(problems, fmt.Sprintf("%s:%s calls %s %d times, want %d", acquisition.file, acquisition.function, acquisition.target, got, want))
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
status := migrationEntry{directory: "internal/state", function: "Status"}
|
|
774
|
+
statusFunction, ok := program.functions[status]
|
|
775
|
+
if !ok || statusFunction.file.name != "internal/state/status.go" {
|
|
776
|
+
problems = append(problems, "internal/state/status.go:Status is missing")
|
|
777
|
+
} else if got := migrationLocalReferenceCount(program, status, "statusWithCallback"); got != 1 {
|
|
778
|
+
problems = append(problems, fmt.Sprintf("internal/state/status.go:Status references statusWithCallback %d times, want 1", got))
|
|
779
|
+
}
|
|
780
|
+
sort.Strings(problems)
|
|
781
|
+
return problems
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
func migrationAcquisitionUses(program *migrationProgram) []migrationAcquisitionUse {
|
|
785
|
+
var uses []migrationAcquisitionUse
|
|
786
|
+
for _, file := range program.files {
|
|
787
|
+
for _, declaration := range file.syntax.Decls {
|
|
788
|
+
switch declaration := declaration.(type) {
|
|
789
|
+
case *ast.FuncDecl:
|
|
790
|
+
if declaration.Body != nil {
|
|
791
|
+
inspectMigrationAcquisitions(file, declaration.Name.Name, declaration.Body, &uses)
|
|
792
|
+
}
|
|
793
|
+
case *ast.GenDecl:
|
|
794
|
+
for _, spec := range declaration.Specs {
|
|
795
|
+
value, ok := spec.(*ast.ValueSpec)
|
|
796
|
+
if !ok {
|
|
797
|
+
continue
|
|
798
|
+
}
|
|
799
|
+
for _, expression := range value.Values {
|
|
800
|
+
inspectMigrationAcquisitions(file, "<package>", expression, &uses)
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
return uses
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
func inspectMigrationAcquisitions(file *migrationFile, function string, root ast.Node, uses *[]migrationAcquisitionUse) {
|
|
810
|
+
ast.Walk(migrationAcquisitionVisitor{file: file, function: function, uses: uses}, root)
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
func inspectMigrationIdentifiers(root ast.Node, visit func(*ast.Ident)) {
|
|
814
|
+
var inspect func(ast.Node) bool
|
|
815
|
+
inspect = func(node ast.Node) bool {
|
|
816
|
+
if selector, ok := node.(*ast.SelectorExpr); ok {
|
|
817
|
+
ast.Inspect(selector.X, inspect)
|
|
818
|
+
return false
|
|
819
|
+
}
|
|
820
|
+
if identifier, ok := node.(*ast.Ident); ok {
|
|
821
|
+
visit(identifier)
|
|
822
|
+
}
|
|
823
|
+
return true
|
|
824
|
+
}
|
|
825
|
+
ast.Inspect(root, inspect)
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
func migrationLocalReferenceCount(program *migrationProgram, entry migrationEntry, target string) int {
|
|
829
|
+
function, ok := program.functions[entry]
|
|
830
|
+
if !ok {
|
|
831
|
+
return 0
|
|
832
|
+
}
|
|
833
|
+
count := 0
|
|
834
|
+
inspectMigrationIdentifiers(function.decl.Body, func(identifier *ast.Ident) {
|
|
835
|
+
if identifier.Name == target {
|
|
836
|
+
count++
|
|
837
|
+
}
|
|
838
|
+
})
|
|
839
|
+
return count
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
func migrationConsumerEntries() []migrationEntry {
|
|
843
|
+
return []migrationEntry{
|
|
844
|
+
{directory: "internal/state", function: "Status"},
|
|
845
|
+
{directory: "internal/gate", function: "Check"},
|
|
846
|
+
{directory: "internal/gate", function: "ReadinessBinding"},
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
func consumerReachabilityProblems(program *migrationProgram, roots []migrationEntry) []string {
|
|
851
|
+
forbiddenImports := map[string]bool{
|
|
852
|
+
"io": true,
|
|
853
|
+
"os": true,
|
|
854
|
+
"path/filepath": true,
|
|
855
|
+
"github.com/devrites/devrites/internal/devritespaths": true,
|
|
856
|
+
}
|
|
857
|
+
allowedInternalImports := map[string]bool{
|
|
858
|
+
"github.com/devrites/devrites/internal/markdowntext": true,
|
|
859
|
+
"github.com/devrites/devrites/internal/reason": true,
|
|
860
|
+
migrationStateImport: true,
|
|
861
|
+
}
|
|
862
|
+
queue := append([]migrationEntry(nil), roots...)
|
|
863
|
+
visited := make(map[migrationEntry]bool)
|
|
864
|
+
seenProblems := make(map[string]bool)
|
|
865
|
+
var problems []string
|
|
866
|
+
addProblem := func(problem string) {
|
|
867
|
+
if !seenProblems[problem] {
|
|
868
|
+
seenProblems[problem] = true
|
|
869
|
+
problems = append(problems, problem)
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
for len(queue) > 0 {
|
|
873
|
+
entry := queue[0]
|
|
874
|
+
queue = queue[1:]
|
|
875
|
+
if visited[entry] {
|
|
876
|
+
continue
|
|
877
|
+
}
|
|
878
|
+
visited[entry] = true
|
|
879
|
+
function, ok := program.functions[entry]
|
|
880
|
+
if !ok {
|
|
881
|
+
addProblem(fmt.Sprintf("consumer entry %s.%s is missing", entry.directory, entry.function))
|
|
882
|
+
continue
|
|
883
|
+
}
|
|
884
|
+
ast.Inspect(function.decl.Body, func(node ast.Node) bool {
|
|
885
|
+
selector, ok := node.(*ast.SelectorExpr)
|
|
886
|
+
if !ok {
|
|
887
|
+
return true
|
|
888
|
+
}
|
|
889
|
+
receiver, ok := selector.X.(*ast.Ident)
|
|
890
|
+
if !ok {
|
|
891
|
+
return true
|
|
892
|
+
}
|
|
893
|
+
importPath := function.file.imports[receiver.Name]
|
|
894
|
+
switch {
|
|
895
|
+
case forbiddenImports[importPath]:
|
|
896
|
+
addProblem(fmt.Sprintf("reachable consumer file %s imports filesystem content-reader package %q", function.file.name, importPath))
|
|
897
|
+
case strings.HasPrefix(importPath, "github.com/devrites/devrites/internal/") && !allowedInternalImports[importPath]:
|
|
898
|
+
addProblem(fmt.Sprintf("reachable consumer file %s delegates through unapproved internal package %q", function.file.name, importPath))
|
|
899
|
+
}
|
|
900
|
+
return true
|
|
901
|
+
})
|
|
902
|
+
inspectMigrationIdentifiers(function.decl.Body, func(identifier *ast.Ident) {
|
|
903
|
+
callee := migrationEntry{directory: entry.directory, function: identifier.Name}
|
|
904
|
+
if _, exists := program.functions[callee]; exists && !migrationAcquisitionBoundary(entry, callee) {
|
|
905
|
+
queue = append(queue, callee)
|
|
906
|
+
}
|
|
907
|
+
})
|
|
908
|
+
for _, callee := range migrationMethodCallees(program, entry, function) {
|
|
909
|
+
if !migrationAcquisitionBoundary(entry, callee) {
|
|
910
|
+
queue = append(queue, callee)
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
sort.Strings(problems)
|
|
915
|
+
return problems
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
func migrationMethodCallees(program *migrationProgram, caller migrationEntry, function *migrationFunction) []migrationEntry {
|
|
919
|
+
variableTypes := migrationVariableTypes(program, function)
|
|
920
|
+
var callees []migrationEntry
|
|
921
|
+
ast.Inspect(function.decl.Body, func(node ast.Node) bool {
|
|
922
|
+
call, ok := node.(*ast.CallExpr)
|
|
923
|
+
if !ok {
|
|
924
|
+
return true
|
|
925
|
+
}
|
|
926
|
+
selector, ok := call.Fun.(*ast.SelectorExpr)
|
|
927
|
+
if !ok {
|
|
928
|
+
return true
|
|
929
|
+
}
|
|
930
|
+
receiver := migrationExpressionType(program, function.file.directory, selector.X, variableTypes)
|
|
931
|
+
if receiver == "" {
|
|
932
|
+
return true
|
|
933
|
+
}
|
|
934
|
+
callee := migrationEntry{directory: caller.directory, receiver: receiver, function: selector.Sel.Name}
|
|
935
|
+
if _, exists := program.functions[callee]; exists {
|
|
936
|
+
callees = append(callees, callee)
|
|
937
|
+
}
|
|
938
|
+
return true
|
|
939
|
+
})
|
|
940
|
+
return callees
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
func migrationVariableTypes(program *migrationProgram, function *migrationFunction) map[string]string {
|
|
944
|
+
types := make(map[string]string)
|
|
945
|
+
addFields := func(fields *ast.FieldList) {
|
|
946
|
+
if fields == nil {
|
|
947
|
+
return
|
|
948
|
+
}
|
|
949
|
+
for _, field := range fields.List {
|
|
950
|
+
typeName := migrationTypeName(field.Type)
|
|
951
|
+
for _, name := range field.Names {
|
|
952
|
+
types[name.Name] = typeName
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
addFields(function.decl.Recv)
|
|
957
|
+
addFields(function.decl.Type.Params)
|
|
958
|
+
addFields(function.decl.Type.Results)
|
|
959
|
+
|
|
960
|
+
ast.Inspect(function.decl.Body, func(node ast.Node) bool {
|
|
961
|
+
switch node := node.(type) {
|
|
962
|
+
case *ast.ValueSpec:
|
|
963
|
+
for i, name := range node.Names {
|
|
964
|
+
typeName := migrationTypeName(node.Type)
|
|
965
|
+
if typeName == "" && i < len(node.Values) {
|
|
966
|
+
typeName = migrationExpressionType(program, function.file.directory, node.Values[i], types)
|
|
967
|
+
}
|
|
968
|
+
if typeName != "" {
|
|
969
|
+
types[name.Name] = typeName
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
case *ast.AssignStmt:
|
|
973
|
+
if len(node.Lhs) != len(node.Rhs) {
|
|
974
|
+
return true
|
|
975
|
+
}
|
|
976
|
+
for i, left := range node.Lhs {
|
|
977
|
+
identifier, ok := left.(*ast.Ident)
|
|
978
|
+
if !ok {
|
|
979
|
+
continue
|
|
980
|
+
}
|
|
981
|
+
typeName := migrationExpressionType(program, function.file.directory, node.Rhs[i], types)
|
|
982
|
+
if typeName != "" {
|
|
983
|
+
types[identifier.Name] = typeName
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
return true
|
|
988
|
+
})
|
|
989
|
+
return types
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
func migrationExpressionType(program *migrationProgram, directory string, expression ast.Expr, variableTypes map[string]string) string {
|
|
993
|
+
switch expression := expression.(type) {
|
|
994
|
+
case *ast.Ident:
|
|
995
|
+
return variableTypes[expression.Name]
|
|
996
|
+
case *ast.CompositeLit:
|
|
997
|
+
return migrationTypeName(expression.Type)
|
|
998
|
+
case *ast.UnaryExpr:
|
|
999
|
+
return migrationExpressionType(program, directory, expression.X, variableTypes)
|
|
1000
|
+
case *ast.ParenExpr:
|
|
1001
|
+
return migrationExpressionType(program, directory, expression.X, variableTypes)
|
|
1002
|
+
case *ast.CallExpr:
|
|
1003
|
+
identifier, ok := expression.Fun.(*ast.Ident)
|
|
1004
|
+
if !ok {
|
|
1005
|
+
return ""
|
|
1006
|
+
}
|
|
1007
|
+
if identifier.Name == "new" && len(expression.Args) == 1 {
|
|
1008
|
+
return migrationTypeName(expression.Args[0])
|
|
1009
|
+
}
|
|
1010
|
+
return migrationFunctionResultType(program.functions[migrationEntry{directory: directory, function: identifier.Name}])
|
|
1011
|
+
}
|
|
1012
|
+
return ""
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
func migrationFunctionResultType(function *migrationFunction) string {
|
|
1016
|
+
if function == nil || function.decl.Type.Results == nil || len(function.decl.Type.Results.List) != 1 {
|
|
1017
|
+
return ""
|
|
1018
|
+
}
|
|
1019
|
+
result := function.decl.Type.Results.List[0]
|
|
1020
|
+
if len(result.Names) > 1 {
|
|
1021
|
+
return ""
|
|
1022
|
+
}
|
|
1023
|
+
return migrationTypeName(result.Type)
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
func migrationAcquisitionBoundary(caller, callee migrationEntry) bool {
|
|
1027
|
+
return caller == (migrationEntry{directory: "internal/state", function: "statusWithCallback"}) &&
|
|
1028
|
+
callee == (migrationEntry{directory: "internal/state", function: "observeWorkspace"})
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
func migrationProblemContains(problems []string, fragments ...string) bool {
|
|
1032
|
+
for _, problem := range problems {
|
|
1033
|
+
matched := true
|
|
1034
|
+
for _, fragment := range fragments {
|
|
1035
|
+
matched = matched && strings.Contains(problem, fragment)
|
|
1036
|
+
}
|
|
1037
|
+
if matched {
|
|
1038
|
+
return true
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
return false
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
func productionGoFiles(t *testing.T, engineRoot string) []string {
|
|
1045
|
+
t.Helper()
|
|
1046
|
+
var files []string
|
|
1047
|
+
err := filepath.WalkDir(engineRoot, func(path string, entry os.DirEntry, walkErr error) error {
|
|
1048
|
+
if walkErr != nil {
|
|
1049
|
+
return walkErr
|
|
1050
|
+
}
|
|
1051
|
+
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") || strings.HasSuffix(entry.Name(), "_test.go") {
|
|
1052
|
+
return nil
|
|
1053
|
+
}
|
|
1054
|
+
relative, err := filepath.Rel(engineRoot, path)
|
|
1055
|
+
if err != nil {
|
|
1056
|
+
return err
|
|
1057
|
+
}
|
|
1058
|
+
files = append(files, filepath.ToSlash(relative))
|
|
1059
|
+
return nil
|
|
1060
|
+
})
|
|
1061
|
+
if err != nil {
|
|
1062
|
+
t.Fatal(err)
|
|
1063
|
+
}
|
|
1064
|
+
return files
|
|
1065
|
+
}
|