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.
Files changed (72) hide show
  1. package/AGENTS.md +8 -5
  2. package/CHANGELOG.md +316 -0
  3. package/CLAUDE.md +3 -1
  4. package/agents/roadmapper.md +16 -14
  5. package/bin/agent-status.js +24 -11
  6. package/bin/batch-plan.js +111 -0
  7. package/bin/branch-hygiene.js +135 -0
  8. package/bin/command-surface.js +2 -0
  9. package/bin/compile-instructions.js +82 -0
  10. package/bin/design-tokens.js +131 -0
  11. package/bin/erp-event.js +177 -0
  12. package/bin/erp-retry.js +12 -1
  13. package/bin/eval-runner.js +218 -0
  14. package/bin/host-adapters.js +84 -12
  15. package/bin/install.js +44 -13
  16. package/bin/knowledge-flush.js +6 -3
  17. package/bin/last-report.js +207 -0
  18. package/bin/project-sync.js +315 -0
  19. package/bin/recall.js +172 -0
  20. package/bin/repo-map.js +188 -0
  21. package/bin/runtime-manifest.js +12 -0
  22. package/bin/state.js +112 -1
  23. package/bin/vault-access.js +82 -0
  24. package/bin/verify-panel.js +294 -0
  25. package/bin/wave-plan.js +211 -0
  26. package/docs/erp-contract.md +180 -0
  27. package/mcp/memory-mcp/server.js +257 -0
  28. package/package.json +6 -3
  29. package/qualia-design/design-dials.md +72 -0
  30. package/qualia-design/design-reference.md +24 -0
  31. package/rules/access.md +42 -0
  32. package/rules/codex-goal.md +28 -26
  33. package/rules/infrastructure.md +1 -1
  34. package/skills/qualia/SKILL.md +6 -0
  35. package/skills/qualia-build/SKILL.md +43 -9
  36. package/skills/qualia-eval/SKILL.md +83 -0
  37. package/skills/qualia-feature/SKILL.md +20 -4
  38. package/skills/qualia-fix/SKILL.md +13 -1
  39. package/skills/qualia-map/SKILL.md +15 -0
  40. package/skills/qualia-milestone/SKILL.md +12 -6
  41. package/skills/qualia-new/REFERENCE.md +6 -4
  42. package/skills/qualia-new/SKILL.md +41 -15
  43. package/skills/qualia-plan/SKILL.md +2 -2
  44. package/skills/qualia-polish/SKILL.md +3 -2
  45. package/skills/qualia-recall/SKILL.md +76 -0
  46. package/skills/qualia-report/SKILL.md +10 -0
  47. package/skills/qualia-scope/SKILL.md +3 -3
  48. package/skills/qualia-ship/SKILL.md +34 -4
  49. package/skills/qualia-update/SKILL.md +4 -0
  50. package/skills/qualia-verify/SKILL.md +53 -24
  51. package/templates/DESIGN.md +15 -0
  52. package/templates/instructions.md +32 -0
  53. package/templates/journey.md +1 -1
  54. package/templates/project-discovery.md +30 -23
  55. package/templates/requirements.md +7 -7
  56. package/tests/agent-status.test.sh +15 -0
  57. package/tests/batch-plan.test.sh +56 -0
  58. package/tests/branch-hygiene.test.sh +93 -0
  59. package/tests/design-tokens.test.sh +53 -0
  60. package/tests/erp-event.test.sh +78 -0
  61. package/tests/eval-runner.test.sh +147 -0
  62. package/tests/instructions.test.sh +109 -0
  63. package/tests/last-report.test.sh +156 -0
  64. package/tests/lib.test.sh +29 -4
  65. package/tests/project-sync.test.sh +175 -0
  66. package/tests/recall.test.sh +91 -0
  67. package/tests/repo-map.test.sh +70 -0
  68. package/tests/run-all.sh +12 -0
  69. package/tests/runner.js +363 -33
  70. package/tests/state.test.sh +92 -0
  71. package/tests/verify-panel.test.sh +162 -0
  72. package/tests/wave-plan.test.sh +153 -0
@@ -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