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
|
@@ -8,6 +8,7 @@ import (
|
|
|
8
8
|
"testing"
|
|
9
9
|
|
|
10
10
|
"github.com/devrites/devrites/internal/reason"
|
|
11
|
+
"github.com/devrites/devrites/internal/state"
|
|
11
12
|
"github.com/devrites/devrites/internal/testutil"
|
|
12
13
|
)
|
|
13
14
|
|
|
@@ -135,12 +136,12 @@ func TestReadinessBindingRejectsUnsafeInputsWithoutDisclosure(t *testing.T) {
|
|
|
135
136
|
}
|
|
136
137
|
}},
|
|
137
138
|
{name: "per input limit", mutate: func(t *testing.T, workspace string) {
|
|
138
|
-
if err := os.WriteFile(filepath.Join(workspace, "test-plan.md"), bytes.Repeat([]byte("a"),
|
|
139
|
+
if err := os.WriteFile(filepath.Join(workspace, "test-plan.md"), bytes.Repeat([]byte("a"), (1<<20)+1), 0o644); err != nil {
|
|
139
140
|
t.Fatal(err)
|
|
140
141
|
}
|
|
141
142
|
}},
|
|
142
143
|
{name: "aggregate limit", mutate: func(t *testing.T, workspace string) {
|
|
143
|
-
content := bytes.Repeat([]byte("a"),
|
|
144
|
+
content := bytes.Repeat([]byte("a"), 1<<20)
|
|
144
145
|
for _, name := range []string{"spec.md", "decision-coverage.md", "architecture.md", "plan.md", "tasks.md", "traceability.md", "test-plan.md", "strategy.md", "design-brief.md"} {
|
|
145
146
|
if err := os.WriteFile(filepath.Join(workspace, name), content, 0o644); err != nil {
|
|
146
147
|
t.Fatal(err)
|
|
@@ -173,11 +174,18 @@ func TestVerifyReadinessBindingRequiresOneStructuralStandaloneLine(t *testing.T)
|
|
|
173
174
|
wantErr bool
|
|
174
175
|
}{
|
|
175
176
|
{name: "exact", content: "# Engineering review\n\n" + expected + "\n"},
|
|
177
|
+
{name: "exact CRLF", content: "# Engineering review\r\n\r\n" + expected + "\r\n"},
|
|
176
178
|
{name: "exact plus fenced example", content: expected + "\n```text\n" + expected + "\n```\n"},
|
|
177
179
|
{name: "missing", content: "# Engineering review\n\nREADY\n", wantErr: true},
|
|
178
180
|
{name: "malformed", content: readinessBindingLabel + strings.Repeat("a", 63) + "\n", wantErr: true},
|
|
179
181
|
{name: "duplicate", content: expected + "\n" + expected + "\n", wantErr: true},
|
|
182
|
+
{name: "prefixed", content: "prefix " + expected + "\n", wantErr: true},
|
|
180
183
|
{name: "fenced only", content: "```text\n" + expected + "\n```\n", wantErr: true},
|
|
184
|
+
{name: "unterminated fence", content: "```text\n" + expected + "\n", wantErr: true},
|
|
185
|
+
{name: "tilde fenced only", content: "~~~text\n" + expected + "\n~~~~~\n", wantErr: true},
|
|
186
|
+
{name: "tilde fenced plus visible", content: "~~~text\n" + expected + "\n~~~~~\n" + expected + "\n"},
|
|
187
|
+
{name: "HTML comment remains structural", content: "<!--\n" + expected + "\n-->\n"},
|
|
188
|
+
{name: "inline comment marker does not hide binding", content: "`<!--`\n" + expected + "\n"},
|
|
181
189
|
{name: "mismatch", content: readinessBindingLabel + strings.Repeat("0", 64) + "\n", wantErr: true},
|
|
182
190
|
{name: "not standalone", content: " " + expected + "\n", wantErr: true},
|
|
183
191
|
}
|
|
@@ -185,7 +193,11 @@ func TestVerifyReadinessBindingRequiresOneStructuralStandaloneLine(t *testing.T)
|
|
|
185
193
|
for _, test := range tests {
|
|
186
194
|
t.Run(test.name, func(t *testing.T) {
|
|
187
195
|
testutil.WriteFile(t, filepath.Join(workspace, "eng-review.md"), test.content)
|
|
188
|
-
|
|
196
|
+
observation, observeErr := state.ObserveWorkspace(root, "markers")
|
|
197
|
+
if observeErr != nil {
|
|
198
|
+
t.Fatal(observeErr)
|
|
199
|
+
}
|
|
200
|
+
got, err := verifyReadinessBinding(observation)
|
|
189
201
|
if (err != nil) != test.wantErr {
|
|
190
202
|
t.Fatalf("verifyReadinessBinding() error=%v, wantErr=%v", err, test.wantErr)
|
|
191
203
|
}
|
|
@@ -238,6 +250,117 @@ func TestCheckUsesReadinessBindingForBuildAndSeal(t *testing.T) {
|
|
|
238
250
|
}
|
|
239
251
|
}
|
|
240
252
|
|
|
253
|
+
func TestReadinessBindingObservationUsesRetainedBytes(t *testing.T) {
|
|
254
|
+
root := t.TempDir()
|
|
255
|
+
workspace := writeReadinessFixture(t, root, "retained-binding", "build")
|
|
256
|
+
observation, err := state.ObserveWorkspace(root, "retained-binding")
|
|
257
|
+
if err != nil {
|
|
258
|
+
t.Fatal(err)
|
|
259
|
+
}
|
|
260
|
+
before, err := readinessBindingFromObservation(observation)
|
|
261
|
+
if err != nil {
|
|
262
|
+
t.Fatal(err)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
testutil.WriteFile(t, filepath.Join(workspace, "spec.md"), "# Spec\n\nChanged after observation.\n")
|
|
266
|
+
testutil.WriteFile(t, filepath.Join(workspace, "strategy.md"), "# Strategy\n\nAdded after observation.\n")
|
|
267
|
+
after, err := readinessBindingFromObservation(observation)
|
|
268
|
+
if err != nil {
|
|
269
|
+
t.Fatal(err)
|
|
270
|
+
}
|
|
271
|
+
if after != before {
|
|
272
|
+
t.Fatalf("retained observation binding changed: before=%q after=%q", before, after)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
func TestReadinessBindingOptionalEmptyFilesRemainPresent(t *testing.T) {
|
|
277
|
+
forms := []struct {
|
|
278
|
+
name string
|
|
279
|
+
body string
|
|
280
|
+
}{
|
|
281
|
+
{name: "zero-byte"},
|
|
282
|
+
{name: "heading-only", body: "# Optional\n"},
|
|
283
|
+
{name: "frontmatter-only", body: "---\nkind: optional\n---\n"},
|
|
284
|
+
}
|
|
285
|
+
for _, logical := range []string{"strategy.md", "design-brief.md", "ai-spec.md", ".devrites/principles.md"} {
|
|
286
|
+
for _, form := range forms {
|
|
287
|
+
t.Run(logical+"/"+form.name, func(t *testing.T) {
|
|
288
|
+
root := t.TempDir()
|
|
289
|
+
workspace := writeReadinessFixture(t, root, "optional-empty", "build")
|
|
290
|
+
baseline := mustReadinessBinding(t, root, "optional-empty")
|
|
291
|
+
path := filepath.Join(workspace, logical)
|
|
292
|
+
if logical == ".devrites/principles.md" {
|
|
293
|
+
path = filepath.Join(root, "principles.md")
|
|
294
|
+
}
|
|
295
|
+
testutil.WriteFile(t, path, form.body)
|
|
296
|
+
present := mustReadinessBinding(t, root, "optional-empty")
|
|
297
|
+
if present == baseline {
|
|
298
|
+
t.Fatalf("adding %s %s did not encode present bytes", form.name, logical)
|
|
299
|
+
}
|
|
300
|
+
if err := os.Remove(path); err != nil {
|
|
301
|
+
t.Fatal(err)
|
|
302
|
+
}
|
|
303
|
+
if restored := mustReadinessBinding(t, root, "optional-empty"); restored != baseline {
|
|
304
|
+
t.Fatalf("removing %s did not restore absent binding: got %q want %q", logical, restored, baseline)
|
|
305
|
+
}
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
func TestCheckReadinessBindingDetectsEmptyOptionalAddAndRemove(t *testing.T) {
|
|
312
|
+
for _, logical := range []string{"strategy.md", "design-brief.md", "ai-spec.md", ".devrites/principles.md"} {
|
|
313
|
+
t.Run(logical, func(t *testing.T) {
|
|
314
|
+
root := t.TempDir()
|
|
315
|
+
workspace := writeReadinessFixture(t, root, "empty-stale", "build")
|
|
316
|
+
binding := mustReadinessBinding(t, root, "empty-stale")
|
|
317
|
+
testutil.AppendFile(t, filepath.Join(workspace, "eng-review.md"), "\n"+binding+"\n")
|
|
318
|
+
path := filepath.Join(workspace, logical)
|
|
319
|
+
if logical == ".devrites/principles.md" {
|
|
320
|
+
path = filepath.Join(root, "principles.md")
|
|
321
|
+
}
|
|
322
|
+
testutil.WriteFile(t, path, "")
|
|
323
|
+
|
|
324
|
+
result, err := Check(Readiness, root, "empty-stale")
|
|
325
|
+
if err != nil {
|
|
326
|
+
t.Fatal(err)
|
|
327
|
+
}
|
|
328
|
+
if !result.Blocked || result.ReasonID != reason.GateReadinessStale {
|
|
329
|
+
t.Fatalf("empty optional add blocked=%v reason=%q", result.Blocked, result.ReasonID)
|
|
330
|
+
}
|
|
331
|
+
if err := os.Remove(path); err != nil {
|
|
332
|
+
t.Fatal(err)
|
|
333
|
+
}
|
|
334
|
+
result, err = Check(Readiness, root, "empty-stale")
|
|
335
|
+
if err != nil {
|
|
336
|
+
t.Fatal(err)
|
|
337
|
+
}
|
|
338
|
+
if result.Blocked {
|
|
339
|
+
t.Fatalf("empty optional removal remained blocked: %s", result.Render())
|
|
340
|
+
}
|
|
341
|
+
})
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
func TestReadinessDiagnosticPayloads(t *testing.T) {
|
|
346
|
+
for _, tc := range []struct {
|
|
347
|
+
diagnostic state.ArtifactDiagnostic
|
|
348
|
+
want string
|
|
349
|
+
}{
|
|
350
|
+
{state.ArtifactDiagnostic{Path: "strategy.md", State: state.ArtifactMalformed, Code: state.DiagnosticMalformedMarkdown}, "readiness input strategy.md is malformed (malformed_markdown); replace invalid Markdown with valid Markdown"},
|
|
351
|
+
{state.ArtifactDiagnostic{Path: "strategy.md", State: state.ArtifactUnsafe, Code: state.DiagnosticParentSymlink}, "readiness input strategy.md is unsafe (parent_symlink); replace the symlinked parent with a real directory"},
|
|
352
|
+
{state.ArtifactDiagnostic{Path: "strategy.md", State: state.ArtifactUnsafe, Code: state.DiagnosticFinalSymlink}, "readiness input strategy.md is unsafe (final_symlink); replace the symlink with a regular file"},
|
|
353
|
+
{state.ArtifactDiagnostic{Path: "strategy.md", State: state.ArtifactUnsafe, Code: state.DiagnosticNonRegular}, "readiness input strategy.md is unsafe (non_regular); replace the non-regular entry with a regular file"},
|
|
354
|
+
{state.ArtifactDiagnostic{Path: "strategy.md", State: state.ArtifactUnsafe, Code: state.DiagnosticFileTooLarge}, "readiness input strategy.md is unsafe (file_too_large); reduce the file to at most 1 MiB"},
|
|
355
|
+
{state.ArtifactDiagnostic{Path: "strategy.md", State: state.ArtifactUnreadable, Code: state.DiagnosticPermissionDenied}, "readiness input strategy.md is unreadable (permission_denied); grant read permission"},
|
|
356
|
+
{state.ArtifactDiagnostic{Path: "strategy.md", State: state.ArtifactUnreadable, Code: state.DiagnosticReadFailure}, "readiness input strategy.md is unreadable (read_failure); restore a readable regular file"},
|
|
357
|
+
} {
|
|
358
|
+
if got := readinessDiagnosticError(tc.diagnostic).Error(); got != tc.want {
|
|
359
|
+
t.Errorf("readinessDiagnosticError(%+v)=%q, want %q", tc.diagnostic, got, tc.want)
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
241
364
|
func writeReadinessFixture(t *testing.T, root, slug, phase string) string {
|
|
242
365
|
t.Helper()
|
|
243
366
|
workspace := filepath.Join(root, "work", slug)
|
|
@@ -286,15 +286,15 @@ func clearAwaiting(sfile, qid string) error {
|
|
|
286
286
|
return fmt.Errorf("read state %s: %w", sfile, err)
|
|
287
287
|
}
|
|
288
288
|
lines := splitLinesNoTrailing(data)
|
|
289
|
-
|
|
289
|
+
resumePolicy, _ := state.PolicyFor(state.PhaseBuild)
|
|
290
290
|
if rawPhase, ok := state.CursorField(lines, state.CursorPhase); ok {
|
|
291
291
|
if fields := strings.Fields(strings.ToLower(rawPhase)); len(fields) > 0 {
|
|
292
|
-
if
|
|
293
|
-
|
|
292
|
+
if policy, known := state.PolicyFor(state.Phase(fields[0])); known && policy.ResumeVerb != "" {
|
|
293
|
+
resumePolicy = policy
|
|
294
294
|
}
|
|
295
295
|
}
|
|
296
296
|
}
|
|
297
|
-
resumeCommand := workflow.ForVerb(
|
|
297
|
+
resumeCommand := workflow.ForVerb(resumePolicy.ResumeVerb)
|
|
298
298
|
// First check whether the awaiting block references this question at all.
|
|
299
299
|
inAw := false
|
|
300
300
|
var awaitingLines []string
|
|
@@ -337,7 +337,7 @@ func clearAwaiting(sfile, qid string) error {
|
|
|
337
337
|
continue
|
|
338
338
|
case inLog && hdrSpaceRe.MatchString(line):
|
|
339
339
|
if !logAppended {
|
|
340
|
-
out = append(out, fmt.Sprintf("- %s %s: resolved %s", ts,
|
|
340
|
+
out = append(out, fmt.Sprintf("- %s %s: resolved %s", ts, resumePolicy.Target, qid))
|
|
341
341
|
logAppended = true
|
|
342
342
|
}
|
|
343
343
|
inLog = false
|
|
@@ -347,7 +347,7 @@ func clearAwaiting(sfile, qid string) error {
|
|
|
347
347
|
out = append(out, line)
|
|
348
348
|
}
|
|
349
349
|
if inLog && !logAppended {
|
|
350
|
-
out = append(out, fmt.Sprintf("- %s %s: resolved %s", ts,
|
|
350
|
+
out = append(out, fmt.Sprintf("- %s %s: resolved %s", ts, resumePolicy.Target, qid))
|
|
351
351
|
}
|
|
352
352
|
out, _ = state.SetCursorField(out, state.CursorStatus, "running")
|
|
353
353
|
out, _ = state.SetCursorField(out, state.CursorNextAction, "(resume: `"+resumeCommand.Both()+"` to continue the workflow)")
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Command workflowmanifest writes the deterministic cross-language derivative
|
|
2
|
-
// of state.
|
|
2
|
+
// of state.PhasePolicies. The typed Go registry remains the authority.
|
|
3
3
|
package main
|
|
4
4
|
|
|
5
5
|
import (
|
|
@@ -12,6 +12,35 @@ import (
|
|
|
12
12
|
"github.com/devrites/devrites/internal/state"
|
|
13
13
|
)
|
|
14
14
|
|
|
15
|
+
type workflowPhase struct {
|
|
16
|
+
ID state.Phase `json:"id"`
|
|
17
|
+
ResumeVerb string `json:"resumeVerb,omitempty"`
|
|
18
|
+
TransitionRight string `json:"transitionRight"`
|
|
19
|
+
RequiredSections []state.Section `json:"requiredSections,omitempty"`
|
|
20
|
+
WorkspaceRequired []state.ArtifactPath `json:"workspaceRequired"`
|
|
21
|
+
ProofRequired bool `json:"proofRequired,omitempty"`
|
|
22
|
+
BlocksOpenQuestions bool `json:"blocksOpenQuestions,omitempty"`
|
|
23
|
+
Shippable bool `json:"shippable,omitempty"`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func workflowPhases() []workflowPhase {
|
|
27
|
+
policies := state.PhasePolicies()
|
|
28
|
+
phases := make([]workflowPhase, len(policies))
|
|
29
|
+
for i, policy := range policies {
|
|
30
|
+
phases[i] = workflowPhase{
|
|
31
|
+
ID: policy.Target,
|
|
32
|
+
ResumeVerb: policy.ResumeVerb,
|
|
33
|
+
TransitionRight: policy.TransitionRight,
|
|
34
|
+
RequiredSections: policy.RequiredSections,
|
|
35
|
+
WorkspaceRequired: policy.RequiredArtifacts,
|
|
36
|
+
ProofRequired: policy.ProofRequired,
|
|
37
|
+
BlocksOpenQuestions: policy.BlocksOpenQuestions,
|
|
38
|
+
Shippable: policy.Shippable,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return phases
|
|
42
|
+
}
|
|
43
|
+
|
|
15
44
|
func main() {
|
|
16
45
|
outPath := flag.String("out", "workflow_manifest.json", "output path")
|
|
17
46
|
check := flag.Bool("check", false, "fail when the output is stale")
|
|
@@ -21,13 +50,13 @@ func main() {
|
|
|
21
50
|
GeneratedBy string `json:"generatedBy"`
|
|
22
51
|
SchemaVersion int `json:"schemaVersion"`
|
|
23
52
|
AuthorityPolicy state.AuthorityPolicy `json:"authorityPolicy"`
|
|
24
|
-
Phases []
|
|
53
|
+
Phases []workflowPhase `json:"phases"`
|
|
25
54
|
CursorKeyAliases []state.CursorKeyAlias `json:"cursorKeyAliases"`
|
|
26
55
|
}{
|
|
27
56
|
GeneratedBy: "go generate ./internal/state; DO NOT EDIT",
|
|
28
57
|
SchemaVersion: state.SchemaVersion,
|
|
29
58
|
AuthorityPolicy: state.WorkflowAuthorityPolicy(),
|
|
30
|
-
Phases:
|
|
59
|
+
Phases: workflowPhases(),
|
|
31
60
|
CursorKeyAliases: state.CursorKeyAliases(),
|
|
32
61
|
}
|
|
33
62
|
data, err := json.MarshalIndent(document, "", " ")
|
|
@@ -9,7 +9,6 @@ import (
|
|
|
9
9
|
"strings"
|
|
10
10
|
|
|
11
11
|
"github.com/devrites/devrites/internal/devritespaths"
|
|
12
|
-
"github.com/devrites/devrites/internal/markdowntext"
|
|
13
12
|
"github.com/devrites/devrites/internal/rootfacts"
|
|
14
13
|
)
|
|
15
14
|
|
|
@@ -24,15 +23,6 @@ func ResolveRoot(override string) (string, error) {
|
|
|
24
23
|
return facts.PhysicalRoot, nil
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
// Feature is a loaded per-feature workspace: its declared phase plus which
|
|
28
|
-
// completeness sections currently have real content.
|
|
29
|
-
type Feature struct {
|
|
30
|
-
Slug string
|
|
31
|
-
Phase Phase
|
|
32
|
-
Present map[Section]bool
|
|
33
|
-
PresentFiles map[string]bool
|
|
34
|
-
}
|
|
35
|
-
|
|
36
26
|
// featureDir is the canonical per-feature state directory under root.
|
|
37
27
|
func featureDir(root, slug string) string {
|
|
38
28
|
return devritespaths.FeatureDir(root, slug)
|
|
@@ -71,74 +61,6 @@ func regularFileExists(path string) bool {
|
|
|
71
61
|
return err == nil && info.Mode().IsRegular()
|
|
72
62
|
}
|
|
73
63
|
|
|
74
|
-
// LoadFeature reads a canonical feature workspace. state.md is the sole
|
|
75
|
-
// lifecycle authority; optional README.md metadata is not runtime state.
|
|
76
|
-
func LoadFeature(root, slug string) (*Feature, error) {
|
|
77
|
-
dir := featureDir(root, slug)
|
|
78
|
-
if !regularFileExists(filepath.Join(dir, LedgerFile)) {
|
|
79
|
-
return nil, fmt.Errorf("feature %q not found", slug)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
phase, ledgerDeclared, err := declaredPhaseFromLedger(filepath.Join(dir, LedgerFile))
|
|
83
|
-
if err != nil {
|
|
84
|
-
return nil, fmt.Errorf("feature %q: %w", slug, err)
|
|
85
|
-
}
|
|
86
|
-
if !ledgerDeclared {
|
|
87
|
-
return nil, fmt.Errorf("feature %q: no phase in %s ledger", slug, LedgerFile)
|
|
88
|
-
}
|
|
89
|
-
if !KnownPhase(phase) {
|
|
90
|
-
return nil, fmt.Errorf("feature %q: unknown phase %q", slug, phase)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
present := make(map[Section]bool, len(Sections))
|
|
94
|
-
for _, section := range Sections {
|
|
95
|
-
present[section] = sectionPresentAny(dir, section)
|
|
96
|
-
}
|
|
97
|
-
presentFiles := make(map[string]bool)
|
|
98
|
-
for _, name := range WorkspaceFiles() {
|
|
99
|
-
presentFiles[name] = sectionPresent(filepath.Join(dir, name))
|
|
100
|
-
}
|
|
101
|
-
return &Feature{
|
|
102
|
-
Slug: slug,
|
|
103
|
-
Phase: phase,
|
|
104
|
-
Present: present,
|
|
105
|
-
PresentFiles: presentFiles,
|
|
106
|
-
}, nil
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
func sectionPresentAny(dir string, s Section) bool {
|
|
110
|
-
for _, name := range sectionFiles[s] {
|
|
111
|
-
if sectionPresent(filepath.Join(dir, name)) {
|
|
112
|
-
return true
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return false
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
func declaredPhaseFromLedger(path string) (Phase, bool, error) {
|
|
119
|
-
raw, err := os.ReadFile(path)
|
|
120
|
-
if err != nil {
|
|
121
|
-
if errors.Is(err, os.ErrNotExist) {
|
|
122
|
-
return "", false, nil
|
|
123
|
-
}
|
|
124
|
-
return "", false, fmt.Errorf("read %s: %w", LedgerFile, err)
|
|
125
|
-
}
|
|
126
|
-
structural, err := markdowntext.Structural(raw)
|
|
127
|
-
if err != nil {
|
|
128
|
-
return "", false, fmt.Errorf("read %s: %w", LedgerFile, err)
|
|
129
|
-
}
|
|
130
|
-
lines := strings.Split(string(structural), "\n")
|
|
131
|
-
value, ok := CursorField(lines, CursorPhase)
|
|
132
|
-
if !ok {
|
|
133
|
-
return "", false, nil
|
|
134
|
-
}
|
|
135
|
-
word := firstPhaseWord(value)
|
|
136
|
-
if phase, known := PhaseForName(word); known {
|
|
137
|
-
return phase, true, nil
|
|
138
|
-
}
|
|
139
|
-
return Phase(word), true, nil
|
|
140
|
-
}
|
|
141
|
-
|
|
142
64
|
func firstPhaseWord(value string) string {
|
|
143
65
|
word := strings.ToLower(strings.TrimSpace(value))
|
|
144
66
|
if i := strings.IndexAny(word, " "); i > 0 {
|
|
@@ -153,27 +75,6 @@ func blankOrHash(trimmed string) bool {
|
|
|
153
75
|
return trimmed == "" || strings.HasPrefix(trimmed, "#")
|
|
154
76
|
}
|
|
155
77
|
|
|
156
|
-
// sectionPresent reports whether a section file has real content: it exists
|
|
157
|
-
// and, after removing a leading YAML frontmatter block, ATX (`#`) headings, and
|
|
158
|
-
// whitespace, some content remains. A stub that is only a heading counts as
|
|
159
|
-
// empty, so scaffolding a file never fakes completeness.
|
|
160
|
-
func sectionPresent(path string) bool {
|
|
161
|
-
raw, err := os.ReadFile(path)
|
|
162
|
-
if err != nil {
|
|
163
|
-
return false
|
|
164
|
-
}
|
|
165
|
-
if _, err := markdowntext.Structural(raw); err != nil {
|
|
166
|
-
return false
|
|
167
|
-
}
|
|
168
|
-
body := stripFrontmatter(raw)
|
|
169
|
-
for _, line := range strings.Split(string(body), "\n") {
|
|
170
|
-
if !blankOrHash(strings.TrimSpace(line)) {
|
|
171
|
-
return true
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return false
|
|
175
|
-
}
|
|
176
|
-
|
|
177
78
|
// stripFrontmatter removes a well-formed leading frontmatter block when
|
|
178
79
|
// determining whether an artifact contains substantive body content. The
|
|
179
80
|
// closing fence must be a line that is exactly `---`.
|
|
@@ -191,7 +92,7 @@ func stripFrontmatter(raw []byte) []byte {
|
|
|
191
92
|
}
|
|
192
93
|
}
|
|
193
94
|
if end < 0 {
|
|
194
|
-
return raw
|
|
95
|
+
return raw
|
|
195
96
|
}
|
|
196
97
|
return []byte(strings.Join(lines[end+1:], "\n"))
|
|
197
98
|
}
|