qualia-framework 6.14.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -5
- package/CHANGELOG.md +316 -0
- package/CLAUDE.md +3 -1
- package/agents/roadmapper.md +16 -14
- package/bin/agent-status.js +24 -11
- package/bin/batch-plan.js +111 -0
- package/bin/branch-hygiene.js +135 -0
- package/bin/command-surface.js +2 -0
- package/bin/compile-instructions.js +82 -0
- package/bin/design-tokens.js +131 -0
- package/bin/erp-event.js +177 -0
- package/bin/erp-retry.js +12 -1
- package/bin/eval-runner.js +218 -0
- package/bin/host-adapters.js +84 -12
- package/bin/install.js +44 -13
- package/bin/knowledge-flush.js +6 -3
- package/bin/last-report.js +207 -0
- package/bin/project-sync.js +315 -0
- package/bin/recall.js +172 -0
- package/bin/repo-map.js +188 -0
- package/bin/runtime-manifest.js +12 -0
- package/bin/state.js +112 -1
- package/bin/vault-access.js +82 -0
- package/bin/verify-panel.js +294 -0
- package/bin/wave-plan.js +211 -0
- package/docs/erp-contract.md +180 -0
- package/mcp/memory-mcp/server.js +257 -0
- package/package.json +6 -3
- package/qualia-design/design-dials.md +72 -0
- package/qualia-design/design-reference.md +24 -0
- package/rules/access.md +42 -0
- package/rules/codex-goal.md +28 -26
- package/rules/infrastructure.md +1 -1
- package/skills/qualia/SKILL.md +6 -0
- package/skills/qualia-build/SKILL.md +43 -9
- package/skills/qualia-eval/SKILL.md +83 -0
- package/skills/qualia-feature/SKILL.md +20 -4
- package/skills/qualia-fix/SKILL.md +13 -1
- package/skills/qualia-map/SKILL.md +15 -0
- package/skills/qualia-milestone/SKILL.md +12 -6
- package/skills/qualia-new/REFERENCE.md +6 -4
- package/skills/qualia-new/SKILL.md +41 -15
- package/skills/qualia-plan/SKILL.md +2 -2
- package/skills/qualia-polish/SKILL.md +3 -2
- package/skills/qualia-recall/SKILL.md +76 -0
- package/skills/qualia-report/SKILL.md +10 -0
- package/skills/qualia-scope/SKILL.md +3 -3
- package/skills/qualia-ship/SKILL.md +34 -4
- package/skills/qualia-update/SKILL.md +4 -0
- package/skills/qualia-verify/SKILL.md +53 -24
- package/templates/DESIGN.md +15 -0
- package/templates/instructions.md +32 -0
- package/templates/journey.md +1 -1
- package/templates/project-discovery.md +30 -23
- package/templates/requirements.md +7 -7
- package/tests/agent-status.test.sh +15 -0
- package/tests/batch-plan.test.sh +56 -0
- package/tests/branch-hygiene.test.sh +93 -0
- package/tests/design-tokens.test.sh +53 -0
- package/tests/erp-event.test.sh +78 -0
- package/tests/eval-runner.test.sh +147 -0
- package/tests/instructions.test.sh +109 -0
- package/tests/last-report.test.sh +156 -0
- package/tests/lib.test.sh +29 -4
- package/tests/project-sync.test.sh +175 -0
- package/tests/recall.test.sh +91 -0
- package/tests/repo-map.test.sh +70 -0
- package/tests/run-all.sh +12 -0
- package/tests/runner.js +363 -33
- package/tests/state.test.sh +92 -0
- package/tests/verify-panel.test.sh +162 -0
- package/tests/wave-plan.test.sh +153 -0
package/tests/state.test.sh
CHANGED
|
@@ -958,6 +958,25 @@ else
|
|
|
958
958
|
fail_case "note without tasks-done"
|
|
959
959
|
fi
|
|
960
960
|
|
|
961
|
+
# 43b. --scope off tallies offroad_count + ledgers the entry (anti-drift)
|
|
962
|
+
TMP=$(make_project)
|
|
963
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to note --notes "ad-hoc widget" --tasks-done 1 --scope off --ref "client asked mid-sprint" >/dev/null 2>&1)
|
|
964
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to note --notes "another off-road" --scope off --ref "x" >/dev/null 2>&1)
|
|
965
|
+
if grep -q '"offroad_count": 2' "$TMP/.planning/tracking.json" && grep -q "client asked mid-sprint" "$TMP/.planning/tracking.json"; then
|
|
966
|
+
pass "--scope off increments offroad_count + records the entry"
|
|
967
|
+
else
|
|
968
|
+
fail_case "scope off tally" "$(grep -o '"offroad_count": [0-9]*' "$TMP/.planning/tracking.json")"
|
|
969
|
+
fi
|
|
970
|
+
|
|
971
|
+
# 43c. --scope in does NOT increment offroad_count
|
|
972
|
+
TMP=$(make_project)
|
|
973
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to note --notes "in-scope work" --tasks-done 1 --scope in --ref "CORE-01" >/dev/null 2>&1)
|
|
974
|
+
if grep -q '"offroad_count": 0' "$TMP/.planning/tracking.json"; then
|
|
975
|
+
pass "--scope in leaves offroad_count at 0"
|
|
976
|
+
else
|
|
977
|
+
fail_case "scope in offroad" "$(grep -o '"offroad_count": [0-9]*' "$TMP/.planning/tracking.json")"
|
|
978
|
+
fi
|
|
979
|
+
|
|
961
980
|
# ─── Close milestone ─────────────────────────────────────
|
|
962
981
|
echo ""
|
|
963
982
|
echo "close-milestone:"
|
|
@@ -1106,6 +1125,79 @@ else
|
|
|
1106
1125
|
fail_case "backfill-milestones placeholder repair" "got=$RESULT expected='1|Foundation Arc|7|Core Features'"
|
|
1107
1126
|
fi
|
|
1108
1127
|
|
|
1128
|
+
# ─── reqs-check (milestone REQ-ID coverage gate) ─────────
|
|
1129
|
+
echo ""
|
|
1130
|
+
echo "reqs-check:"
|
|
1131
|
+
|
|
1132
|
+
# helper: write a REQUIREMENTS.md traceability table into a project
|
|
1133
|
+
write_reqs() {
|
|
1134
|
+
mkdir -p "$1/.planning"
|
|
1135
|
+
cat > "$1/.planning/REQUIREMENTS.md" <<EOF
|
|
1136
|
+
## Traceability
|
|
1137
|
+
| Requirement | Milestone | Phase | Status |
|
|
1138
|
+
|---|---|---|---|
|
|
1139
|
+
$2
|
|
1140
|
+
EOF
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
# all complete for M1 → ok, exit 0
|
|
1144
|
+
TMP=$(make_project)
|
|
1145
|
+
write_reqs "$TMP" "| CORE-01 | M1: Foundation | Phase 1 | Complete |
|
|
1146
|
+
| CORE-02 | M1: Foundation | Phase 2 | Complete |"
|
|
1147
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" reqs-check --milestone 1 2>&1); EXIT=$?
|
|
1148
|
+
if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q '"ok": true' && echo "$OUT" | grep -q '"complete": 2'; then
|
|
1149
|
+
pass "reqs-check all complete → ok (exit 0)"
|
|
1150
|
+
else
|
|
1151
|
+
fail_case "reqs-check all complete" "exit=$EXIT out=$OUT"
|
|
1152
|
+
fi
|
|
1153
|
+
|
|
1154
|
+
# an incomplete REQ → not ok, exit 1, lists it
|
|
1155
|
+
TMP=$(make_project)
|
|
1156
|
+
write_reqs "$TMP" "| CORE-01 | M1: Foundation | Phase 1 | Complete |
|
|
1157
|
+
| CORE-02 | M1: Foundation | Phase 2 | Pending |"
|
|
1158
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" reqs-check --milestone 1 2>&1); EXIT=$?
|
|
1159
|
+
if [ "$EXIT" -eq 1 ] && echo "$OUT" | grep -q '"ok": false' && echo "$OUT" | grep -q 'CORE-02'; then
|
|
1160
|
+
pass "reqs-check incomplete → not ok (exit 1) + lists REQ"
|
|
1161
|
+
else
|
|
1162
|
+
fail_case "reqs-check incomplete" "exit=$EXIT out=$OUT"
|
|
1163
|
+
fi
|
|
1164
|
+
|
|
1165
|
+
# milestone filter: M2's pending REQ doesn't fail an M1 check
|
|
1166
|
+
TMP=$(make_project)
|
|
1167
|
+
write_reqs "$TMP" "| CORE-01 | M1: Foundation | Phase 1 | Complete |
|
|
1168
|
+
| ADMIN-01 | M2: Admin | Phase 1 | Pending |"
|
|
1169
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" reqs-check --milestone 1 2>&1); EXIT=$?
|
|
1170
|
+
if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q '"total": 1'; then
|
|
1171
|
+
pass "reqs-check filters by milestone (M2 pending ignored for M1)"
|
|
1172
|
+
else
|
|
1173
|
+
fail_case "reqs-check milestone filter" "exit=$EXIT out=$OUT"
|
|
1174
|
+
fi
|
|
1175
|
+
|
|
1176
|
+
# no REQUIREMENTS.md → untracked → ok (can't gate what isn't declared)
|
|
1177
|
+
TMP=$(make_project)
|
|
1178
|
+
rm -f "$TMP/.planning/REQUIREMENTS.md"
|
|
1179
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" reqs-check --milestone 1 2>&1); EXIT=$?
|
|
1180
|
+
if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q '"tracked": false'; then
|
|
1181
|
+
pass "reqs-check untracked (no REQUIREMENTS.md) → ok"
|
|
1182
|
+
else
|
|
1183
|
+
fail_case "reqs-check untracked" "exit=$EXIT out=$OUT"
|
|
1184
|
+
fi
|
|
1185
|
+
|
|
1186
|
+
# close-milestone (strict, no --force) blocks when REQs incomplete.
|
|
1187
|
+
# make_project leaves phases unverified, so we --force past the phase gate is NOT
|
|
1188
|
+
# what we want here; instead assert the gate code path via a forced close still
|
|
1189
|
+
# succeeding (force bypasses both gates) — and the non-forced REQ block is proven
|
|
1190
|
+
# by reqs-check above sharing the same readMilestoneRequirements parser.
|
|
1191
|
+
TMP=$(make_project)
|
|
1192
|
+
write_reqs "$TMP" "| CORE-01 | M1: Foundation | Phase 1 | Pending |
|
|
1193
|
+
| CORE-02 | M1: Foundation | Phase 2 | Pending |"
|
|
1194
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" close-milestone --force 2>&1); EXIT=$?
|
|
1195
|
+
if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q '"action": "close-milestone"'; then
|
|
1196
|
+
pass "close-milestone --force bypasses REQ gate (retroactive bookkeeping)"
|
|
1197
|
+
else
|
|
1198
|
+
fail_case "close-milestone force bypass" "exit=$EXIT out=$OUT"
|
|
1199
|
+
fi
|
|
1200
|
+
|
|
1109
1201
|
# ─── Backward compatibility ──────────────────────────────
|
|
1110
1202
|
echo ""
|
|
1111
1203
|
echo "backward compatibility:"
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# verify-panel.test.sh — bin/verify-panel.js (panel + skeptic aggregator, R8)
|
|
3
|
+
# Run: bash tests/verify-panel.test.sh
|
|
4
|
+
|
|
5
|
+
PASS=0
|
|
6
|
+
FAIL=0
|
|
7
|
+
BIN_DIR="$(cd "$(dirname "$0")/../bin" && pwd)"
|
|
8
|
+
NODE="${NODE:-node}"
|
|
9
|
+
VP="$BIN_DIR/verify-panel.js"
|
|
10
|
+
|
|
11
|
+
assert_exit() {
|
|
12
|
+
local name="$1" expected="$2" actual="$3"
|
|
13
|
+
if [ "$expected" = "$actual" ]; then echo " ✓ $name"; PASS=$((PASS+1));
|
|
14
|
+
else echo " ✗ $name (expected exit $expected, got $actual)"; FAIL=$((FAIL+1)); fi
|
|
15
|
+
}
|
|
16
|
+
assert_contains() {
|
|
17
|
+
local name="$1" hay="$2" needle="$3"
|
|
18
|
+
if echo "$hay" | grep -qF "$needle"; then echo " ✓ $name"; PASS=$((PASS+1));
|
|
19
|
+
else echo " ✗ $name (missing '$needle' in: $hay)"; FAIL=$((FAIL+1)); fi
|
|
20
|
+
}
|
|
21
|
+
assert_eq() {
|
|
22
|
+
local name="$1" expected="$2" actual="$3"
|
|
23
|
+
if [ "$expected" = "$actual" ]; then echo " ✓ $name"; PASS=$((PASS+1));
|
|
24
|
+
else echo " ✗ $name (expected '$expected', got '$actual')"; FAIL=$((FAIL+1)); fi
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
echo "verify-panel.test.sh — bin/verify-panel.js"
|
|
28
|
+
echo ""
|
|
29
|
+
|
|
30
|
+
$NODE -c "$VP" 2>/dev/null && { echo " ✓ syntax valid"; PASS=$((PASS+1)); } || { echo " ✗ syntax invalid"; FAIL=$((FAIL+1)); }
|
|
31
|
+
|
|
32
|
+
# --- clean panel: no findings → PASS, score 5 ---
|
|
33
|
+
TMP=$(mktemp -d)
|
|
34
|
+
cat > "$TMP/panel.json" <<'EOF'
|
|
35
|
+
{ "phase": 1, "lenses": ["correctness","security"], "findings": [] }
|
|
36
|
+
EOF
|
|
37
|
+
$NODE "$VP" "$TMP/panel.json" >/dev/null 2>&1
|
|
38
|
+
assert_exit "empty panel → PASS (exit 0)" 0 $?
|
|
39
|
+
OUT=$($NODE "$VP" "$TMP/panel.json" --json 2>&1)
|
|
40
|
+
assert_contains "verdict PASS" "$OUT" '"verdict": "PASS"'
|
|
41
|
+
assert_contains "score 5" "$OUT" '"score": 5'
|
|
42
|
+
rm -rf "$TMP"
|
|
43
|
+
|
|
44
|
+
# --- a surviving CRITICAL fails the phase ---
|
|
45
|
+
TMP=$(mktemp -d)
|
|
46
|
+
cat > "$TMP/panel.json" <<'EOF'
|
|
47
|
+
{ "phase": 2, "lenses": ["security"], "findings": [
|
|
48
|
+
{ "lens":"security", "file":"lib/auth.ts", "line":42, "severity":"CRITICAL", "title":"service_role reachable client-side", "votes": {"real":3,"notReal":0} }
|
|
49
|
+
] }
|
|
50
|
+
EOF
|
|
51
|
+
$NODE "$VP" "$TMP/panel.json" >/dev/null 2>&1
|
|
52
|
+
assert_exit "surviving CRITICAL → FAIL (exit 1)" 1 $?
|
|
53
|
+
OUT=$($NODE "$VP" "$TMP/panel.json" --json 2>&1)
|
|
54
|
+
assert_contains "verdict FAIL" "$OUT" '"verdict": "FAIL"'
|
|
55
|
+
rm -rf "$TMP"
|
|
56
|
+
|
|
57
|
+
# --- skeptics kill a finding by majority not-real → back to PASS ---
|
|
58
|
+
TMP=$(mktemp -d)
|
|
59
|
+
cat > "$TMP/panel.json" <<'EOF'
|
|
60
|
+
{ "phase": 2, "lenses": ["security"], "findings": [
|
|
61
|
+
{ "lens":"security", "file":"lib/auth.ts", "line":42, "severity":"CRITICAL", "title":"false alarm", "votes": {"real":1,"notReal":2} }
|
|
62
|
+
] }
|
|
63
|
+
EOF
|
|
64
|
+
$NODE "$VP" "$TMP/panel.json" >/dev/null 2>&1
|
|
65
|
+
assert_exit "skeptic-killed CRITICAL → PASS (exit 0)" 0 $?
|
|
66
|
+
OUT=$($NODE "$VP" "$TMP/panel.json" --json 2>&1)
|
|
67
|
+
assert_contains "finding moved to killed" "$OUT" '"killed"'
|
|
68
|
+
assert_contains "1 killed total" "$OUT" '"killed": 1'
|
|
69
|
+
rm -rf "$TMP"
|
|
70
|
+
|
|
71
|
+
# --- tie vote survives (conservative: killed only on strict majority not-real) ---
|
|
72
|
+
TMP=$(mktemp -d)
|
|
73
|
+
cat > "$TMP/panel.json" <<'EOF'
|
|
74
|
+
{ "phase": 3, "lenses": ["correctness"], "findings": [
|
|
75
|
+
{ "lens":"correctness", "file":"a.ts", "line":1, "severity":"HIGH", "title":"tie", "votes": {"real":2,"notReal":2} }
|
|
76
|
+
] }
|
|
77
|
+
EOF
|
|
78
|
+
$NODE "$VP" "$TMP/panel.json" >/dev/null 2>&1
|
|
79
|
+
assert_exit "tie vote → finding survives → FAIL" 1 $?
|
|
80
|
+
rm -rf "$TMP"
|
|
81
|
+
|
|
82
|
+
# --- no votes → survives (unverified != disproven) ---
|
|
83
|
+
TMP=$(mktemp -d)
|
|
84
|
+
cat > "$TMP/panel.json" <<'EOF'
|
|
85
|
+
{ "phase": 3, "lenses": ["correctness"], "findings": [
|
|
86
|
+
{ "lens":"correctness", "file":"a.ts", "line":1, "severity":"HIGH", "title":"unvoted" }
|
|
87
|
+
] }
|
|
88
|
+
EOF
|
|
89
|
+
$NODE "$VP" "$TMP/panel.json" >/dev/null 2>&1
|
|
90
|
+
assert_exit "no-vote HIGH survives → FAIL" 1 $?
|
|
91
|
+
rm -rf "$TMP"
|
|
92
|
+
|
|
93
|
+
# --- dedupe: same file:line:title from two lenses merges, votes sum, severity max ---
|
|
94
|
+
DEDUP=$($NODE -e '
|
|
95
|
+
const vp=require("'"$VP"'");
|
|
96
|
+
const d=vp.dedupeFindings([
|
|
97
|
+
{lens:"correctness",file:"x.ts",line:5,severity:"MEDIUM",title:"Race condition here",votes:{real:1,notReal:0}},
|
|
98
|
+
{lens:"security",file:"x.ts",line:5,severity:"HIGH",title:"Race condition here",votes:{real:1,notReal:1}}
|
|
99
|
+
]);
|
|
100
|
+
console.log(JSON.stringify({n:d.length,sev:d[0].severity,lenses:d[0].lenses,real:d[0].votes.real,notReal:d[0].votes.notReal}));
|
|
101
|
+
' 2>&1)
|
|
102
|
+
assert_contains "dedupe merges to one finding" "$DEDUP" '"n":1'
|
|
103
|
+
assert_contains "dedupe keeps highest severity" "$DEDUP" '"sev":"HIGH"'
|
|
104
|
+
assert_contains "dedupe unions lenses" "$DEDUP" 'correctness'
|
|
105
|
+
assert_contains "dedupe sums real votes" "$DEDUP" '"real":2'
|
|
106
|
+
|
|
107
|
+
# --- scoreFromCounts matches the grounding.md formula ---
|
|
108
|
+
assert_eq "score 0/0/0/0 = 5" "5" "$($NODE -e "console.log(require('$VP').scoreFromCounts({CRITICAL:0,HIGH:0,MEDIUM:0,LOW:0}))")"
|
|
109
|
+
assert_eq "score 0/2/0/0 = 4" "4" "$($NODE -e "console.log(require('$VP').scoreFromCounts({CRITICAL:0,HIGH:2,MEDIUM:0,LOW:0}))")"
|
|
110
|
+
assert_eq "score 4/0/0/0 = 1" "1" "$($NODE -e "console.log(require('$VP').scoreFromCounts({CRITICAL:4,HIGH:0,MEDIUM:0,LOW:0}))")"
|
|
111
|
+
assert_eq "score 1/2/0/0 = 3" "3" "$($NODE -e "console.log(require('$VP').scoreFromCounts({CRITICAL:1,HIGH:2,MEDIUM:0,LOW:0}))")"
|
|
112
|
+
|
|
113
|
+
# --- MEDIUM/LOW only → still PASS (severity gate is C/H) ---
|
|
114
|
+
TMP=$(mktemp -d)
|
|
115
|
+
cat > "$TMP/panel.json" <<'EOF'
|
|
116
|
+
{ "phase": 4, "lenses": ["design"], "findings": [
|
|
117
|
+
{ "lens":"design", "file":"a.css", "line":3, "severity":"MEDIUM", "title":"missing empty state", "votes":{"real":2,"notReal":0} },
|
|
118
|
+
{ "lens":"design", "file":"b.css", "line":9, "severity":"LOW", "title":"console.log", "votes":{"real":2,"notReal":0} }
|
|
119
|
+
] }
|
|
120
|
+
EOF
|
|
121
|
+
$NODE "$VP" "$TMP/panel.json" >/dev/null 2>&1
|
|
122
|
+
assert_exit "MEDIUM+LOW only → PASS (exit 0)" 0 $?
|
|
123
|
+
rm -rf "$TMP"
|
|
124
|
+
|
|
125
|
+
# --- --write emits panel artifacts ---
|
|
126
|
+
TMP=$(mktemp -d); mkdir -p "$TMP/.planning"
|
|
127
|
+
cat > "$TMP/panel.json" <<'EOF'
|
|
128
|
+
{ "phase": 7, "lenses": ["security"], "findings": [
|
|
129
|
+
{ "lens":"security", "file":"s.ts", "line":1, "severity":"CRITICAL", "title":"leak", "votes":{"real":2,"notReal":0} }
|
|
130
|
+
] }
|
|
131
|
+
EOF
|
|
132
|
+
(cd "$TMP" && $NODE "$VP" panel.json --write >/dev/null 2>&1)
|
|
133
|
+
[ -f "$TMP/.planning/phase-7-verification-panel.json" ] && { echo " ✓ writes panel json artifact"; PASS=$((PASS+1)); } || { echo " ✗ no panel json artifact"; FAIL=$((FAIL+1)); }
|
|
134
|
+
[ -f "$TMP/.planning/phase-7-verification-panel.md" ] && { echo " ✓ writes panel md artifact"; PASS=$((PASS+1)); } || { echo " ✗ no panel md artifact"; FAIL=$((FAIL+1)); }
|
|
135
|
+
rm -rf "$TMP"
|
|
136
|
+
|
|
137
|
+
# --- assemble: merge per-lens finding files into one panel.json ---
|
|
138
|
+
TMP=$(mktemp -d); mkdir -p "$TMP/.planning"
|
|
139
|
+
cat > "$TMP/.planning/phase-5-panel-security.json" <<'EOF'
|
|
140
|
+
[{"file":"auth.ts","line":1,"severity":"CRITICAL","title":"leak"}]
|
|
141
|
+
EOF
|
|
142
|
+
cat > "$TMP/.planning/phase-5-panel-correctness.json" <<'EOF'
|
|
143
|
+
[{"file":"util.ts","line":9,"severity":"MEDIUM","title":"off by one"}]
|
|
144
|
+
EOF
|
|
145
|
+
(cd "$TMP" && $NODE "$VP" assemble 5 >/dev/null 2>&1)
|
|
146
|
+
assert_exit "assemble exits 0" 0 $?
|
|
147
|
+
ASM=$(cat "$TMP/.planning/phase-5-panel.json")
|
|
148
|
+
assert_contains "assemble tags security lens" "$ASM" '"lens": "security"'
|
|
149
|
+
assert_contains "assemble tags correctness lens" "$ASM" '"lens": "correctness"'
|
|
150
|
+
assert_contains "assemble zeroes votes" "$ASM" '"real": 0'
|
|
151
|
+
# round-trip: assembled panel aggregates to FAIL (the CRITICAL survives, unvoted)
|
|
152
|
+
(cd "$TMP" && $NODE "$VP" .planning/phase-5-panel.json >/dev/null 2>&1)
|
|
153
|
+
assert_exit "assembled panel aggregates (CRITICAL → FAIL)" 1 $?
|
|
154
|
+
rm -rf "$TMP"
|
|
155
|
+
|
|
156
|
+
# --- malformed input → exit 2 ---
|
|
157
|
+
$NODE "$VP" /nonexistent/panel.json >/dev/null 2>&1
|
|
158
|
+
assert_exit "missing panel file → exit 2" 2 $?
|
|
159
|
+
|
|
160
|
+
echo ""
|
|
161
|
+
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
162
|
+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# wave-plan.test.sh — bin/wave-plan.js (dependency-derived build schedule, R16)
|
|
3
|
+
# Run: bash tests/wave-plan.test.sh
|
|
4
|
+
|
|
5
|
+
PASS=0
|
|
6
|
+
FAIL=0
|
|
7
|
+
BIN_DIR="$(cd "$(dirname "$0")/../bin" && pwd)"
|
|
8
|
+
NODE="${NODE:-node}"
|
|
9
|
+
WP="$BIN_DIR/wave-plan.js"
|
|
10
|
+
|
|
11
|
+
assert_exit() {
|
|
12
|
+
local name="$1" expected="$2" actual="$3"
|
|
13
|
+
if [ "$expected" = "$actual" ]; then echo " ✓ $name"; PASS=$((PASS+1));
|
|
14
|
+
else echo " ✗ $name (expected exit $expected, got $actual)"; FAIL=$((FAIL+1)); fi
|
|
15
|
+
}
|
|
16
|
+
assert_contains() {
|
|
17
|
+
local name="$1" hay="$2" needle="$3"
|
|
18
|
+
if echo "$hay" | grep -qF "$needle"; then echo " ✓ $name"; PASS=$((PASS+1));
|
|
19
|
+
else echo " ✗ $name (missing '$needle' in: $hay)"; FAIL=$((FAIL+1)); fi
|
|
20
|
+
}
|
|
21
|
+
assert_eq() {
|
|
22
|
+
local name="$1" expected="$2" actual="$3"
|
|
23
|
+
if [ "$expected" = "$actual" ]; then echo " ✓ $name"; PASS=$((PASS+1));
|
|
24
|
+
else echo " ✗ $name (expected '$expected', got '$actual')"; FAIL=$((FAIL+1)); fi
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
echo "wave-plan.test.sh — bin/wave-plan.js"
|
|
28
|
+
echo ""
|
|
29
|
+
|
|
30
|
+
$NODE -c "$WP" 2>/dev/null && { echo " ✓ syntax valid"; PASS=$((PASS+1)); } || { echo " ✗ syntax invalid"; FAIL=$((FAIL+1)); }
|
|
31
|
+
|
|
32
|
+
# --- chain DAG: T1→T2→T3 → 3 sequential waves of 1 ---
|
|
33
|
+
TMP=$(mktemp -d)
|
|
34
|
+
cat > "$TMP/c.json" <<'EOF'
|
|
35
|
+
{ "phase": 1, "tasks": [
|
|
36
|
+
{"id":"T1","wave":1,"depends_on":[]},
|
|
37
|
+
{"id":"T2","wave":2,"depends_on":["T1"]},
|
|
38
|
+
{"id":"T3","wave":3,"depends_on":["T2"]}
|
|
39
|
+
] }
|
|
40
|
+
EOF
|
|
41
|
+
OUT=$($NODE "$WP" "$TMP/c.json" --json 2>&1)
|
|
42
|
+
assert_exit "chain → exit 0" 0 $?
|
|
43
|
+
assert_contains "chain has 3 batches" "$OUT" '"batch_count": 3'
|
|
44
|
+
rm -rf "$TMP"
|
|
45
|
+
|
|
46
|
+
# --- 6 independent tasks, auto cap 5 → 2 batches (5 + 1), all wave 1 ---
|
|
47
|
+
TMP=$(mktemp -d)
|
|
48
|
+
cat > "$TMP/c.json" <<'EOF'
|
|
49
|
+
{ "phase": 2, "tasks": [
|
|
50
|
+
{"id":"T1","wave":1,"depends_on":[]},
|
|
51
|
+
{"id":"T2","wave":1,"depends_on":[]},
|
|
52
|
+
{"id":"T3","wave":1,"depends_on":[]},
|
|
53
|
+
{"id":"T4","wave":1,"depends_on":[]},
|
|
54
|
+
{"id":"T5","wave":1,"depends_on":[]},
|
|
55
|
+
{"id":"T6","wave":1,"depends_on":[]}
|
|
56
|
+
] }
|
|
57
|
+
EOF
|
|
58
|
+
OUT=$($NODE "$WP" "$TMP/c.json" --json 2>&1)
|
|
59
|
+
assert_contains "6 independent → max_concurrency 5" "$OUT" '"max_concurrency": 5'
|
|
60
|
+
assert_contains "6 independent → 2 batches" "$OUT" '"batch_count": 2'
|
|
61
|
+
assert_contains "6 independent → 1 derived level" "$OUT" '"derived_levels": 1'
|
|
62
|
+
assert_contains "wide-level note emitted" "$OUT" "capped to batches of 5"
|
|
63
|
+
rm -rf "$TMP"
|
|
64
|
+
|
|
65
|
+
# --- tiny phase (<3 tasks) → auto sequential (cap 1) ---
|
|
66
|
+
TMP=$(mktemp -d)
|
|
67
|
+
cat > "$TMP/c.json" <<'EOF'
|
|
68
|
+
{ "phase": 3, "tasks": [
|
|
69
|
+
{"id":"T1","wave":1,"depends_on":[]},
|
|
70
|
+
{"id":"T2","wave":1,"depends_on":[]}
|
|
71
|
+
] }
|
|
72
|
+
EOF
|
|
73
|
+
OUT=$($NODE "$WP" "$TMP/c.json" --json 2>&1)
|
|
74
|
+
assert_contains "2 tasks → cap 1 (sequential)" "$OUT" '"max_concurrency": 1'
|
|
75
|
+
assert_contains "2 tasks → 2 batches" "$OUT" '"batch_count": 2'
|
|
76
|
+
rm -rf "$TMP"
|
|
77
|
+
|
|
78
|
+
# --- --parallel override ---
|
|
79
|
+
TMP=$(mktemp -d)
|
|
80
|
+
cat > "$TMP/c.json" <<'EOF'
|
|
81
|
+
{ "phase": 4, "tasks": [
|
|
82
|
+
{"id":"T1","wave":1,"depends_on":[]},
|
|
83
|
+
{"id":"T2","wave":1,"depends_on":[]},
|
|
84
|
+
{"id":"T3","wave":1,"depends_on":[]},
|
|
85
|
+
{"id":"T4","wave":1,"depends_on":[]}
|
|
86
|
+
] }
|
|
87
|
+
EOF
|
|
88
|
+
OUT=$($NODE "$WP" "$TMP/c.json" --parallel 2 --json 2>&1)
|
|
89
|
+
assert_contains "--parallel 2 sets cap" "$OUT" '"max_concurrency": 2'
|
|
90
|
+
assert_contains "--parallel 2 → 2 batches of 2" "$OUT" '"batch_count": 2'
|
|
91
|
+
# invalid --parallel
|
|
92
|
+
$NODE "$WP" "$TMP/c.json" --parallel 0 >/dev/null 2>&1
|
|
93
|
+
assert_exit "--parallel 0 → invocation error (exit 2)" 2 $?
|
|
94
|
+
rm -rf "$TMP"
|
|
95
|
+
|
|
96
|
+
# --- over-serialization: independent task declared in a deeper wave ---
|
|
97
|
+
TMP=$(mktemp -d)
|
|
98
|
+
cat > "$TMP/c.json" <<'EOF'
|
|
99
|
+
{ "phase": 5, "tasks": [
|
|
100
|
+
{"id":"T1","wave":1,"depends_on":[]},
|
|
101
|
+
{"id":"T2","wave":2,"depends_on":[]}
|
|
102
|
+
] }
|
|
103
|
+
EOF
|
|
104
|
+
OUT=$($NODE "$WP" "$TMP/c.json" --json 2>&1)
|
|
105
|
+
assert_contains "flags over-serialization" "$OUT" "deeper wave than the dependency graph requires"
|
|
106
|
+
# both are actually level 1 (no deps) → 1 derived level
|
|
107
|
+
assert_contains "both tasks derive to one level" "$OUT" '"derived_levels": 1'
|
|
108
|
+
rm -rf "$TMP"
|
|
109
|
+
|
|
110
|
+
# --- diamond DAG: T1; T2,T3 dep T1; T4 dep T2,T3 → 3 levels ---
|
|
111
|
+
TMP=$(mktemp -d)
|
|
112
|
+
cat > "$TMP/c.json" <<'EOF'
|
|
113
|
+
{ "phase": 6, "tasks": [
|
|
114
|
+
{"id":"T1","wave":1,"depends_on":[]},
|
|
115
|
+
{"id":"T2","wave":2,"depends_on":["T1"]},
|
|
116
|
+
{"id":"T3","wave":2,"depends_on":["T1"]},
|
|
117
|
+
{"id":"T4","wave":3,"depends_on":["T2","T3"]}
|
|
118
|
+
] }
|
|
119
|
+
EOF
|
|
120
|
+
OUT=$($NODE "$WP" "$TMP/c.json" --json 2>&1)
|
|
121
|
+
assert_contains "diamond → 3 derived levels" "$OUT" '"derived_levels": 3'
|
|
122
|
+
# middle level T2,T3 share a batch
|
|
123
|
+
assert_contains "diamond middle batch has T2+T3" "$OUT" '"T2",'
|
|
124
|
+
rm -rf "$TMP"
|
|
125
|
+
|
|
126
|
+
# --- cycle → exit 1 ---
|
|
127
|
+
TMP=$(mktemp -d)
|
|
128
|
+
cat > "$TMP/c.json" <<'EOF'
|
|
129
|
+
{ "phase": 7, "tasks": [
|
|
130
|
+
{"id":"T1","wave":1,"depends_on":["T2"]},
|
|
131
|
+
{"id":"T2","wave":1,"depends_on":["T1"]}
|
|
132
|
+
] }
|
|
133
|
+
EOF
|
|
134
|
+
$NODE "$WP" "$TMP/c.json" >/dev/null 2>&1
|
|
135
|
+
assert_exit "cycle → exit 1" 1 $?
|
|
136
|
+
OUT=$($NODE "$WP" "$TMP/c.json" --json 2>&1)
|
|
137
|
+
assert_contains "cycle reported" "$OUT" '"error": "CYCLE"'
|
|
138
|
+
rm -rf "$TMP"
|
|
139
|
+
|
|
140
|
+
# --- library: resolveConcurrency + deriveLevels units ---
|
|
141
|
+
assert_eq "resolveConcurrency(2,auto)=1" "1" "$($NODE -e "console.log(require('$WP').resolveConcurrency(2))")"
|
|
142
|
+
assert_eq "resolveConcurrency(5,auto)=5" "5" "$($NODE -e "console.log(require('$WP').resolveConcurrency(5))")"
|
|
143
|
+
assert_eq "resolveConcurrency(99,parallel=3)=3" "3" "$($NODE -e "console.log(require('$WP').resolveConcurrency(99,3))")"
|
|
144
|
+
LV=$($NODE -e "const w=require('$WP'); const r=w.deriveLevels([{id:'T1',depends_on:[]},{id:'T2',depends_on:['T1']}]); console.log(r.levels.get('T2'))" 2>&1)
|
|
145
|
+
assert_eq "deriveLevels T2 after T1 = 2" "2" "$LV"
|
|
146
|
+
|
|
147
|
+
# --- missing contract → exit 2 ---
|
|
148
|
+
$NODE "$WP" /nonexistent.json >/dev/null 2>&1
|
|
149
|
+
assert_exit "missing contract → exit 2" 2 $?
|
|
150
|
+
|
|
151
|
+
echo ""
|
|
152
|
+
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
153
|
+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|