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.
Files changed (84) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +1 -1
  3. package/docs/engine/commands.md +65 -6
  4. package/engine/go.mod +1 -1
  5. package/engine/internal/gate/gate.go +154 -46
  6. package/engine/internal/gate/gate_test.go +290 -2
  7. package/engine/internal/gate/readiness_binding.go +66 -100
  8. package/engine/internal/gate/readiness_binding_test.go +126 -3
  9. package/engine/internal/lib/resolve.go +6 -6
  10. package/engine/internal/state/cmd/workflowmanifest/main.go +32 -3
  11. package/engine/internal/state/feature.go +1 -100
  12. package/engine/internal/state/observation.go +584 -0
  13. package/engine/internal/state/observation_open_other.go +9 -0
  14. package/engine/internal/state/observation_open_unix.go +12 -0
  15. package/engine/internal/state/observation_test.go +960 -0
  16. package/engine/internal/state/observation_unix_test.go +68 -0
  17. package/engine/internal/state/schema.go +60 -160
  18. package/engine/internal/state/state_test.go +489 -101
  19. package/engine/internal/state/status.go +67 -48
  20. package/engine/tests/adr_0004_required_by_phase_test.go +16 -13
  21. package/engine/tests/adr_0011_define_plan_test.go +14 -12
  22. package/engine/tests/gate_test.go +403 -28
  23. package/engine/tests/workspace_observation_migration_test.go +1065 -0
  24. package/pack/.claude/skills/devrites-debug-recovery/SKILL.md +1 -10
  25. package/pack/.claude/skills/devrites-lib/reference/standards/acceptance-preserving-reslice.md +30 -0
  26. package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +2 -9
  27. package/pack/.claude/skills/devrites-lib/reference/standards/one-shot-actions.md +1 -6
  28. package/pack/.claude/skills/devrites-lib/reference/standards/workflow-artifacts.md +411 -102
  29. package/pack/.claude/skills/rite-autocomplete/SKILL.md +98 -146
  30. package/pack/.claude/skills/rite-autocomplete/reference/decision-policy.md +11 -5
  31. package/pack/.claude/skills/rite-autocomplete/reference/loop.md +96 -134
  32. package/pack/.claude/skills/rite-autocomplete/reference/stop-conditions.md +92 -131
  33. package/pack/.claude/skills/rite-build/SKILL.md +51 -79
  34. package/pack/.claude/skills/rite-build/reference/phase-contract.md +3 -17
  35. package/pack/.claude/skills/rite-plan/SKILL.md +17 -17
  36. package/pack/.claude/skills/rite-plan/reference/anti-patterns.md +11 -1
  37. package/pack/.claude/skills/rite-plan/reference/replan-and-repair.md +10 -3
  38. package/pack/.claude/skills/rite-prove/SKILL.md +95 -153
  39. package/pack/.claude/skills/rite-vet/SKILL.md +121 -174
  40. package/pack/.claude/skills/rite-vet/reference/anti-patterns.md +14 -2
  41. package/pack/.claude/skills/rite-vet/reference/artifacts.md +10 -6
  42. package/pack/.claude/skills/rite-vet/reference/depth.md +10 -3
  43. package/pack/.claude/skills/rite-vet/reference/review-axes.md +12 -4
  44. package/pack/generated/claude/skills/devrites-debug-recovery/SKILL.md +1 -10
  45. package/pack/generated/claude/skills/devrites-lib/reference/standards/acceptance-preserving-reslice.md +30 -0
  46. package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +2 -9
  47. package/pack/generated/claude/skills/devrites-lib/reference/standards/one-shot-actions.md +1 -6
  48. package/pack/generated/claude/skills/devrites-lib/reference/standards/workflow-artifacts.md +411 -102
  49. package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +98 -146
  50. package/pack/generated/claude/skills/rite-autocomplete/reference/decision-policy.md +11 -5
  51. package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +96 -134
  52. package/pack/generated/claude/skills/rite-autocomplete/reference/stop-conditions.md +92 -131
  53. package/pack/generated/claude/skills/rite-build/SKILL.md +51 -79
  54. package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +3 -17
  55. package/pack/generated/claude/skills/rite-plan/SKILL.md +17 -17
  56. package/pack/generated/claude/skills/rite-plan/reference/anti-patterns.md +11 -1
  57. package/pack/generated/claude/skills/rite-plan/reference/replan-and-repair.md +10 -3
  58. package/pack/generated/claude/skills/rite-prove/SKILL.md +95 -153
  59. package/pack/generated/claude/skills/rite-vet/SKILL.md +121 -174
  60. package/pack/generated/claude/skills/rite-vet/reference/anti-patterns.md +14 -2
  61. package/pack/generated/claude/skills/rite-vet/reference/artifacts.md +10 -6
  62. package/pack/generated/claude/skills/rite-vet/reference/depth.md +10 -3
  63. package/pack/generated/claude/skills/rite-vet/reference/review-axes.md +12 -4
  64. package/pack/generated/codex/skills/devrites-debug-recovery/SKILL.md +1 -10
  65. package/pack/generated/codex/skills/devrites-lib/reference/standards/acceptance-preserving-reslice.md +30 -0
  66. package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +2 -9
  67. package/pack/generated/codex/skills/devrites-lib/reference/standards/one-shot-actions.md +1 -6
  68. package/pack/generated/codex/skills/devrites-lib/reference/standards/workflow-artifacts.md +411 -102
  69. package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +98 -146
  70. package/pack/generated/codex/skills/rite-autocomplete/reference/decision-policy.md +11 -5
  71. package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +96 -134
  72. package/pack/generated/codex/skills/rite-autocomplete/reference/stop-conditions.md +92 -131
  73. package/pack/generated/codex/skills/rite-build/SKILL.md +51 -79
  74. package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +3 -17
  75. package/pack/generated/codex/skills/rite-plan/SKILL.md +17 -17
  76. package/pack/generated/codex/skills/rite-plan/reference/anti-patterns.md +11 -1
  77. package/pack/generated/codex/skills/rite-plan/reference/replan-and-repair.md +10 -3
  78. package/pack/generated/codex/skills/rite-prove/SKILL.md +95 -153
  79. package/pack/generated/codex/skills/rite-vet/SKILL.md +121 -174
  80. package/pack/generated/codex/skills/rite-vet/reference/anti-patterns.md +14 -2
  81. package/pack/generated/codex/skills/rite-vet/reference/artifacts.md +10 -6
  82. package/pack/generated/codex/skills/rite-vet/reference/depth.md +10 -3
  83. package/pack/generated/codex/skills/rite-vet/reference/review-axes.md +12 -4
  84. 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
+ }