devrites 6.0.2 → 6.0.3
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 +10 -0
- package/README.md +1 -1
- package/docs/adr/0034-workflow-artifact-byte-corrections-get-code-review.md +60 -0
- package/docs/adr/README.md +2 -1
- package/docs/engine/commands.md +6 -6
- package/engine/help.go +12 -7
- package/engine/help_test.go +1 -0
- package/engine/internal/gate/readiness_binding.go +19 -0
- package/engine/internal/lib/cli_observe.go +16 -0
- package/engine/internal/lib/context.go +191 -19
- package/engine/internal/lib/context_next_test.go +186 -0
- package/engine/internal/lib/dispatch.go +146 -11
- package/engine/internal/lib/drift.go +10 -2
- package/engine/internal/lib/drift_test.go +29 -0
- package/engine/internal/lib/slicecheck.go +19 -0
- package/engine/internal/lib/slicecheck_test.go +41 -0
- package/engine/internal/lib/windows.go +109 -13
- package/engine/internal/lib/windows_test.go +85 -0
- package/engine/main.go +2 -2
- package/pack/.claude/agents/devrites-slice-wright.md +3 -1
- package/pack/.claude/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
- package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/README.md +1 -1
- package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/code-review.md +3 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/glossary.md +1 -1
- package/pack/.claude/skills/devrites-lib/reference/standards/security.md +1 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/windows.md +11 -3
- package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
- package/pack/.claude/skills/rite-build/SKILL.md +1 -1
- package/pack/.claude/skills/rite-build/reference/one-slice-cycle.md +3 -2
- package/pack/.claude/skills/rite-build/reference/parallel-batch.md +5 -3
- package/pack/.claude/skills/rite-build/reference/phase-contract.md +5 -3
- package/pack/.claude/skills/rite-build/reference/spec-drift-guard.md +3 -1
- package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +23 -9
- package/pack/.claude/skills/rite-define/SKILL.md +2 -1
- package/pack/.claude/skills/rite-plan/SKILL.md +2 -1
- package/pack/.claude/skills/rite-pressure-test/SKILL.md +6 -3
- package/pack/.claude/skills/rite-vet/SKILL.md +9 -1
- package/pack/.claude/skills/rite-vet/reference/review-axes.md +6 -2
- package/pack/generated/claude/agents/devrites-slice-wright.md +3 -1
- package/pack/generated/claude/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
- package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/README.md +1 -1
- package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/code-review.md +3 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/glossary.md +1 -1
- package/pack/generated/claude/skills/devrites-lib/reference/standards/security.md +1 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/windows.md +11 -3
- package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
- package/pack/generated/claude/skills/rite-build/SKILL.md +1 -1
- package/pack/generated/claude/skills/rite-build/reference/one-slice-cycle.md +3 -2
- package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +5 -3
- package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +5 -3
- package/pack/generated/claude/skills/rite-build/reference/spec-drift-guard.md +3 -1
- package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +23 -9
- package/pack/generated/claude/skills/rite-define/SKILL.md +2 -1
- package/pack/generated/claude/skills/rite-plan/SKILL.md +2 -1
- package/pack/generated/claude/skills/rite-pressure-test/SKILL.md +6 -3
- package/pack/generated/claude/skills/rite-vet/SKILL.md +9 -1
- package/pack/generated/claude/skills/rite-vet/reference/review-axes.md +6 -2
- package/pack/generated/codex/agents/devrites-slice-wright.toml +3 -1
- package/pack/generated/codex/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
- package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/README.md +1 -1
- package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/code-review.md +3 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/glossary.md +1 -1
- package/pack/generated/codex/skills/devrites-lib/reference/standards/security.md +1 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/windows.md +11 -3
- package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
- package/pack/generated/codex/skills/rite-build/SKILL.md +1 -1
- package/pack/generated/codex/skills/rite-build/reference/one-slice-cycle.md +3 -2
- package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +5 -3
- package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +5 -3
- package/pack/generated/codex/skills/rite-build/reference/spec-drift-guard.md +3 -1
- package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +23 -9
- package/pack/generated/codex/skills/rite-define/SKILL.md +2 -1
- package/pack/generated/codex/skills/rite-plan/SKILL.md +2 -1
- package/pack/generated/codex/skills/rite-pressure-test/SKILL.md +6 -3
- package/pack/generated/codex/skills/rite-vet/SKILL.md +9 -1
- package/pack/generated/codex/skills/rite-vet/reference/review-axes.md +6 -2
- package/pack/generated/devin/agents/devrites-slice-wright.md +3 -1
- package/pack/generated/devin/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
- package/pack/generated/devin/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
- package/pack/generated/devin/skills/devrites-lib/reference/standards/README.md +1 -1
- package/pack/generated/devin/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
- package/pack/generated/devin/skills/devrites-lib/reference/standards/code-review.md +3 -0
- package/pack/generated/devin/skills/devrites-lib/reference/standards/glossary.md +1 -1
- package/pack/generated/devin/skills/devrites-lib/reference/standards/security.md +1 -0
- package/pack/generated/devin/skills/devrites-lib/reference/standards/windows.md +11 -3
- package/pack/generated/devin/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
- package/pack/generated/devin/skills/rite-build/SKILL.md +1 -1
- package/pack/generated/devin/skills/rite-build/reference/one-slice-cycle.md +3 -2
- package/pack/generated/devin/skills/rite-build/reference/parallel-batch.md +5 -3
- package/pack/generated/devin/skills/rite-build/reference/phase-contract.md +5 -3
- package/pack/generated/devin/skills/rite-build/reference/spec-drift-guard.md +3 -1
- package/pack/generated/devin/skills/rite-build/reference/wright-dispatch.md +23 -9
- package/pack/generated/devin/skills/rite-define/SKILL.md +2 -1
- package/pack/generated/devin/skills/rite-plan/SKILL.md +2 -1
- package/pack/generated/devin/skills/rite-pressure-test/SKILL.md +6 -3
- package/pack/generated/devin/skills/rite-vet/SKILL.md +9 -1
- package/pack/generated/devin/skills/rite-vet/reference/review-axes.md +6 -2
- package/pack/generated/omp/agents/devrites-slice-wright.md +3 -1
- package/pack/generated/omp/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
- package/pack/generated/omp/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
- package/pack/generated/omp/skills/devrites-lib/reference/standards/README.md +1 -1
- package/pack/generated/omp/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
- package/pack/generated/omp/skills/devrites-lib/reference/standards/code-review.md +3 -0
- package/pack/generated/omp/skills/devrites-lib/reference/standards/glossary.md +1 -1
- package/pack/generated/omp/skills/devrites-lib/reference/standards/security.md +1 -0
- package/pack/generated/omp/skills/devrites-lib/reference/standards/windows.md +11 -3
- package/pack/generated/omp/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
- package/pack/generated/omp/skills/rite-build/SKILL.md +1 -1
- package/pack/generated/omp/skills/rite-build/reference/one-slice-cycle.md +3 -2
- package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +5 -3
- package/pack/generated/omp/skills/rite-build/reference/phase-contract.md +5 -3
- package/pack/generated/omp/skills/rite-build/reference/spec-drift-guard.md +3 -1
- package/pack/generated/omp/skills/rite-build/reference/wright-dispatch.md +23 -9
- package/pack/generated/omp/skills/rite-define/SKILL.md +2 -1
- package/pack/generated/omp/skills/rite-plan/SKILL.md +2 -1
- package/pack/generated/omp/skills/rite-pressure-test/SKILL.md +6 -3
- package/pack/generated/omp/skills/rite-vet/SKILL.md +9 -1
- package/pack/generated/omp/skills/rite-vet/reference/review-axes.md +6 -2
- package/pack/generated/pi/agents/devrites-slice-wright.md +3 -1
- package/pack/generated/pi/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
- package/pack/generated/pi/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
- package/pack/generated/pi/skills/devrites-lib/reference/standards/README.md +1 -1
- package/pack/generated/pi/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
- package/pack/generated/pi/skills/devrites-lib/reference/standards/code-review.md +3 -0
- package/pack/generated/pi/skills/devrites-lib/reference/standards/glossary.md +1 -1
- package/pack/generated/pi/skills/devrites-lib/reference/standards/security.md +1 -0
- package/pack/generated/pi/skills/devrites-lib/reference/standards/windows.md +11 -3
- package/pack/generated/pi/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
- package/pack/generated/pi/skills/rite-build/SKILL.md +1 -1
- package/pack/generated/pi/skills/rite-build/reference/one-slice-cycle.md +3 -2
- package/pack/generated/pi/skills/rite-build/reference/parallel-batch.md +5 -3
- package/pack/generated/pi/skills/rite-build/reference/phase-contract.md +5 -3
- package/pack/generated/pi/skills/rite-build/reference/spec-drift-guard.md +3 -1
- package/pack/generated/pi/skills/rite-build/reference/wright-dispatch.md +23 -9
- package/pack/generated/pi/skills/rite-define/SKILL.md +2 -1
- package/pack/generated/pi/skills/rite-plan/SKILL.md +2 -1
- package/pack/generated/pi/skills/rite-pressure-test/SKILL.md +6 -3
- package/pack/generated/pi/skills/rite-vet/SKILL.md +9 -1
- package/pack/generated/pi/skills/rite-vet/reference/review-axes.md +6 -2
- package/package.json +1 -1
|
@@ -672,3 +672,189 @@ func TestContextWorkspaceIncludeMarkerStaysLiteral(t *testing.T) {
|
|
|
672
672
|
t.Fatalf("workspace include marker not kept literal:\n%s", text)
|
|
673
673
|
}
|
|
674
674
|
}
|
|
675
|
+
|
|
676
|
+
func TestContextSliceScopesWorkspaceAndRole(t *testing.T) {
|
|
677
|
+
root, featureDir := newWorkspace(t, "build")
|
|
678
|
+
skills := newSkillsRoot(t)
|
|
679
|
+
writeFile(t, filepath.Join(skills, "rite-build", "reference", "root-procedure.md"), "# root procedure body\n")
|
|
680
|
+
writeFile(t, filepath.Join(skills, "rite-build", "SKILL.md"), "---\nname: rite-build\n---\n<!-- loads: {\"always\":[\"devrites-lib/reference/standards/core.md\",\"rite-build/reference/root-procedure.md\"],\"rootOnly\":[\"rite-build/reference/root-procedure.md\"],\"workspace\":[\"tasks.md\",\"decisions.md\",\"spec.md\"]} -->\n# build\n")
|
|
681
|
+
writeFile(t, filepath.Join(featureDir, "tasks.md"), "# Tasks\n\n## SLICE-001 First\nGoal: first body\n\n## SLICE-002 Second\nGoal: second body\nSatisfies: AC-001\nDecisions: DEC-010\ndepends_on: [SLICE-001]\n")
|
|
682
|
+
var decisions strings.Builder
|
|
683
|
+
decisions.WriteString("# Decisions\n")
|
|
684
|
+
for i := 0; i < 250; i++ {
|
|
685
|
+
decisions.WriteString("filler decision row\n")
|
|
686
|
+
}
|
|
687
|
+
decisions.WriteString("## DEC-010 Use the store\nDEC-0100 is a different id\n")
|
|
688
|
+
writeFile(t, filepath.Join(featureDir, "decisions.md"), decisions.String())
|
|
689
|
+
writeFile(t, filepath.Join(featureDir, "spec.md"), "# spec\nAC-001 small spec body\n")
|
|
690
|
+
|
|
691
|
+
stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
|
|
692
|
+
args := []string{"feat", "--phase", "build", "--role", "slice-wright", "--slice", "SLICE-002", "--skills-root", skills}
|
|
693
|
+
if code := RunContext(root, args, stdout, stderr); code != 0 {
|
|
694
|
+
t.Fatalf("code=%d\n%s%s", code, stdout, stderr)
|
|
695
|
+
}
|
|
696
|
+
if !strings.Contains(stdout.String(), "scope: slice SLICE-002 (1 whole, 2 by-reference)") {
|
|
697
|
+
t.Fatalf("scope line missing:\n%s", stdout)
|
|
698
|
+
}
|
|
699
|
+
raw, err := os.ReadFile(filepath.Join(featureDir, "ctx", "build-slice-wright.SLICE-002.bundle.md"))
|
|
700
|
+
if err != nil {
|
|
701
|
+
t.Fatalf("bundle: %v", err)
|
|
702
|
+
}
|
|
703
|
+
text := string(raw)
|
|
704
|
+
for _, want := range []string{
|
|
705
|
+
"Goal: second body",
|
|
706
|
+
"workspace/decisions.md (by reference)",
|
|
707
|
+
"/work/feat/decisions.md sha256: ",
|
|
708
|
+
"- L252 ## DEC-010 Use the store",
|
|
709
|
+
"- DEC-010: L252\n",
|
|
710
|
+
"- SLICE-001: L3 L10\n",
|
|
711
|
+
"- AC-001: L8\n",
|
|
712
|
+
"small spec body",
|
|
713
|
+
"===== root-only (not inlined for role slice-wright) =====\nrite-build/reference/root-procedure.md",
|
|
714
|
+
} {
|
|
715
|
+
if !strings.Contains(text, want) {
|
|
716
|
+
t.Fatalf("bundle missing %q:\n%s", want, text)
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
for _, absent := range []string{"Goal: first body", "filler decision row", "root procedure body"} {
|
|
720
|
+
if strings.Contains(text, absent) {
|
|
721
|
+
t.Fatalf("bundle inlined %q:\n%s", absent, text)
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if strings.Index(text, "# wright") > strings.Index(text, "# core") {
|
|
725
|
+
t.Fatalf("role contract must lead the bundle:\n%s", text)
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Without --role the root still gets its procedure inline.
|
|
729
|
+
stdout.Reset()
|
|
730
|
+
if code := RunContext(root, []string{"feat", "--phase", "build", "--skills-root", skills}, stdout, stderr); code != 0 {
|
|
731
|
+
t.Fatalf("phase bundle code=%d\n%s", code, stderr)
|
|
732
|
+
}
|
|
733
|
+
phase, err := os.ReadFile(filepath.Join(featureDir, "ctx", "build.bundle.md"))
|
|
734
|
+
if err != nil || !strings.Contains(string(phase), "root procedure body") || !strings.Contains(string(phase), "Goal: first body") {
|
|
735
|
+
t.Fatalf("phase bundle must stay whole: err=%v\n%s", err, phase)
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
func TestContextSliceRejectsBadOrUnknownID(t *testing.T) {
|
|
740
|
+
root, featureDir := newWorkspace(t, "build")
|
|
741
|
+
skills := newSkillsRoot(t)
|
|
742
|
+
writeFile(t, filepath.Join(featureDir, "tasks.md"), "# Tasks\n\n## SLICE-001 First\nGoal: x\n")
|
|
743
|
+
stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
|
|
744
|
+
if code := RunContext(root, []string{"feat", "--phase", "build", "--slice", "slice-1", "--skills-root", skills}, stdout, stderr); code != 2 {
|
|
745
|
+
t.Fatalf("malformed id: code=%d want 2\n%s", code, stderr)
|
|
746
|
+
}
|
|
747
|
+
if code := RunContext(root, []string{"--skill", "rite-build", "--slice", "SLICE-001", "--out", "x.md", "--skills-root", skills}, stdout, stderr); code != 2 {
|
|
748
|
+
t.Fatalf("slice without slug: code=%d want 2\n%s", code, stderr)
|
|
749
|
+
}
|
|
750
|
+
stdout.Reset()
|
|
751
|
+
if code := RunContext(root, []string{"feat", "--phase", "build", "--slice", "SLICE-009", "--skills-root", skills}, stdout, stderr); code != 3 || !strings.Contains(stdout.String(), "BLOCKED: SLICE-009 not found") {
|
|
752
|
+
t.Fatalf("unknown slice: code=%d\n%s", code, stdout)
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
func TestDispatchWrightRoundCap(t *testing.T) {
|
|
757
|
+
root, featureDir := newWorkspace(t, "build")
|
|
758
|
+
run := func(args ...string) (int, string) {
|
|
759
|
+
stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
|
|
760
|
+
code := RunDispatch(root, append([]string{"feat"}, args...), stdout, stderr)
|
|
761
|
+
return code, stdout.String() + stderr.String()
|
|
762
|
+
}
|
|
763
|
+
open := func(wave string, extra ...string) (int, string) {
|
|
764
|
+
return run(append([]string{"open", "--phase", "build", "--wave", wave, "--role", "slice-wright", "--slice", "SLICE-001"}, extra...)...)
|
|
765
|
+
}
|
|
766
|
+
if code, out := run("open", "--phase", "build", "--wave", "w0", "--role", "slice-wright"); code != 2 || !strings.Contains(out, "needs --slice") {
|
|
767
|
+
t.Fatalf("missing --slice: code=%d\n%s", code, out)
|
|
768
|
+
}
|
|
769
|
+
if code, out := run("open", "--phase", "vet", "--wave", "v1", "--role", "plan-reviewer"); code != 0 {
|
|
770
|
+
t.Fatalf("non-build wave must not need --slice: code=%d\n%s", code, out)
|
|
771
|
+
}
|
|
772
|
+
for i := 1; i <= wrightRoundsPerReview; i++ {
|
|
773
|
+
code, out := open("r" + string(rune('0'+i)))
|
|
774
|
+
if code != 0 || !strings.Contains(out, "SLICE-001 wright round "+string(rune('0'+i))) {
|
|
775
|
+
t.Fatalf("round %d: code=%d\n%s", i, code, out)
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if code, out := run("abandon", "--wave", "r5", "--reason", "orphaned"); !strings.Contains(out, "wave r5 abandoned") {
|
|
779
|
+
t.Fatalf("abandon: code=%d\n%s", code, out)
|
|
780
|
+
}
|
|
781
|
+
if code, out := open("r5b"); code != 0 || !strings.Contains(out, "wright round 5") {
|
|
782
|
+
t.Fatalf("abandoned wave must not count: code=%d\n%s", code, out)
|
|
783
|
+
}
|
|
784
|
+
if code, out := open("r6"); code != 3 || !strings.Contains(out, "BLOCKED: SLICE-001 reached 5 wright rounds") {
|
|
785
|
+
t.Fatalf("round 6 must block: code=%d\n%s", code, out)
|
|
786
|
+
}
|
|
787
|
+
writeFile(t, filepath.Join(featureDir, "drift.md"), "# Drift\n\n## D-010 — SLICE-002 unrelated\n\n## D-011 — SLICE-001 reslice review: no smaller AC-preserving split\n")
|
|
788
|
+
if code, out := open("r6", "--reslice-review", "D-099"); code != 3 {
|
|
789
|
+
t.Fatalf("unknown drift id: code=%d\n%s", code, out)
|
|
790
|
+
}
|
|
791
|
+
if code, out := open("r6", "--reslice-review", "D-010"); code != 3 || !strings.Contains(out, "naming SLICE-001") {
|
|
792
|
+
t.Fatalf("drift heading for another slice: code=%d\n%s", code, out)
|
|
793
|
+
}
|
|
794
|
+
if code, out := open("r6", "--reslice-review", "D-011"); code != 0 || !strings.Contains(out, "wright round 6") {
|
|
795
|
+
t.Fatalf("valid review must admit: code=%d\n%s", code, out)
|
|
796
|
+
}
|
|
797
|
+
for i := 7; i <= 2*wrightRoundsPerReview; i++ {
|
|
798
|
+
if code, out := open("r" + string(rune('a'+i))); code != 0 {
|
|
799
|
+
t.Fatalf("round %d inside the reviewed window: code=%d\n%s", i, code, out)
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
if code, out := open("r11", "--reslice-review", "D-011"); code != 3 || !strings.Contains(out, "already admitted") {
|
|
803
|
+
t.Fatalf("reused review: code=%d\n%s", code, out)
|
|
804
|
+
}
|
|
805
|
+
if code, out := run("open", "--phase", "build", "--wave", "s2", "--role", "slice-wright", "--slice", "SLICE-002", "--reslice-review", "D-010"); code != 2 {
|
|
806
|
+
t.Fatalf("review before the cap must be refused: code=%d\n%s", code, out)
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
func TestDispatchVetRoundsCountSeparately(t *testing.T) {
|
|
811
|
+
root, featureDir := newWorkspace(t, "build")
|
|
812
|
+
run := func(args ...string) (int, string) {
|
|
813
|
+
stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
|
|
814
|
+
code := RunDispatch(root, append([]string{"feat"}, args...), stdout, stderr)
|
|
815
|
+
return code, stdout.String() + stderr.String()
|
|
816
|
+
}
|
|
817
|
+
for i := 0; i < 7; i++ {
|
|
818
|
+
if code, out := run("open", "--phase", "vet", "--wave", "full"+string(rune('a'+i)), "--role", "plan-reviewer"); code != 0 {
|
|
819
|
+
t.Fatalf("full Vet without --slice stays uncapped: code=%d\n%s", code, out)
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
if code, out := run("open", "--phase", "build", "--wave", "w1", "--role", "slice-wright", "--slice", "SLICE-001"); code != 0 || !strings.Contains(out, "wright round 1") {
|
|
823
|
+
t.Fatalf("wright round must not see vet waves: code=%d\n%s", code, out)
|
|
824
|
+
}
|
|
825
|
+
for i := 1; i <= wrightRoundsPerReview; i++ {
|
|
826
|
+
if code, out := run("open", "--phase", "vet", "--wave", "v"+string(rune('0'+i)), "--role", "plan-reviewer", "--slice", "SLICE-001"); code != 0 || !strings.Contains(out, "vet round "+string(rune('0'+i))) {
|
|
827
|
+
t.Fatalf("vet round %d: code=%d\n%s", i, code, out)
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
if code, out := run("open", "--phase", "vet", "--wave", "v6", "--role", "plan-reviewer", "--slice", "SLICE-001"); code != 3 || !strings.Contains(out, "reached 5 vet rounds") {
|
|
831
|
+
t.Fatalf("vet round 6 must block: code=%d\n%s", code, out)
|
|
832
|
+
}
|
|
833
|
+
writeFile(t, filepath.Join(featureDir, "drift.md"), "## D-001 — SLICE-001 reslice review: proof seam already minimal\n")
|
|
834
|
+
if code, out := run("open", "--phase", "vet", "--wave", "v6", "--role", "plan-reviewer", "--slice", "SLICE-001", "--reslice-review", "D-001"); code != 0 {
|
|
835
|
+
t.Fatalf("reviewed vet round 6: code=%d\n%s", code, out)
|
|
836
|
+
}
|
|
837
|
+
if code, out := run("open", "--phase", "plan", "--wave", "p1", "--role", "plan-drafter", "--slice", "SLICE-001"); code != 2 {
|
|
838
|
+
t.Fatalf("--slice on a plan wave: code=%d\n%s", code, out)
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
func TestContextRepeatedSliceUnion(t *testing.T) {
|
|
843
|
+
root, featureDir := newWorkspace(t, "build")
|
|
844
|
+
skills := newSkillsRoot(t)
|
|
845
|
+
writeFile(t, filepath.Join(skills, "rite-build", "SKILL.md"), "---\nname: rite-build\n---\n<!-- loads: {\"always\":[\"devrites-lib/reference/standards/core.md\"],\"workspace\":[\"tasks.md\"]} -->\n# build\n")
|
|
846
|
+
writeFile(t, filepath.Join(featureDir, "tasks.md"), "# Tasks\n\n## SLICE-001 A\nGoal: alpha body\n\n## SLICE-002 B\nGoal: beta body\n\n## SLICE-003 C\nGoal: gamma body\n")
|
|
847
|
+
stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
|
|
848
|
+
args := []string{"feat", "--phase", "build", "--slice", "SLICE-001", "--slice", "SLICE-003", "--skills-root", skills}
|
|
849
|
+
if code := RunContext(root, args, stdout, stderr); code != 0 {
|
|
850
|
+
t.Fatalf("code=%d\n%s%s", code, stdout, stderr)
|
|
851
|
+
}
|
|
852
|
+
raw, err := os.ReadFile(filepath.Join(featureDir, "ctx", "build.SLICE-001+SLICE-003.bundle.md"))
|
|
853
|
+
if err != nil {
|
|
854
|
+
t.Fatal(err)
|
|
855
|
+
}
|
|
856
|
+
text := string(raw)
|
|
857
|
+
if !strings.Contains(text, "alpha body") || !strings.Contains(text, "gamma body") || strings.Contains(text, "beta body") {
|
|
858
|
+
t.Fatalf("union of slice blocks wrong:\n%s", text)
|
|
859
|
+
}
|
|
860
|
+
}
|
|
@@ -6,6 +6,8 @@ import (
|
|
|
6
6
|
"io"
|
|
7
7
|
"os"
|
|
8
8
|
"path/filepath"
|
|
9
|
+
"regexp"
|
|
10
|
+
"slices"
|
|
9
11
|
"sort"
|
|
10
12
|
"strings"
|
|
11
13
|
"time"
|
|
@@ -19,24 +21,38 @@ import (
|
|
|
19
21
|
// fan-out cannot silently degrade to serial (open -> every role started ->
|
|
20
22
|
// seal -> returns -> complete). start/return also auto-record metrics events.
|
|
21
23
|
//
|
|
22
|
-
// devrites-engine dispatch <slug> <sub> [--phase p] [--wave w] [--role r] [--handle h] [--reason s]
|
|
24
|
+
// devrites-engine dispatch <slug> <sub> [--phase p] [--wave w] [--role r] [--handle h] [--reason s] [--slice SLICE-###] [--reslice-review <DRIFT-ID>]
|
|
23
25
|
|
|
24
|
-
const dispatchUsage = `usage: devrites-engine dispatch <slug> <open|start|seal|return|status|abandon> [--phase p] [--wave w] [--role r] [--handle h] [--reason s]
|
|
26
|
+
const dispatchUsage = `usage: devrites-engine dispatch <slug> <open|start|seal|return|status|abandon> [--phase p] [--wave w] [--role r] [--handle h] [--reason s] [--slice SLICE-###] [--reslice-review <DRIFT-ID>]
|
|
25
27
|
|
|
26
28
|
Wave barrier for parallel agent dispatch: seal fails until every declared
|
|
27
29
|
role has a distinct handle; return fails before seal; status exits nonzero
|
|
28
30
|
while a wave is open, sealed, or abandoned.
|
|
31
|
+
|
|
32
|
+
A build wave with a wright role must name its slice(s) with --slice; a vet
|
|
33
|
+
wave may (slice-bound recheck). Every 5 rounds of one kind on one slice, the
|
|
34
|
+
next open is blocked until --reslice-review names a drift.md
|
|
35
|
+
"## <DRIFT-ID>" heading for that slice.
|
|
29
36
|
`
|
|
30
37
|
|
|
38
|
+
// wrightRoundsPerReview is how many build wright rounds (or slice-bound Vet
|
|
39
|
+
// rechecks) one slice may take before (and after each) reslice review.
|
|
40
|
+
// Recovery itself stays unbounded; the cap only forces the split question
|
|
41
|
+
// onto the record.
|
|
42
|
+
const wrightRoundsPerReview = 5
|
|
43
|
+
|
|
31
44
|
type dispatchFile struct {
|
|
32
45
|
Waves map[string]*dispatchWave `json:"waves"`
|
|
33
46
|
}
|
|
34
47
|
|
|
35
48
|
type dispatchWave struct {
|
|
36
|
-
Phase
|
|
37
|
-
State
|
|
38
|
-
Reason
|
|
39
|
-
Roles
|
|
49
|
+
Phase string `json:"phase"`
|
|
50
|
+
State string `json:"state"` // open, sealed, abandoned, complete
|
|
51
|
+
Reason string `json:"reason,omitempty"`
|
|
52
|
+
Roles map[string]*dispatchRole `json:"roles"`
|
|
53
|
+
Slices []string `json:"slices,omitempty"`
|
|
54
|
+
ResliceReview string `json:"reslice_review,omitempty"`
|
|
55
|
+
ResliceReviewed []string `json:"reslice_reviewed,omitempty"`
|
|
40
56
|
}
|
|
41
57
|
|
|
42
58
|
type dispatchRole struct {
|
|
@@ -89,8 +105,8 @@ func nowStamp() string { return time.Now().UTC().Format(time.RFC3339) }
|
|
|
89
105
|
|
|
90
106
|
// RunDispatch implements the dispatch wave state machine.
|
|
91
107
|
func RunDispatch(root string, args []string, stdout, stderr io.Writer) int {
|
|
92
|
-
var slug, sub, phase, wave, role, handle, reason string
|
|
93
|
-
var roles []string
|
|
108
|
+
var slug, sub, phase, wave, role, handle, reason, review string
|
|
109
|
+
var roles, sliceIDs []string
|
|
94
110
|
var positional []string
|
|
95
111
|
for i := 0; i < len(args); i++ {
|
|
96
112
|
value := argAt(args, i+1)
|
|
@@ -106,6 +122,10 @@ func RunDispatch(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
106
122
|
handle = value
|
|
107
123
|
case "--reason":
|
|
108
124
|
reason = value
|
|
125
|
+
case "--slice":
|
|
126
|
+
sliceIDs = append(sliceIDs, value)
|
|
127
|
+
case "--reslice-review":
|
|
128
|
+
review = value
|
|
109
129
|
default:
|
|
110
130
|
if strings.HasPrefix(args[i], "-") {
|
|
111
131
|
fmt.Fprintf(stderr, "dispatch: unknown flag %q\n", args[i])
|
|
@@ -129,7 +149,7 @@ func RunDispatch(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
129
149
|
// an update and strand the wave short of complete.
|
|
130
150
|
code := 0
|
|
131
151
|
if err := state.WithFeatureLock(root, slug, func() error {
|
|
132
|
-
code = dispatchLocked(root, slug, sub, phase, wave, role, handle, reason, roles, stdout, stderr)
|
|
152
|
+
code = dispatchLocked(root, slug, sub, phase, wave, role, handle, reason, roles, sliceIDs, review, stdout, stderr)
|
|
133
153
|
return nil
|
|
134
154
|
}); err != nil {
|
|
135
155
|
fmt.Fprintf(stderr, "dispatch: %v\n", err)
|
|
@@ -138,7 +158,7 @@ func RunDispatch(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
138
158
|
return code
|
|
139
159
|
}
|
|
140
160
|
|
|
141
|
-
func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, roles []string, stdout, stderr io.Writer) int {
|
|
161
|
+
func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, roles, sliceIDs []string, review string, stdout, stderr io.Writer) int {
|
|
142
162
|
f, path, err := loadDispatch(root, slug)
|
|
143
163
|
if err != nil {
|
|
144
164
|
fmt.Fprintf(stderr, "dispatch: %v\n", err)
|
|
@@ -166,18 +186,25 @@ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, r
|
|
|
166
186
|
if w := getWave(); w != nil && w.State != "abandoned" {
|
|
167
187
|
return fail("wave %q already exists (state %s)", wave, w.State)
|
|
168
188
|
}
|
|
169
|
-
w := &dispatchWave{Phase: phase, State: "open", Roles: map[string]*dispatchRole{}}
|
|
189
|
+
w := &dispatchWave{Phase: phase, State: "open", Roles: map[string]*dispatchRole{}, Slices: sliceIDs}
|
|
170
190
|
for _, r := range roles {
|
|
171
191
|
if w.Roles[r] != nil {
|
|
172
192
|
return fail("duplicate role %q in wave", r)
|
|
173
193
|
}
|
|
174
194
|
w.Roles[r] = &dispatchRole{}
|
|
175
195
|
}
|
|
196
|
+
rounds, code := admitWrightRounds(root, slug, f, w, review, stderr)
|
|
197
|
+
if code != 0 {
|
|
198
|
+
return code
|
|
199
|
+
}
|
|
176
200
|
f.Waves[wave] = w
|
|
177
201
|
if err := saveDispatch(path, f); err != nil {
|
|
178
202
|
return fail("%v", err)
|
|
179
203
|
}
|
|
180
204
|
fmt.Fprintf(stdout, "dispatch: wave %s open, %d roles declared\n", wave, len(roles))
|
|
205
|
+
for _, line := range rounds {
|
|
206
|
+
fmt.Fprintln(stdout, line)
|
|
207
|
+
}
|
|
181
208
|
return 0
|
|
182
209
|
|
|
183
210
|
case "start":
|
|
@@ -322,6 +349,114 @@ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, r
|
|
|
322
349
|
return 2
|
|
323
350
|
}
|
|
324
351
|
|
|
352
|
+
// roundKind names the capped loop a wave belongs to: build wright rounds or
|
|
353
|
+
// slice-bound Vet rechecks. Other waves are uncapped.
|
|
354
|
+
func roundKind(w *dispatchWave) string {
|
|
355
|
+
switch {
|
|
356
|
+
case isBuildWrightWave(w):
|
|
357
|
+
return "wright"
|
|
358
|
+
case strings.EqualFold(w.Phase, "vet") && len(w.Slices) > 0:
|
|
359
|
+
return "vet"
|
|
360
|
+
}
|
|
361
|
+
return ""
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
func isBuildWrightWave(w *dispatchWave) bool {
|
|
365
|
+
if w.Phase != "build" {
|
|
366
|
+
return false
|
|
367
|
+
}
|
|
368
|
+
for r := range w.Roles {
|
|
369
|
+
if strings.Contains(r, "wright") {
|
|
370
|
+
return true
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return false
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// admitWrightRounds enforces the per-slice round cap for a new build wright
|
|
377
|
+
// or slice-bound Vet wave and returns the round lines to print. A slice may
|
|
378
|
+
// take wrightRoundsPerReview rounds of each kind per reslice review (the first
|
|
379
|
+
// window is free); beyond that the open needs --reslice-review naming an
|
|
380
|
+
// unused drift.md heading for every slice over the cap.
|
|
381
|
+
func admitWrightRounds(root, slug string, f *dispatchFile, w *dispatchWave, review string, stderr io.Writer) ([]string, int) {
|
|
382
|
+
kind := roundKind(w)
|
|
383
|
+
if kind == "" {
|
|
384
|
+
if review != "" || (len(w.Slices) > 0 && !strings.EqualFold(w.Phase, "vet")) {
|
|
385
|
+
fmt.Fprintln(stderr, "dispatch: --slice/--reslice-review apply only to build wright waves and slice-bound vet waves")
|
|
386
|
+
return nil, 2
|
|
387
|
+
}
|
|
388
|
+
return nil, 0
|
|
389
|
+
}
|
|
390
|
+
if len(w.Slices) == 0 {
|
|
391
|
+
fmt.Fprintln(stderr, "dispatch: a build wright wave needs --slice SLICE-### for each slice it builds")
|
|
392
|
+
return nil, 2
|
|
393
|
+
}
|
|
394
|
+
for _, id := range w.Slices {
|
|
395
|
+
if !state.ValidSliceID(id) {
|
|
396
|
+
fmt.Fprintf(stderr, "dispatch: %q is not a SLICE-### id\n", id)
|
|
397
|
+
return nil, 2
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
var lines, overCap []string
|
|
401
|
+
for _, id := range w.Slices {
|
|
402
|
+
prior, reviews := 0, 0
|
|
403
|
+
for _, other := range f.Waves {
|
|
404
|
+
if other.State == "abandoned" || roundKind(other) != kind {
|
|
405
|
+
continue
|
|
406
|
+
}
|
|
407
|
+
if slices.Contains(other.Slices, id) {
|
|
408
|
+
prior++
|
|
409
|
+
}
|
|
410
|
+
if slices.Contains(other.ResliceReviewed, id) {
|
|
411
|
+
reviews++
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
round := prior + 1
|
|
415
|
+
if round > wrightRoundsPerReview*(1+reviews) {
|
|
416
|
+
overCap = append(overCap, id)
|
|
417
|
+
}
|
|
418
|
+
lines = append(lines, fmt.Sprintf("dispatch: %s %s round %d (reslice review every %d)", id, kind, round, wrightRoundsPerReview))
|
|
419
|
+
}
|
|
420
|
+
if len(overCap) == 0 {
|
|
421
|
+
if review != "" {
|
|
422
|
+
fmt.Fprintf(stderr, "dispatch: --reslice-review given but no slice has reached its %d-round cap\n", wrightRoundsPerReview)
|
|
423
|
+
return nil, 2
|
|
424
|
+
}
|
|
425
|
+
return lines, 0
|
|
426
|
+
}
|
|
427
|
+
if review == "" {
|
|
428
|
+
fmt.Fprintf(stderr, "dispatch: BLOCKED: %s reached %d %s rounds; run a reslice review (acceptance-preserving split or simpler proof seam, or a drift.md entry recording why neither applies) and reopen with --reslice-review <DRIFT-ID>\n",
|
|
429
|
+
strings.Join(overCap, ","), wrightRoundsPerReview, kind)
|
|
430
|
+
return nil, 3
|
|
431
|
+
}
|
|
432
|
+
for _, other := range f.Waves {
|
|
433
|
+
if other.State != "abandoned" && other.ResliceReview == review && roundKind(other) == kind {
|
|
434
|
+
fmt.Fprintf(stderr, "dispatch: BLOCKED: reslice review %s already admitted another round window\n", review)
|
|
435
|
+
return nil, 3
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
drift, err := readWorkspaceArtifact(root, slug, "drift.md")
|
|
439
|
+
if err != nil {
|
|
440
|
+
fmt.Fprintf(stderr, "dispatch: BLOCKED: reslice review %s: %v\n", review, err)
|
|
441
|
+
return nil, 3
|
|
442
|
+
}
|
|
443
|
+
heading := driftHeading(string(drift), review)
|
|
444
|
+
for _, id := range overCap {
|
|
445
|
+
if !regexp.MustCompile(`\b` + regexp.QuoteMeta(id) + `\b`).MatchString(heading) {
|
|
446
|
+
fmt.Fprintf(stderr, "dispatch: BLOCKED: drift.md has no \"## %s\" heading naming %s\n", review, id)
|
|
447
|
+
return nil, 3
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
w.ResliceReview, w.ResliceReviewed = review, overCap
|
|
451
|
+
return lines, 0
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// driftHeading returns the drift.md level-2 heading line for id, or "".
|
|
455
|
+
func driftHeading(drift, id string) string {
|
|
456
|
+
re := regexp.MustCompile(`(?m)^## ` + regexp.QuoteMeta(id) + `\b.*$`)
|
|
457
|
+
return re.FindString(drift)
|
|
458
|
+
}
|
|
459
|
+
|
|
325
460
|
func waveState(w *dispatchWave) string {
|
|
326
461
|
if w == nil {
|
|
327
462
|
return "absent"
|
|
@@ -48,7 +48,7 @@ type driftBaseline struct {
|
|
|
48
48
|
// driftDiff is one input whose state differs from the baseline.
|
|
49
49
|
type driftDiff struct {
|
|
50
50
|
Path string `json:"path"`
|
|
51
|
-
Kind string `json:"kind"` // changed | missing | added
|
|
51
|
+
Kind string `json:"kind"` // changed | missing | added | reviewed-identity-only
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
// RunCheckDrift implements `devrites-engine check drift`.
|
|
@@ -130,6 +130,10 @@ func compareDrift(workspace, baselinePath string, digests []gate.ReadinessInputD
|
|
|
130
130
|
if baseline.Binding != binding && len(diffs) == 0 {
|
|
131
131
|
fmt.Fprintln(stdout, "DRIFT: aggregate binding changed without a recorded input diff; re-record the baseline")
|
|
132
132
|
}
|
|
133
|
+
if len(diffs) == 1 && diffs[0].Kind == "reviewed-identity-only" {
|
|
134
|
+
fmt.Fprintln(stdout, "check drift: reviewed-identity-only — only the Workflow Artifact reviewed identity moved; /rite-vet may use its bytes-only recheck")
|
|
135
|
+
return 0
|
|
136
|
+
}
|
|
133
137
|
fmt.Fprintf(stdout, "check drift: %d input(s) differ — a change is legal only as a dated delta through the owning rite; otherwise restore the input or rerun /rite-vet and --record\n", len(diffs))
|
|
134
138
|
return 0
|
|
135
139
|
}
|
|
@@ -189,7 +193,11 @@ func diffInputs(base, current []gate.ReadinessInputDigest) []driftDiff {
|
|
|
189
193
|
}
|
|
190
194
|
diffs = append(diffs, driftDiff{Path: b.Path, Kind: kind})
|
|
191
195
|
case b.Present && b.SHA256 != c.SHA256:
|
|
192
|
-
|
|
196
|
+
kind := "changed"
|
|
197
|
+
if b.Normalized != "" && b.Normalized == c.Normalized {
|
|
198
|
+
kind = "reviewed-identity-only"
|
|
199
|
+
}
|
|
200
|
+
diffs = append(diffs, driftDiff{Path: b.Path, Kind: kind})
|
|
193
201
|
}
|
|
194
202
|
}
|
|
195
203
|
sort.Slice(diffs, func(i, j int) bool { return diffs[i].Path < diffs[j].Path })
|
|
@@ -104,3 +104,32 @@ func TestDriftCorruptBaselineBlocks(t *testing.T) {
|
|
|
104
104
|
t.Fatalf("code=%d out=%s", code, out)
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
|
+
|
|
108
|
+
func TestDriftClassifiesReviewedIdentityOnlyChange(t *testing.T) {
|
|
109
|
+
root, featureDir := driftWorkspace(t)
|
|
110
|
+
if code, out := checkDrift(t, root, "feat", "--record"); code != 0 {
|
|
111
|
+
t.Fatalf("record code=%d out=%s", code, out)
|
|
112
|
+
}
|
|
113
|
+
plan := filepath.Join(featureDir, "test-plan.md")
|
|
114
|
+
line := "Workflow Artifact reviewed identity: " + strings.Repeat("ab", 32) + "\n"
|
|
115
|
+
writeFile(t, plan, "# test-plan.md\n\ncontent\n"+line)
|
|
116
|
+
code, out := checkDrift(t, root, "feat")
|
|
117
|
+
if code != 0 || !strings.Contains(out, "test-plan.md reviewed-identity-only") || !strings.Contains(out, "bytes-only recheck") {
|
|
118
|
+
t.Fatalf("identity-only change not classified: code=%d\n%s", code, out)
|
|
119
|
+
}
|
|
120
|
+
// Any other edit, including a malformed identity line, is ordinary drift.
|
|
121
|
+
for _, body := range []string{
|
|
122
|
+
"# test-plan.md\n\nchanged\n" + line,
|
|
123
|
+
"# test-plan.md\n\ncontent\nWorkflow Artifact reviewed identity: not-a-digest\n",
|
|
124
|
+
} {
|
|
125
|
+
writeFile(t, plan, body)
|
|
126
|
+
if _, out := checkDrift(t, root, "feat"); !strings.Contains(out, "test-plan.md changed") || strings.Contains(out, "bytes-only") {
|
|
127
|
+
t.Fatalf("non-identity edit hidden:\n%s", out)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
writeFile(t, plan, "# test-plan.md\n\ncontent\n"+line)
|
|
131
|
+
writeFile(t, filepath.Join(featureDir, "plan.md"), "# plan.md\n\nrewritten\n")
|
|
132
|
+
if _, out := checkDrift(t, root, "feat"); strings.Contains(out, "bytes-only") {
|
|
133
|
+
t.Fatalf("a second changed input must void the bytes-only route:\n%s", out)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -29,6 +29,22 @@ var (
|
|
|
29
29
|
windowsDrivePath = regexp.MustCompile(`^[A-Za-z]:[\\/]`)
|
|
30
30
|
)
|
|
31
31
|
|
|
32
|
+
// sliceFileAdvisory is the Writer allowlist size at which a slice is flagged
|
|
33
|
+
// for an acceptance-preserving split. Advisory only: it never changes an exit
|
|
34
|
+
// code. Measured on real plans, slices at or above it averaged ~10 wright
|
|
35
|
+
// rounds versus ~3.5 below it.
|
|
36
|
+
const sliceFileAdvisory = 12
|
|
37
|
+
|
|
38
|
+
// sliceSizeAdvisory returns the Writer allowlist size and whether the slice is
|
|
39
|
+
// oversized without a recorded `Size rationale:`.
|
|
40
|
+
func sliceSizeAdvisory(block string) (int, bool) {
|
|
41
|
+
fields := sliceFields(block)
|
|
42
|
+
raw, _ := fieldValue(fields, "writer allowlist")
|
|
43
|
+
n := len(parsePathList(raw))
|
|
44
|
+
rationale, _ := fieldValue(fields, "size rationale")
|
|
45
|
+
return n, n >= sliceFileAdvisory && rationale == ""
|
|
46
|
+
}
|
|
47
|
+
|
|
32
48
|
// sliceFields extracts the single-line `Field: value` pairs of a slice block.
|
|
33
49
|
func sliceFields(block string) map[string]string {
|
|
34
50
|
fields := map[string]string{}
|
|
@@ -169,6 +185,9 @@ func RunCheckSlice(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
169
185
|
problems = append(problems, "Tests/proof field missing or empty — no evidence plan for the slice")
|
|
170
186
|
}
|
|
171
187
|
|
|
188
|
+
if n, over := sliceSizeAdvisory(block); over {
|
|
189
|
+
fmt.Fprintf(stdout, "check slice: ADVISORY size: %s has %d Writer allowlist files (>= %d); consider an acceptance-preserving split\n", sliceID, n, sliceFileAdvisory)
|
|
190
|
+
}
|
|
172
191
|
if len(problems) > 0 {
|
|
173
192
|
fmt.Fprintf(stdout, "check slice: %s BLOCKED\n", sliceID)
|
|
174
193
|
for _, p := range problems {
|
|
@@ -127,3 +127,44 @@ func TestCheckSliceTouchedComparesNormalizedPaths(t *testing.T) {
|
|
|
127
127
|
t.Fatalf("code=%d want 0\n%s%s", code, stdout, stderr)
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
|
+
|
|
131
|
+
func bigSliceTasks(rationale string) string {
|
|
132
|
+
var paths []string
|
|
133
|
+
for i := 1; i <= sliceFileAdvisory; i++ {
|
|
134
|
+
paths = append(paths, "`src/f"+strings.Repeat("x", i)+".go`")
|
|
135
|
+
}
|
|
136
|
+
return "# Tasks\n\n## SLICE-001 Big\nGoal: do it.\nSatisfies: AC-001\ndepends_on: []\nWriter allowlist: " +
|
|
137
|
+
strings.Join(paths, ", ") + "\nTests/proof: go test ./...\n" + rationale +
|
|
138
|
+
"\n## SLICE-002 Small\nGoal: tiny.\nSatisfies: AC-001\ndepends_on: [SLICE-001]\nWriter allowlist: `src/a.go`\nTests/proof: go test ./...\n"
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
func TestSliceSizeAdvisoryNeverBlocks(t *testing.T) {
|
|
142
|
+
root := sliceWorkspace(t, bigSliceTasks(""), "## Acceptance criteria\n- AC-001 works\n")
|
|
143
|
+
stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
|
|
144
|
+
if code := RunCheckSlice(root, []string{"feat", "SLICE-001"}, stdout, stderr); code != 0 {
|
|
145
|
+
t.Fatalf("advisory must not block: code=%d\n%s%s", code, stdout, stderr)
|
|
146
|
+
}
|
|
147
|
+
if !strings.Contains(stdout.String(), "ADVISORY size: SLICE-001 has 12 Writer allowlist files") {
|
|
148
|
+
t.Fatalf("advisory missing:\n%s", stdout)
|
|
149
|
+
}
|
|
150
|
+
stdout.Reset()
|
|
151
|
+
if code := RunTaskGraphCheck(root, "feat", stdout, stderr); code != 0 {
|
|
152
|
+
t.Fatalf("task-graph code=%d\n%s%s", code, stdout, stderr)
|
|
153
|
+
}
|
|
154
|
+
out := stdout.String()
|
|
155
|
+
if !strings.Contains(out, "task-graph: ADVISORY size: SLICE-001 has 12") || strings.Contains(out, "SLICE-002 has") {
|
|
156
|
+
t.Fatalf("task-graph advisory wrong:\n%s", out)
|
|
157
|
+
}
|
|
158
|
+
stdout.Reset()
|
|
159
|
+
if code := RunMetrics(root, []string{"summary", "feat"}, stdout, stderr); code != 0 || !strings.Contains(stdout.String(), "size-advisory=1") {
|
|
160
|
+
t.Fatalf("size-advisory metric not recorded: code=%d\n%s", code, stdout)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
func TestSliceSizeRationaleSilencesAdvisory(t *testing.T) {
|
|
165
|
+
root := sliceWorkspace(t, bigSliceTasks("Size rationale: one generated golden set moves together\n"), "## Acceptance criteria\n- AC-001 works\n")
|
|
166
|
+
stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
|
|
167
|
+
if code := RunTaskGraphCheck(root, "feat", stdout, stderr); code != 0 || strings.Contains(stdout.String(), "ADVISORY") {
|
|
168
|
+
t.Fatalf("rationale must silence advisory: code=%d\n%s", code, stdout)
|
|
169
|
+
}
|
|
170
|
+
}
|