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.
- package/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/docs/adr/0004-state-schema-phases-sections.md +44 -0
- package/docs/command-map.md +1 -1
- package/docs/engine/state-schema.md +15 -12
- package/docs/flow.md +1 -1
- package/docs/research/go-authoritative-workflow-schema-and-quality-2026-07-20.md +212 -0
- package/engine/hooks_workspace.go +7 -7
- package/engine/internal/gate/gate.go +4 -4
- package/engine/internal/gate/gate_test.go +2 -2
- package/engine/internal/lib/cursor_compat_test.go +29 -0
- package/engine/internal/lib/preamble.go +1 -1
- package/engine/internal/lib/preamble_questions_test.go +10 -0
- package/engine/internal/lib/progress.go +59 -20
- package/engine/internal/lib/resolve.go +15 -6
- package/engine/internal/lib/resolve_remediation_test.go +9 -0
- package/engine/internal/lib/tickafk.go +2 -2
- package/engine/internal/migrate/migrate.go +2 -33
- package/engine/internal/migrate/migrate_test.go +7 -1
- package/engine/internal/state/cmd/workflowmanifest/main.go +54 -0
- package/engine/internal/state/cursor.go +42 -7
- package/engine/internal/state/feature.go +1 -27
- package/engine/internal/state/schema.go +135 -20
- package/engine/internal/state/snapshot.go +49 -3
- package/engine/internal/state/state_test.go +71 -0
- package/engine/internal/state/workflow_manifest.json +310 -0
- package/engine/internal/workflow/commands.go +0 -36
- package/engine/internal/workflow/commands_test.go +0 -17
- package/engine/testdata/golden/TestParityProgress/arg=allbuilt.golden +1 -1
- package/engine/testdata/golden/TestParityProgress/arg=done.golden +1 -1
- package/engine/testdata/golden/TestParityProgress/arg=mid.golden +1 -1
- package/engine/testdata/golden/TestParityProgress/arg=nophase.golden +1 -1
- package/engine/testdata/golden/TestParityProgress/arg=noslice.golden +1 -1
- package/engine/testdata/golden/TestParityProgress/arg=plan.golden +1 -1
- package/engine/testdata/golden/TestParityProgress/arg=seal.golden +1 -1
- package/engine/tests/adr_0004_required_by_phase_test.go +1 -18
- package/engine/tests/lib_parity_test.go +1 -1
- package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -3
- package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -3
- package/pack/generated/codex/AGENTS.md +1 -1
- package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -3
- package/package.json +1 -1
- package/scripts/codex-generate.sh +1 -1
- package/scripts/grade-feature.sh +5 -3
- package/scripts/run-outcome-evals.sh +21 -0
- package/scripts/validate-workspace-schema.py +9 -73
- package/scripts/validate.sh +10 -0
- 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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
378
|
-
|
|
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
|
|
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
|
|
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)):
|
package/scripts/validate.sh
CHANGED
|
@@ -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))
|