devrites 3.0.2 → 3.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +1 -1
  3. package/docs/adr/0004-state-schema-phases-sections.md +44 -0
  4. package/docs/command-map.md +1 -1
  5. package/docs/engine/state-schema.md +15 -12
  6. package/docs/flow.md +1 -1
  7. package/docs/research/go-authoritative-workflow-schema-and-quality-2026-07-20.md +212 -0
  8. package/engine/hooks_workspace.go +7 -7
  9. package/engine/internal/gate/gate.go +4 -4
  10. package/engine/internal/gate/gate_test.go +2 -2
  11. package/engine/internal/lib/cursor_compat_test.go +29 -0
  12. package/engine/internal/lib/preamble.go +1 -1
  13. package/engine/internal/lib/preamble_questions_test.go +10 -0
  14. package/engine/internal/lib/progress.go +59 -20
  15. package/engine/internal/lib/resolve.go +15 -6
  16. package/engine/internal/lib/resolve_remediation_test.go +9 -0
  17. package/engine/internal/lib/tickafk.go +2 -2
  18. package/engine/internal/migrate/migrate.go +2 -33
  19. package/engine/internal/migrate/migrate_test.go +7 -1
  20. package/engine/internal/state/cmd/workflowmanifest/main.go +54 -0
  21. package/engine/internal/state/cursor.go +42 -7
  22. package/engine/internal/state/feature.go +1 -27
  23. package/engine/internal/state/schema.go +135 -20
  24. package/engine/internal/state/snapshot.go +49 -3
  25. package/engine/internal/state/state_test.go +71 -0
  26. package/engine/internal/state/workflow_manifest.json +310 -0
  27. package/engine/internal/workflow/commands.go +0 -36
  28. package/engine/internal/workflow/commands_test.go +0 -17
  29. package/engine/testdata/golden/TestParityProgress/arg=allbuilt.golden +1 -1
  30. package/engine/testdata/golden/TestParityProgress/arg=done.golden +1 -1
  31. package/engine/testdata/golden/TestParityProgress/arg=mid.golden +1 -1
  32. package/engine/testdata/golden/TestParityProgress/arg=nophase.golden +1 -1
  33. package/engine/testdata/golden/TestParityProgress/arg=noslice.golden +1 -1
  34. package/engine/testdata/golden/TestParityProgress/arg=plan.golden +1 -1
  35. package/engine/testdata/golden/TestParityProgress/arg=seal.golden +1 -1
  36. package/engine/tests/adr_0004_required_by_phase_test.go +1 -18
  37. package/engine/tests/lib_parity_test.go +1 -1
  38. package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -3
  39. package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -3
  40. package/pack/generated/codex/AGENTS.md +1 -1
  41. package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -3
  42. package/package.json +1 -1
  43. package/scripts/codex-generate.sh +1 -1
  44. package/scripts/grade-feature.sh +5 -3
  45. package/scripts/run-outcome-evals.sh +21 -0
  46. package/scripts/validate-workspace-schema.py +9 -73
  47. package/scripts/validate.sh +10 -0
  48. package/scripts/workflow_schema.py +69 -0
@@ -12,6 +12,8 @@ import re
12
12
  import sys
13
13
  from pathlib import Path
14
14
 
15
+ from workflow_schema import PHASES, cursor_field
16
+
15
17
 
16
18
  ID_PATTERNS = {
17
19
  "AC": re.compile(r"\bAC-\d{3}\b"),
@@ -112,68 +114,6 @@ SLICE_REQUIRED_FIELDS = (
112
114
  "Done condition",
113
115
  )
114
116
 
115
- PHASE_REQUIRED = {
116
- "frame": ["state.md"],
117
- "spec": ["brief.md", "spec.md", "state.md", "decisions.md", "assumptions.md", "questions.md"],
118
- "temper": ["brief.md", "spec.md", "state.md", "decisions.md", "assumptions.md", "questions.md"],
119
- "plan": [
120
- "brief.md",
121
- "spec.md",
122
- "architecture.md",
123
- "plan.md",
124
- "tasks.md",
125
- "traceability.md",
126
- "state.md",
127
- "decisions.md",
128
- "assumptions.md",
129
- "questions.md",
130
- ],
131
- "vet": [
132
- "brief.md",
133
- "spec.md",
134
- "architecture.md",
135
- "plan.md",
136
- "tasks.md",
137
- "traceability.md",
138
- "state.md",
139
- "decisions.md",
140
- "assumptions.md",
141
- "questions.md",
142
- ],
143
- "build": [
144
- "brief.md",
145
- "spec.md",
146
- "architecture.md",
147
- "plan.md",
148
- "tasks.md",
149
- "traceability.md",
150
- "state.md",
151
- "decisions.md",
152
- "assumptions.md",
153
- "questions.md",
154
- ],
155
- "prove": [
156
- "brief.md",
157
- "spec.md",
158
- "architecture.md",
159
- "plan.md",
160
- "tasks.md",
161
- "traceability.md",
162
- "state.md",
163
- "decisions.md",
164
- "assumptions.md",
165
- "questions.md",
166
- ],
167
- "polish": [],
168
- "review": [],
169
- "seal": [],
170
- "ship": [],
171
- "done": [],
172
- }
173
- for phase in ("polish", "review", "seal", "ship", "done"):
174
- PHASE_REQUIRED[phase] = PHASE_REQUIRED["prove"]
175
-
176
-
177
117
  def read(path: Path) -> str:
178
118
  return path.read_text(encoding="utf-8")
179
119
 
@@ -198,12 +138,9 @@ def has_budget_override(text: str) -> bool:
198
138
  def phase_for(workspace: Path) -> str:
199
139
  for name in ("state.md", "status.md", "README.md", "index.md", "feature.md"):
200
140
  p = workspace / name
201
- if not p.exists():
202
- continue
203
- text = read(p)
204
- m = re.search(r"(?im)^\s*-?\s*phase\s*:\s*([a-z-]+)", text)
205
- if m:
206
- return m.group(1).lower()
141
+ value = cursor_field(p, "phase")
142
+ if value:
143
+ return value.split()[0].lower()
207
144
  return "spec"
208
145
 
209
146
 
@@ -374,9 +311,8 @@ def validate_workspace(workspace: Path) -> list[str]:
374
311
  if not workspace_index_present(workspace):
375
312
  errors.append(f"{workspace}: missing README.md/index.md/feature.md workspace map")
376
313
 
377
- required = PHASE_REQUIRED.get(phase, PHASE_REQUIRED["spec"])
378
- if phase in {"prove", "polish", "review", "seal", "ship", "done"}:
379
- required = [*required, "evidence.md", "touched-files.md"]
314
+ metadata = PHASES.get(phase, PHASES["spec"])
315
+ required = list(metadata["workspaceRequired"])
380
316
  for name in required:
381
317
  if not existing_or_alias(workspace, name):
382
318
  errors.append(f"{workspace}: phase {phase} requires {name}")
@@ -456,7 +392,7 @@ def validate_workspace(workspace: Path) -> list[str]:
456
392
  for ac in sorted(ac_ids):
457
393
  if ac not in rows:
458
394
  errors.append(f"{workspace / 'traceability.md'}: {ac} absent from coverage matrix")
459
- if phase in {"prove", "polish", "review", "seal", "ship", "done"}:
395
+ if bool(metadata.get("proofRequired")):
460
396
  for eid in sorted(evid_ids):
461
397
  if eid not in trace:
462
398
  errors.append(f"{workspace / 'traceability.md'}: evidence ID {eid} from evidence/browser proof is not mapped")
@@ -467,7 +403,7 @@ def validate_workspace(workspace: Path) -> list[str]:
467
403
  elif slice_id not in evidence:
468
404
  errors.append(f"{evidence_path}: completed {slice_id} is not referenced by evidence")
469
405
 
470
- if phase in {"plan", "vet", "build", "prove", "polish", "review", "seal", "ship", "done"}:
406
+ if bool(metadata.get("blocksOpenQuestions")):
471
407
  qfile = workspace / "questions.md"
472
408
  if qfile.exists():
473
409
  for qid in blocking_questions(read(qfile)):
@@ -219,6 +219,16 @@ fi
219
219
 
220
220
  # ---- 11b. generated workspace schema fixtures ----------------------------
221
221
  section "workspace artifact schema"
222
+ if command -v go >/dev/null 2>&1; then
223
+ if (cd "$ROOT/engine" && go run ./internal/state/cmd/workflowmanifest -check -out internal/state/workflow_manifest.json) >/tmp/dr_workflow_manifest 2>&1; then
224
+ good "workflow manifest is fresh"
225
+ else
226
+ cat /tmp/dr_workflow_manifest
227
+ bad "workflow manifest drifted from the typed state registry"
228
+ fi
229
+ else
230
+ echo "skip: go not found; workflow manifest freshness not checked"
231
+ fi
222
232
  if command -v python3 >/dev/null 2>&1; then
223
233
  if python3 "$ROOT/scripts/validate-workspace-schema.py" "$ROOT/tests/fixtures/workspace-schema" >/tmp/dr_workspace_schema 2>&1; then
224
234
  cat /tmp/dr_workspace_schema
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env python3
2
+ """Read generated workflow metadata and canonical/legacy cursor fields."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import re
8
+ import sys
9
+ from pathlib import Path
10
+
11
+
12
+ MANIFEST = Path(__file__).resolve().parents[1] / "engine" / "internal" / "state" / "workflow_manifest.json"
13
+
14
+
15
+ DOCUMENT = json.loads(MANIFEST.read_text(encoding="utf-8"))
16
+
17
+
18
+ PHASES = {str(phase["id"]): phase for phase in DOCUMENT["phases"]}
19
+ CURSOR_KEY_ALIASES = {
20
+ str(entry["alias"]): str(entry["canonical"])
21
+ for entry in DOCUMENT["cursorKeyAliases"]
22
+ }
23
+
24
+
25
+ def normalize_cursor_key(key: str) -> str:
26
+ normalized = "".join(char for char in key.lower() if char.isalnum())
27
+ canonical = CURSOR_KEY_ALIASES.get(normalized, normalized)
28
+ return "".join(char for char in canonical.lower() if char.isalnum())
29
+
30
+
31
+ def cursor_field_text(text: str, key: str) -> str | None:
32
+ wanted = normalize_cursor_key(key)
33
+ for line in text.splitlines():
34
+ stripped = line.strip()
35
+ if stripped.startswith("|") and stripped.endswith("|"):
36
+ cells = [cell.strip() for cell in stripped[1:-1].split("|")]
37
+ if len(cells) >= 2 and normalize_cursor_key(cells[0]) == wanted:
38
+ return cells[1]
39
+ legacy = re.match(r"^\s*[-*+]?\s*([^:]+):\s*(.*?)\s*$", line)
40
+ if legacy and normalize_cursor_key(legacy.group(1)) == wanted:
41
+ return re.sub(r"\s*(?:#|\|).*?$", "", legacy.group(2)).rstrip()
42
+ return None
43
+
44
+
45
+ def cursor_field(path: Path, key: str) -> str | None:
46
+ if not path.is_file():
47
+ return None
48
+ return cursor_field_text(path.read_text(encoding="utf-8"), key)
49
+
50
+
51
+ def phase_property(phase: str, name: str) -> bool:
52
+ return bool(PHASES.get(phase.lower(), {}).get(name, False))
53
+
54
+
55
+ def main(argv: list[str]) -> int:
56
+ if len(argv) == 4 and argv[1] == "field":
57
+ value = cursor_field(Path(argv[2]), argv[3])
58
+ if value is None:
59
+ return 1
60
+ print(value)
61
+ return 0
62
+ if len(argv) == 4 and argv[1] == "phase-property":
63
+ return 0 if phase_property(argv[2], argv[3]) else 1
64
+ print("usage: workflow_schema.py field <state.md> <key> | phase-property <phase> <property>", file=sys.stderr)
65
+ return 2
66
+
67
+
68
+ if __name__ == "__main__":
69
+ raise SystemExit(main(sys.argv))