devrites 3.0.2 → 3.0.4
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 +13 -0
- package/README.md +1 -1
- package/docs/adr/0001-go-engine-as-control-plane.md +43 -0
- package/docs/adr/0002-dual-host-harness.md +34 -0
- package/docs/adr/0003-gate-model-hitl-pause.md +38 -0
- package/docs/adr/0004-state-schema-phases-sections.md +47 -0
- package/docs/adr/0005-hooks-as-engine-subcommands.md +39 -0
- package/docs/adr/0006-clock-seam-and-engine-ci-gates.md +50 -0
- package/docs/adr/0007-canonical-live-workspace-filenames.md +36 -0
- package/docs/adr/0008-sanctioned-engine-network-boundary.md +33 -0
- package/docs/adr/README.md +58 -0
- package/docs/command-map.md +1 -1
- package/docs/engine/state-schema.md +18 -14
- 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 +18 -44
- package/engine/internal/migrate/migrate_test.go +33 -5
- package/engine/internal/state/cmd/workflowmanifest/main.go +54 -0
- package/engine/internal/state/cursor.go +42 -7
- package/engine/internal/state/feature.go +29 -41
- package/engine/internal/state/schema.go +155 -28
- package/engine/internal/state/snapshot.go +49 -3
- package/engine/internal/state/state_test.go +77 -6
- 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/budget_test.go +2 -2
- package/engine/tests/lib_parity_test.go +1 -1
- package/engine/tests/meta_test.go +3 -3
- package/engine/tests/migrate_cli_test.go +24 -22
- 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 +2 -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-workflow-security.py +3 -8
- package/scripts/validate-workspace-schema.py +9 -73
- package/scripts/validate.sh +10 -0
- package/scripts/workflow_schema.py +69 -0
|
@@ -58,25 +58,26 @@ func TestMigrateNormalizesCanonicalWorkSchema(t *testing.T) {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
feat := filepath.Join(root, "work", slug)
|
|
61
|
-
//
|
|
62
|
-
for _, want := range []string{"
|
|
61
|
+
// Current canonical files stay canonical; migration adds only the missing map.
|
|
62
|
+
for _, want := range []string{"README.md", "state.md", "spec.md", "plan.md", "decisions.md", "tasks.md", "evidence.md"} {
|
|
63
63
|
if _, err := os.Stat(filepath.Join(feat, want)); err != nil {
|
|
64
64
|
t.Errorf("missing new-schema file %s: %v", want, err)
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
-
|
|
67
|
+
readme, err := os.ReadFile(filepath.Join(feat, "README.md"))
|
|
68
68
|
if err != nil {
|
|
69
69
|
t.Fatal(err)
|
|
70
70
|
}
|
|
71
|
-
if !strings.Contains(string(
|
|
72
|
-
t.Errorf("
|
|
71
|
+
if !strings.Contains(string(readme), "phase: temper") {
|
|
72
|
+
t.Errorf("README.md phase not derived from state.md\n%s", readme)
|
|
73
73
|
}
|
|
74
|
-
if !strings.Contains(string(
|
|
75
|
-
t.Errorf("
|
|
74
|
+
if !strings.Contains(string(readme), "schemaVersion: 1") {
|
|
75
|
+
t.Errorf("README.md missing schemaVersion\n%s", readme)
|
|
76
76
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
for _, unwanted := range []string{"feature.md", "proof.md", "status.md"} {
|
|
78
|
+
if _, err := os.Stat(filepath.Join(feat, unwanted)); !os.IsNotExist(err) {
|
|
79
|
+
t.Errorf("migration created legacy alias %s: %v", unwanted, err)
|
|
80
|
+
}
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
// A backup of the pre-migration state must exist.
|
|
@@ -127,11 +128,12 @@ func TestMigrateNormalizesLiveFeatureInPlace(t *testing.T) {
|
|
|
127
128
|
t.Fatal(err)
|
|
128
129
|
}
|
|
129
130
|
files := map[string]string{
|
|
130
|
-
"
|
|
131
|
+
"feature.md": "# Legacy feature map\n",
|
|
132
|
+
"status.md": "# State\n\n- Phase: prove\n- Status: running\n",
|
|
131
133
|
"spec.md": "# Spec\n\nRotate tokens.\n",
|
|
132
134
|
"plan.md": "# Plan\n\nStep 1, step 2.\n",
|
|
133
135
|
"tasks.md": "# Tasks\n\n- [x] one\n",
|
|
134
|
-
"
|
|
136
|
+
"proof.md": "# Evidence\n\nTests green.\n",
|
|
135
137
|
"decisions.md": "# Decisions\n\nUse HMAC.\n",
|
|
136
138
|
}
|
|
137
139
|
for name, body := range files {
|
|
@@ -147,25 +149,25 @@ func TestMigrateNormalizesLiveFeatureInPlace(t *testing.T) {
|
|
|
147
149
|
if !strings.Contains(out, "migrated 1 feature(s)") {
|
|
148
150
|
t.Errorf("unexpected migrate output\n%s", out)
|
|
149
151
|
}
|
|
150
|
-
for _, want := range []string{"feature.md", "proof.md", "status.md", "state.md", "evidence.md"} {
|
|
152
|
+
for _, want := range []string{"README.md", "feature.md", "proof.md", "status.md", "state.md", "evidence.md"} {
|
|
151
153
|
if _, err := os.Stat(filepath.Join(feat, want)); err != nil {
|
|
152
154
|
t.Errorf("missing normalized file %s: %v", want, err)
|
|
153
155
|
}
|
|
154
156
|
}
|
|
155
|
-
|
|
157
|
+
readme, err := os.ReadFile(filepath.Join(feat, "README.md"))
|
|
156
158
|
if err != nil {
|
|
157
159
|
t.Fatal(err)
|
|
158
160
|
}
|
|
159
|
-
if !strings.Contains(string(
|
|
160
|
-
t.Errorf("
|
|
161
|
+
if !strings.Contains(string(readme), "Legacy feature map") {
|
|
162
|
+
t.Errorf("README.md did not carry feature.md content\n%s", readme)
|
|
161
163
|
}
|
|
162
|
-
|
|
163
|
-
if !strings.Contains(string(
|
|
164
|
-
t.Errorf("
|
|
164
|
+
stateFile, _ := os.ReadFile(filepath.Join(feat, "state.md"))
|
|
165
|
+
if !strings.Contains(string(stateFile), "running") {
|
|
166
|
+
t.Errorf("state.md did not carry status.md content\n%s", stateFile)
|
|
165
167
|
}
|
|
166
|
-
|
|
167
|
-
if !strings.Contains(string(
|
|
168
|
-
t.Errorf("
|
|
168
|
+
evidence, _ := os.ReadFile(filepath.Join(feat, "evidence.md"))
|
|
169
|
+
if !strings.Contains(string(evidence), "Tests green") {
|
|
170
|
+
t.Errorf("evidence.md did not carry proof.md content\n%s", evidence)
|
|
169
171
|
}
|
|
170
172
|
if got := backupDirs(t, root); len(got) != 1 {
|
|
171
173
|
t.Errorf("want exactly one backup dir, got %v", got)
|
|
@@ -9,9 +9,11 @@ workspace map; `proof.md` may stand in for `evidence.md`.
|
|
|
9
9
|
|
|
10
10
|
| Phase | Required artifacts |
|
|
11
11
|
| --- | --- |
|
|
12
|
+
| frame | `state.md` |
|
|
12
13
|
| spec | `README.md`/`index.md`/`feature.md`, `brief.md`, `spec.md`, `state.md`, `decisions.md`, `assumptions.md`, `questions.md` |
|
|
13
|
-
|
|
|
14
|
-
|
|
|
14
|
+
| temper | spec artifacts |
|
|
15
|
+
| define/plan/vet/build/converge | spec artifacts plus `architecture.md`, `plan.md`, `tasks.md`, `traceability.md` |
|
|
16
|
+
| prove/polish/review/seal/ship/done | plan artifacts plus `evidence.md`/`proof.md`, `touched-files.md` |
|
|
15
17
|
| conditional | `flows.md` when diagrams clarify; `design-brief.md` and `browser-evidence.md` for UI; `drift.md` for drift; `handoff.md` only when requested; `references.md` + `references/` when references exist |
|
|
16
18
|
|
|
17
19
|
## What each file owns
|
|
@@ -33,7 +35,7 @@ workspace map; `proof.md` may stand in for `evidence.md`.
|
|
|
33
35
|
| `evidence.md` / `proof.md` | `EVID-###` command/action, result, timestamp if available, related AC/slice IDs, limitation | 280 lines |
|
|
34
36
|
| `browser-evidence.md` | UI route/viewports/screenshots/console/network/interactions and Visual Verdict | 220 lines |
|
|
35
37
|
| `drift.md` | `DRIFT-###` spec/plan drift and resolution | 160 lines |
|
|
36
|
-
| `touched-files.md` | implementation files, slice ownership, reason, and a concern-ordered `## Review trail` of `path:line` stops for human review |
|
|
38
|
+
| `touched-files.md` | implementation files, slice ownership, reason, and a concern-ordered `## Review trail` of `path:line` stops for human review | 160 lines |
|
|
37
39
|
| `design-brief.md` | UI design direction, states, interaction model | 160 lines |
|
|
38
40
|
| `handoff.md` | cold-resume guide: current objective, last completed slice, next action, blockers, read-next links | 120 lines |
|
|
39
41
|
|
|
@@ -9,9 +9,11 @@ workspace map; `proof.md` may stand in for `evidence.md`.
|
|
|
9
9
|
|
|
10
10
|
| Phase | Required artifacts |
|
|
11
11
|
| --- | --- |
|
|
12
|
+
| frame | `state.md` |
|
|
12
13
|
| spec | `README.md`/`index.md`/`feature.md`, `brief.md`, `spec.md`, `state.md`, `decisions.md`, `assumptions.md`, `questions.md` |
|
|
13
|
-
|
|
|
14
|
-
|
|
|
14
|
+
| temper | spec artifacts |
|
|
15
|
+
| define/plan/vet/build/converge | spec artifacts plus `architecture.md`, `plan.md`, `tasks.md`, `traceability.md` |
|
|
16
|
+
| prove/polish/review/seal/ship/done | plan artifacts plus `evidence.md`/`proof.md`, `touched-files.md` |
|
|
15
17
|
| conditional | `flows.md` when diagrams clarify; `design-brief.md` and `browser-evidence.md` for UI; `drift.md` for drift; `handoff.md` only when requested; `references.md` + `references/` when references exist |
|
|
16
18
|
|
|
17
19
|
## What each file owns
|
|
@@ -33,7 +35,7 @@ workspace map; `proof.md` may stand in for `evidence.md`.
|
|
|
33
35
|
| `evidence.md` / `proof.md` | `EVID-###` command/action, result, timestamp if available, related AC/slice IDs, limitation | 280 lines |
|
|
34
36
|
| `browser-evidence.md` | UI route/viewports/screenshots/console/network/interactions and Visual Verdict | 220 lines |
|
|
35
37
|
| `drift.md` | `DRIFT-###` spec/plan drift and resolution | 160 lines |
|
|
36
|
-
| `touched-files.md` | implementation files, slice ownership, reason, and a concern-ordered `## Review trail` of `path:line` stops for human review |
|
|
38
|
+
| `touched-files.md` | implementation files, slice ownership, reason, and a concern-ordered `## Review trail` of `path:line` stops for human review | 160 lines |
|
|
37
39
|
| `design-brief.md` | UI design direction, states, interaction model | 160 lines |
|
|
38
40
|
| `handoff.md` | cold-resume guide: current objective, last completed slice, next action, blockers, read-next links | 120 lines |
|
|
39
41
|
|
|
@@ -18,6 +18,6 @@ This project has DevRites installed for both Claude Code and Codex.
|
|
|
18
18
|
## Workflow contract
|
|
19
19
|
|
|
20
20
|
- Keep all feature state in `.devrites/work/<slug>/` and preserve `.devrites/ACTIVE`.
|
|
21
|
-
- Follow the DevRites lifecycle: spec -> define -> build -> prove -> polish -> review -> seal -> ship.
|
|
21
|
+
- Follow the DevRites lifecycle: frame -> spec -> temper -> define -> plan -> vet -> build -> converge -> prove -> polish -> review -> seal -> ship -> done.
|
|
22
22
|
- Claims of completion need recorded evidence in the feature workspace, not confidence alone.
|
|
23
23
|
<!-- END DEVRITES CODEX -->
|
|
@@ -9,9 +9,11 @@ workspace map; `proof.md` may stand in for `evidence.md`.
|
|
|
9
9
|
|
|
10
10
|
| Phase | Required artifacts |
|
|
11
11
|
| --- | --- |
|
|
12
|
+
| frame | `state.md` |
|
|
12
13
|
| spec | `README.md`/`index.md`/`feature.md`, `brief.md`, `spec.md`, `state.md`, `decisions.md`, `assumptions.md`, `questions.md` |
|
|
13
|
-
|
|
|
14
|
-
|
|
|
14
|
+
| temper | spec artifacts |
|
|
15
|
+
| define/plan/vet/build/converge | spec artifacts plus `architecture.md`, `plan.md`, `tasks.md`, `traceability.md` |
|
|
16
|
+
| prove/polish/review/seal/ship/done | plan artifacts plus `evidence.md`/`proof.md`, `touched-files.md` |
|
|
15
17
|
| conditional | `flows.md` when diagrams clarify; `design-brief.md` and `browser-evidence.md` for UI; `drift.md` for drift; `handoff.md` only when requested; `references.md` + `references/` when references exist |
|
|
16
18
|
|
|
17
19
|
## What each file owns
|
|
@@ -33,7 +35,7 @@ workspace map; `proof.md` may stand in for `evidence.md`.
|
|
|
33
35
|
| `evidence.md` / `proof.md` | `EVID-###` command/action, result, timestamp if available, related AC/slice IDs, limitation | 280 lines |
|
|
34
36
|
| `browser-evidence.md` | UI route/viewports/screenshots/console/network/interactions and Visual Verdict | 220 lines |
|
|
35
37
|
| `drift.md` | `DRIFT-###` spec/plan drift and resolution | 160 lines |
|
|
36
|
-
| `touched-files.md` | implementation files, slice ownership, reason, and a concern-ordered `## Review trail` of `path:line` stops for human review |
|
|
38
|
+
| `touched-files.md` | implementation files, slice ownership, reason, and a concern-ordered `## Review trail` of `path:line` stops for human review | 160 lines |
|
|
37
39
|
| `design-brief.md` | UI design direction, states, interaction model | 160 lines |
|
|
38
40
|
| `handoff.md` | cold-resume guide: current objective, last completed slice, next action, blockers, read-next links | 120 lines |
|
|
39
41
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devrites",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.4",
|
|
4
4
|
"description": "DevRites — disciplined senior-engineer workflow skills pack for Claude Code and Codex",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"homepage": "https://github.com/ViktorsBaikers/DevRites#readme",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"test:fast": "node scripts/run-tests.mjs --fast",
|
|
55
55
|
"test:parallel": "node scripts/run-tests.mjs",
|
|
56
56
|
"test:serial": "node scripts/run-tests.mjs --serial",
|
|
57
|
+
"audit": "npm audit --audit-level=moderate",
|
|
57
58
|
"size:baseline": "node scripts/check-instruction-size-baseline.mjs --write",
|
|
58
59
|
"release": "semantic-release",
|
|
59
60
|
"release:dry": "semantic-release --dry-run --no-ci"
|
|
@@ -132,7 +132,7 @@ This project has DevRites installed for both Claude Code and Codex.
|
|
|
132
132
|
## Workflow contract
|
|
133
133
|
|
|
134
134
|
- Keep all feature state in `.devrites/work/<slug>/` and preserve `.devrites/ACTIVE`.
|
|
135
|
-
- Follow the DevRites lifecycle: spec -> define -> build -> prove -> polish -> review -> seal -> ship.
|
|
135
|
+
- Follow the DevRites lifecycle: frame -> spec -> temper -> define -> plan -> vet -> build -> converge -> prove -> polish -> review -> seal -> ship -> done.
|
|
136
136
|
- Claims of completion need recorded evidence in the feature workspace, not confidence alone.
|
|
137
137
|
<!-- END DEVRITES CODEX -->
|
|
138
138
|
EOF
|
package/scripts/grade-feature.sh
CHANGED
|
@@ -77,9 +77,11 @@ fi
|
|
|
77
77
|
|
|
78
78
|
# 7 — state phase / status
|
|
79
79
|
if [ -f "$st" ]; then
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
ph="$(python3 "$ROOT/scripts/workflow_schema.py" field "$st" phase 2>/dev/null || true)"
|
|
81
|
+
stt="$(python3 "$ROOT/scripts/workflow_schema.py" field "$st" status 2>/dev/null || true)"
|
|
82
|
+
if ! python3 "$ROOT/scripts/workflow_schema.py" phase-property "$ph" shippable; then
|
|
83
|
+
problems+=("state.md Phase='${ph}' (expected a shippable phase)")
|
|
84
|
+
fi
|
|
83
85
|
case "$stt" in awaiting_human|blocked) problems+=("state.md Status='${stt}' (not shippable)") ;; esac
|
|
84
86
|
else
|
|
85
87
|
problems+=("state.md missing")
|
|
@@ -23,6 +23,27 @@ else
|
|
|
23
23
|
echo " FAIL — a known-shippable workspace should grade GO"; fail=1
|
|
24
24
|
fi
|
|
25
25
|
|
|
26
|
+
echo
|
|
27
|
+
echo "== grade synthetic/canonical-cursor (expect GO) =="
|
|
28
|
+
canonical="$tmp/canonical-cursor"
|
|
29
|
+
mkdir -p "$canonical"
|
|
30
|
+
cp -R "$good/." "$canonical/"
|
|
31
|
+
cat > "$canonical/state.md" <<'MD'
|
|
32
|
+
# State
|
|
33
|
+
|
|
34
|
+
## Cursor
|
|
35
|
+
| Key | Value |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| phase | done |
|
|
38
|
+
| status | done |
|
|
39
|
+
| next_action | archived |
|
|
40
|
+
MD
|
|
41
|
+
if bash "$GRADER" "$canonical"; then
|
|
42
|
+
echo " PASS — canonical cursor graded GO"
|
|
43
|
+
else
|
|
44
|
+
echo " FAIL — canonical cursor should grade GO"; fail=1
|
|
45
|
+
fi
|
|
46
|
+
|
|
26
47
|
echo
|
|
27
48
|
echo "== grade golden/blocked-feature (expect NO-GO) =="
|
|
28
49
|
if bash "$GRADER" "$bad"; then
|
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
DevRites' publish (release.yml) and auto-merge paths are high-value targets, so this
|
|
5
5
|
gate fails CI when a workflow:
|
|
6
6
|
|
|
7
|
-
- uses
|
|
8
|
-
like `@v2` lets a compromised upstream inject code into the pipeline
|
|
9
|
-
`actions/*` and `github/*` tags are tolerated);
|
|
7
|
+
- uses any non-local action not pinned to a full 40-char commit SHA — a moving
|
|
8
|
+
tag like `@v2` lets a compromised upstream inject code into the pipeline;
|
|
10
9
|
- declares no `permissions:` scope anywhere — the default token is broad;
|
|
11
10
|
- uses `permissions: write-all` (over-broad);
|
|
12
11
|
- uses `pull_request_target`, except for a Dependabot-only workflow that never
|
|
@@ -19,7 +18,6 @@ import os
|
|
|
19
18
|
import re
|
|
20
19
|
import sys
|
|
21
20
|
|
|
22
|
-
FIRST_PARTY = {"actions", "github"} # GitHub-owned; major-tag refs tolerated
|
|
23
21
|
SHA_RE = re.compile(r"^[0-9a-f]{40}$")
|
|
24
22
|
USES_RE = re.compile(r"^\s*-?\s*uses:\s*([^\s#]+)")
|
|
25
23
|
DEPENDABOT_ONLY_RE = re.compile(
|
|
@@ -61,13 +59,10 @@ def scan_text(path, text):
|
|
|
61
59
|
ref = m.group(1)
|
|
62
60
|
if ref.startswith("./") or ref.startswith("."):
|
|
63
61
|
continue # local action
|
|
64
|
-
owner = ref.split("/", 1)[0]
|
|
65
|
-
if owner in FIRST_PARTY:
|
|
66
|
-
continue
|
|
67
62
|
at = ref.rsplit("@", 1)
|
|
68
63
|
pin = at[1] if len(at) == 2 else ""
|
|
69
64
|
if not SHA_RE.match(pin):
|
|
70
|
-
findings.append("%s:%d:
|
|
65
|
+
findings.append("%s:%d: action '%s' not pinned to a full commit "
|
|
71
66
|
"SHA — pin it (a moving tag is a supply-chain risk)"
|
|
72
67
|
% (path, i, ref))
|
|
73
68
|
return findings
|
|
@@ -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))
|