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
@@ -0,0 +1,56 @@
1
+ #!/bin/bash
2
+ # batch-plan.test.sh — bin/batch-plan.js (/qualia-build --batch split, R20)
3
+ # Run: bash tests/batch-plan.test.sh
4
+
5
+ PASS=0
6
+ FAIL=0
7
+ BIN_DIR="$(cd "$(dirname "$0")/../bin" && pwd)"
8
+ NODE="${NODE:-node}"
9
+ BP="$BIN_DIR/batch-plan.js"
10
+
11
+ assert_exit() { if [ "$2" = "$3" ]; then echo " ✓ $1"; PASS=$((PASS+1)); else echo " ✗ $1 (expected exit $2, got $3)"; FAIL=$((FAIL+1)); fi; }
12
+ assert_contains() { if echo "$2" | grep -qF -- "$3"; then echo " ✓ $1"; PASS=$((PASS+1)); else echo " ✗ $1 (missing '$3')"; FAIL=$((FAIL+1)); fi; }
13
+ jcheck() { echo "$1" | $NODE -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const p=JSON.parse(s);process.exit(($2)?0:1)})"; }
14
+
15
+ echo "batch-plan.test.sh — bin/batch-plan.js"
16
+ echo ""
17
+
18
+ $NODE -c "$BP" 2>/dev/null && { echo " ✓ syntax valid"; PASS=$((PASS+1)); } || { echo " ✗ syntax invalid"; FAIL=$((FAIL+1)); }
19
+
20
+ FILES=$(seq -f "src/f%g.ts" 1 23)
21
+
22
+ # ── split math ──
23
+ JSON=$($NODE "$BP" $FILES --batch-size 10 --max-workers 3 --json 2>&1); assert_exit "valid run → exit 0" 0 $?
24
+ jcheck "$JSON" "p.total_files===23" && { echo " ✓ counts all files"; PASS=$((PASS+1)); } || { echo " ✗ total_files"; FAIL=$((FAIL+1)); }
25
+ jcheck "$JSON" "p.batch_count===3" && { echo " ✓ ceil(23/10)=3 batches"; PASS=$((PASS+1)); } || { echo " ✗ batch_count"; FAIL=$((FAIL+1)); }
26
+ jcheck "$JSON" "p.batches.every(b=>b.files.length<=10)" && { echo " ✓ every batch ≤ batch-size"; PASS=$((PASS+1)); } || { echo " ✗ batch size cap"; FAIL=$((FAIL+1)); }
27
+
28
+ # ── disjoint + complete: every file in exactly one batch ──
29
+ jcheck "$JSON" "(()=>{const a=p.batches.flatMap(b=>b.files);return a.length===23&&new Set(a).size===23})()" \
30
+ && { echo " ✓ file-disjoint + complete (each file exactly once)"; PASS=$((PASS+1)); } || { echo " ✗ disjointness"; FAIL=$((FAIL+1)); }
31
+
32
+ # ── waves cap at max-workers ──
33
+ jcheck "$JSON" "p.waves.every(w=>w.length<=3)" && { echo " ✓ waves capped at max-workers"; PASS=$((PASS+1)); } || { echo " ✗ wave cap"; FAIL=$((FAIL+1)); }
34
+ jcheck "$JSON" "typeof p.staging_branch==='string'&&Array.isArray(p.waves)&&Array.isArray(p.batches)" \
35
+ && { echo " ✓ --json shape (staging_branch/waves/batches)"; PASS=$((PASS+1)); } || { echo " ✗ json shape"; FAIL=$((FAIL+1)); }
36
+
37
+ # ── dedup ──
38
+ jcheck "$($NODE "$BP" a a b --json 2>&1)" "p.total_files===2" && { echo " ✓ de-dups repeated files"; PASS=$((PASS+1)); } || { echo " ✗ dedup"; FAIL=$((FAIL+1)); }
39
+
40
+ # ── --from FILE + stdin ──
41
+ TMP=$(mktemp -d); printf 'x.ts\ny.ts\nz.ts\n' > "$TMP/list.txt"
42
+ jcheck "$($NODE "$BP" --from "$TMP/list.txt" --json 2>&1)" "p.total_files===3" && { echo " ✓ reads --from FILE"; PASS=$((PASS+1)); } || { echo " ✗ --from file"; FAIL=$((FAIL+1)); }
43
+ jcheck "$(printf 'a\nb\n' | $NODE "$BP" --from - --json 2>&1)" "p.total_files===2" && { echo " ✓ reads --from - (stdin)"; PASS=$((PASS+1)); } || { echo " ✗ --from stdin"; FAIL=$((FAIL+1)); }
44
+
45
+ # ── human output mentions staging branch ──
46
+ assert_contains "human output names staging branch" "$($NODE "$BP" a b --staging mig/x 2>&1)" "mig/x"
47
+
48
+ # ── edge cases ──
49
+ $NODE "$BP" --from /no/such/list.txt >/dev/null 2>&1; assert_exit "missing --from file → exit 2" 2 $?
50
+ EOUT=$($NODE "$BP" 2>&1); assert_exit "no files → exit 0" 0 $?
51
+ assert_contains "no files → friendly message" "$EOUT" "nothing to migrate"
52
+
53
+ rm -rf "$TMP"
54
+ echo ""
55
+ echo "=== Results: $PASS passed, $FAIL failed ==="
56
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,93 @@
1
+ #!/bin/bash
2
+ # branch-hygiene.test.sh — bin/branch-hygiene.js (clock-out stranded-branch sweep)
3
+ # Run: bash tests/branch-hygiene.test.sh
4
+
5
+ PASS=0
6
+ FAIL=0
7
+ BIN_DIR="$(cd "$(dirname "$0")/../bin" && pwd)"
8
+ NODE="${NODE:-node}"
9
+ BH="$BIN_DIR/branch-hygiene.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
+
22
+ # fresh repo on main with one commit; prints the dir (caller rm -rf)
23
+ setup_repo() {
24
+ local tmp
25
+ tmp=$(mktemp -d)
26
+ (cd "$tmp" \
27
+ && git init -q \
28
+ && git checkout -q -b main 2>/dev/null \
29
+ && git config user.email t@t.com && git config user.name T \
30
+ && echo seed > seed.txt && git add seed.txt && git commit -q -m seed)
31
+ echo "$tmp"
32
+ }
33
+
34
+ echo "branch-hygiene.test.sh — bin/branch-hygiene.js"
35
+ echo ""
36
+
37
+ $NODE -c "$BH" 2>/dev/null && { echo " ✓ syntax valid"; PASS=$((PASS+1)); } || { echo " ✗ syntax invalid"; FAIL=$((FAIL+1)); }
38
+
39
+ # --- not a git repo → exit 2 ---
40
+ TMP=$(mktemp -d)
41
+ (cd "$TMP" && $NODE "$BH" >/dev/null 2>&1)
42
+ assert_exit "not a git repo → exit 2" 2 $?
43
+ rm -rf "$TMP"
44
+
45
+ # --- clean: only main → exit 0 ---
46
+ TMP=$(setup_repo)
47
+ (cd "$TMP" && $NODE "$BH" >/dev/null 2>&1)
48
+ assert_exit "clean repo (main only) → exit 0" 0 $?
49
+ OUT=$(cd "$TMP" && $NODE "$BH" 2>&1)
50
+ assert_contains "reports clean" "$OUT" "clean"
51
+ rm -rf "$TMP"
52
+
53
+ # --- stranded feature branch ahead of main → exit 1, listed ---
54
+ TMP=$(setup_repo)
55
+ (cd "$TMP" && git checkout -q -b feat/stranded && echo work > w.txt && git add w.txt && git commit -q -m "wip work")
56
+ (cd "$TMP" && $NODE "$BH" >/dev/null 2>&1)
57
+ assert_exit "branch ahead of main → exit 1" 1 $?
58
+ OUT=$(cd "$TMP" && $NODE "$BH" 2>&1)
59
+ assert_contains "lists the stranded branch" "$OUT" "feat/stranded"
60
+ assert_contains "shows commits ahead" "$OUT" "+1 commit"
61
+ # json shape
62
+ OUT=$(cd "$TMP" && $NODE "$BH" --json 2>&1)
63
+ assert_contains "json stranded entry" "$OUT" '"branch": "feat/stranded"'
64
+ assert_contains "json ahead count" "$OUT" '"ahead": 1'
65
+ rm -rf "$TMP"
66
+
67
+ # --- once integrated (ff-merged) into main → no longer stranded → exit 0 ---
68
+ TMP=$(setup_repo)
69
+ (cd "$TMP" && git checkout -q -b feat/done && echo x > x.txt && git add x.txt && git commit -q -m "done work")
70
+ (cd "$TMP" && git checkout -q main && git merge -q --ff-only feat/done)
71
+ (cd "$TMP" && $NODE "$BH" >/dev/null 2>&1)
72
+ assert_exit "ff-merged branch no longer stranded → exit 0" 0 $?
73
+ rm -rf "$TMP"
74
+
75
+ # --- master as the base branch is detected ---
76
+ TMP=$(mktemp -d)
77
+ (cd "$TMP" && git init -q && git checkout -q -b master 2>/dev/null && git config user.email t@t.com && git config user.name T && echo s > s.txt && git add s.txt && git commit -q -m s)
78
+ (cd "$TMP" && git checkout -q -b feature && echo y > y.txt && git add y.txt && git commit -q -m y)
79
+ OUT=$(cd "$TMP" && $NODE "$BH" --json 2>&1)
80
+ assert_contains "detects master as base" "$OUT" '"base": "master"'
81
+ assert_contains "stranded vs master" "$OUT" '"branch": "feature"'
82
+ rm -rf "$TMP"
83
+
84
+ # --- library: analyze() returns structured result ---
85
+ TMP=$(setup_repo)
86
+ (cd "$TMP" && git checkout -q -b b1 && echo a>a && git add a && git commit -q -m a)
87
+ RES=$($NODE -e "console.log(JSON.stringify(require('$BH').analyze('$TMP').stranded.length))" 2>&1)
88
+ assert_contains "analyze() finds 1 stranded" "$RES" "1"
89
+ rm -rf "$TMP"
90
+
91
+ echo ""
92
+ echo "=== Results: $PASS passed, $FAIL failed ==="
93
+ [ "$FAIL" -eq 0 ] && exit 0 || exit 1
@@ -0,0 +1,53 @@
1
+ #!/bin/bash
2
+ # design-tokens.test.sh — bin/design-tokens.js (per-client token registry, R10)
3
+ # Run: bash tests/design-tokens.test.sh
4
+
5
+ PASS=0
6
+ FAIL=0
7
+ BIN_DIR="$(cd "$(dirname "$0")/../bin" && pwd)"
8
+ NODE="${NODE:-node}"
9
+ DT="$BIN_DIR/design-tokens.js"
10
+
11
+ assert_exit() { if [ "$2" = "$3" ]; then echo " ✓ $1"; PASS=$((PASS+1)); else echo " ✗ $1 (expected exit $2, got $3)"; FAIL=$((FAIL+1)); fi; }
12
+ assert_contains() { if echo "$2" | grep -qF -- "$3"; then echo " ✓ $1"; PASS=$((PASS+1)); else echo " ✗ $1 (missing '$3')"; FAIL=$((FAIL+1)); fi; }
13
+
14
+ echo "design-tokens.test.sh — bin/design-tokens.js"
15
+ echo ""
16
+
17
+ $NODE -c "$DT" 2>/dev/null && { echo " ✓ syntax valid"; PASS=$((PASS+1)); } || { echo " ✗ syntax invalid"; FAIL=$((FAIL+1)); }
18
+
19
+ TMP=$(mktemp -d)
20
+
21
+ # ── init ──
22
+ $NODE "$DT" init --out "$TMP/tokens.json" >/dev/null 2>&1; assert_exit "init → exit 0" 0 $?
23
+ INITJSON=$(cat "$TMP/tokens.json")
24
+ echo "$INITJSON" | $NODE -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const j=JSON.parse(s);process.exit(j.color&&j.font?0:1)})" \
25
+ && { echo " ✓ init writes valid registry JSON (color+font)"; PASS=$((PASS+1)); } || { echo " ✗ init JSON shape"; FAIL=$((FAIL+1)); }
26
+
27
+ # ── compile → CSS ──
28
+ CSS=$($NODE "$DT" compile "$TMP/tokens.json" 2>&1); assert_exit "compile → exit 0" 0 $?
29
+ assert_contains "emits :root block" "$CSS" ":root {"
30
+ assert_contains "flattens nested color → var" "$CSS" "--color-accent:"
31
+ assert_contains "flattens nested font → var" "$CSS" "--font-sans:"
32
+ assert_contains "flattens nested radius → var" "$CSS" "--radius-md:"
33
+ assert_contains "carries the hardcoded-hex ban reminder" "$CSS" "ABS-HEX-IN-JSX"
34
+
35
+ # ── compile --json (flat map) ──
36
+ FLAT=$($NODE "$DT" compile "$TMP/tokens.json" --json 2>&1)
37
+ echo "$FLAT" | $NODE -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const j=JSON.parse(s);process.exit(j['color-bg']?0:1)})" \
38
+ && { echo " ✓ --json flat var map valid"; PASS=$((PASS+1)); } || { echo " ✗ --json flat map"; FAIL=$((FAIL+1)); }
39
+
40
+ # ── compile --out writes file ──
41
+ $NODE "$DT" compile "$TMP/tokens.json" --out "$TMP/tokens.css" >/dev/null 2>&1
42
+ [ -f "$TMP/tokens.css" ] && grep -q "^:root" "$TMP/tokens.css" && { echo " ✓ compile --out writes tokens.css"; PASS=$((PASS+1)); } || { echo " ✗ compile --out"; FAIL=$((FAIL+1)); }
43
+
44
+ # ── error paths ──
45
+ $NODE "$DT" compile /no/such/file.json >/dev/null 2>&1; assert_exit "missing file → exit 2" 2 $?
46
+ echo '{ broken' > "$TMP/bad.json"
47
+ $NODE "$DT" compile "$TMP/bad.json" >/dev/null 2>&1; assert_exit "invalid JSON → exit 2" 2 $?
48
+ $NODE "$DT" bogus-cmd >/dev/null 2>&1; assert_exit "unknown command → exit 2" 2 $?
49
+
50
+ rm -rf "$TMP"
51
+ echo ""
52
+ echo "=== Results: $PASS passed, $FAIL failed ==="
53
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,78 @@
1
+ #!/bin/bash
2
+ # erp-event.test.sh — bin/erp-event.js (framework EMIT side of the R14 event log)
3
+ # Run: bash tests/erp-event.test.sh
4
+
5
+ PASS=0
6
+ FAIL=0
7
+ BIN_DIR="$(cd "$(dirname "$0")/../bin" && pwd)"
8
+ NODE="${NODE:-node}"
9
+ EE="$BIN_DIR/erp-event.js"
10
+
11
+ assert_exit() { if [ "$2" = "$3" ]; then echo " ✓ $1"; PASS=$((PASS+1)); else echo " ✗ $1 (expected exit $2, got $3)"; FAIL=$((FAIL+1)); fi; }
12
+ assert_contains() { if echo "$2" | grep -qF -- "$3"; then echo " ✓ $1"; PASS=$((PASS+1)); else echo " ✗ $1 (missing '$3')"; FAIL=$((FAIL+1)); fi; }
13
+ assert_absent() { if echo "$2" | grep -qF -- "$3"; then echo " ✗ $1 (unexpected '$3')"; FAIL=$((FAIL+1)); else echo " ✓ $1"; PASS=$((PASS+1)); fi; }
14
+
15
+ echo "erp-event.test.sh — bin/erp-event.js"
16
+ echo ""
17
+
18
+ $NODE -c "$EE" 2>/dev/null && { echo " ✓ syntax valid"; PASS=$((PASS+1)); } || { echo " ✗ syntax invalid"; FAIL=$((FAIL+1)); }
19
+
20
+ # ── unit: build + sign (the cross-repo contract) ──
21
+ $NODE -e '
22
+ const { buildSignedEvent, signingContent, computeSignature } = require("'"$EE"'");
23
+ const cfg = { code: "QS-FAWZI-11", installed_by: "Fawzi", role: "OWNER" };
24
+ const b = buildSignedEvent("verify_pass", { id: "evt1", now: 1750000000, apiKey: "qlt_k", config: cfg, targets: [{type:"project",ref:"acme"}] });
25
+ const ok =
26
+ b.event.action === "verify_pass" &&
27
+ b.event.actor.code === "QS-FAWZI-11" && b.event.actor.role === "OWNER" &&
28
+ b.headers["Qualia-Event-Id"] === "evt1" &&
29
+ b.headers["Qualia-Event-Timestamp"] === "1750000000" &&
30
+ // signature must equal a fresh recompute over `${id}.${ts}.${payload}` (the ERP verify)
31
+ b.headers["Qualia-Signature"] === computeSignature(signingContent("evt1","1750000000",b.payload), "qlt_k") &&
32
+ // and must NOT match a wrong key
33
+ b.headers["Qualia-Signature"] !== computeSignature(signingContent("evt1","1750000000",b.payload), "qlt_other");
34
+ process.exit(ok ? 0 : 1);
35
+ ' && { echo " ✓ builds envelope + signature byte-matches the ERP verify algorithm"; PASS=$((PASS+1)); } || { echo " ✗ build/sign contract"; FAIL=$((FAIL+1)); }
36
+
37
+ # unsigned when no key: no Qualia-Signature header, still a valid envelope
38
+ $NODE -e '
39
+ const { buildSignedEvent } = require("'"$EE"'");
40
+ const b = buildSignedEvent("session_started", { id:"e", now:1, apiKey:"", config:{} });
41
+ process.exit(b.headers["Qualia-Signature"] === undefined && b.headers["Qualia-Event-Id"] === "e" ? 0 : 1);
42
+ ' && { echo " ✓ no API key → unsigned envelope (Bearer still authenticates server-side)"; PASS=$((PASS+1)); } || { echo " ✗ unsigned path"; FAIL=$((FAIL+1)); }
43
+
44
+ # ── CLI behavior with a temp install home ──
45
+ HOME_DIR=$(mktemp -d)
46
+ mkdir -p "$HOME_DIR/bin"
47
+ cp "$BIN_DIR/erp-retry.js" "$HOME_DIR/bin/erp-retry.js"
48
+ printf '{"role":"OWNER","code":"QS-FAWZI-11","installed_by":"Fawzi","erp":{"enabled":true,"url":"https://example.test"}}' > "$HOME_DIR/.qualia-config.json"
49
+ printf 'qlt_testkey' > "$HOME_DIR/.erp-api-key"
50
+ RUN(){ QUALIA_HOME="$HOME_DIR" $NODE "$EE" "$@"; }
51
+
52
+ # dry-run: does NOT enqueue
53
+ OUT=$(RUN emit verify_pass --target project:x --dry-run --json 2>&1); assert_exit "dry-run → exit 0" 0 $?
54
+ assert_contains "dry-run builds a signature" "$OUT" "Qualia-Signature"
55
+ [ ! -f "$HOME_DIR/.erp-retry-queue.json" ] && { echo " ✓ dry-run does not enqueue"; PASS=$((PASS+1)); } || { echo " ✗ dry-run enqueued"; FAIL=$((FAIL+1)); }
56
+
57
+ # real emit: enqueues an item carrying the signed headers + /events url
58
+ RUN emit build_wave_started --target project:x --project 7b5d3b4e-2b8a-4de4-91a1-9b2f3182f5ef >/dev/null 2>&1
59
+ Q=$(cat "$HOME_DIR/.erp-retry-queue.json" 2>/dev/null)
60
+ assert_contains "emit enqueues to /api/v1/events" "$Q" "/api/v1/events"
61
+ assert_contains "queued item carries the signature header" "$Q" "Qualia-Signature"
62
+ assert_contains "queued item carries the event action" "$Q" "build_wave_started"
63
+
64
+ # ERP disabled → skipped, no enqueue
65
+ HOME2=$(mktemp -d)
66
+ printf '{"role":"OWNER","erp":{"enabled":false}}' > "$HOME2/.qualia-config.json"
67
+ OUT=$(QUALIA_HOME="$HOME2" $NODE "$EE" emit verify_fail 2>&1); assert_exit "erp disabled → exit 0" 0 $?
68
+ assert_contains "erp disabled → skipped" "$OUT" "skipped"
69
+ [ ! -f "$HOME2/.erp-retry-queue.json" ] && { echo " ✓ erp disabled → no enqueue"; PASS=$((PASS+1)); } || { echo " ✗ enqueued while disabled"; FAIL=$((FAIL+1)); }
70
+
71
+ # bad invocation
72
+ $NODE "$EE" >/dev/null 2>&1; assert_exit "no action → exit 2" 2 $?
73
+ $NODE "$EE" emit >/dev/null 2>&1; assert_exit "emit without action → exit 2" 2 $?
74
+
75
+ rm -rf "$HOME_DIR" "$HOME2"
76
+ echo ""
77
+ echo "=== Results: $PASS passed, $FAIL failed ==="
78
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,147 @@
1
+ #!/bin/bash
2
+ # eval-runner.test.sh — bin/eval-runner.js (layered AI-feature eval, R7)
3
+ # Run: bash tests/eval-runner.test.sh
4
+
5
+ PASS=0
6
+ FAIL=0
7
+ BIN_DIR="$(cd "$(dirname "$0")/../bin" && pwd)"
8
+ NODE="${NODE:-node}"
9
+ ER="$BIN_DIR/eval-runner.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
+
22
+ echo "eval-runner.test.sh — bin/eval-runner.js"
23
+ echo ""
24
+
25
+ $NODE -c "$ER" 2>/dev/null && { echo " ✓ syntax valid"; PASS=$((PASS+1)); } || { echo " ✗ syntax invalid"; FAIL=$((FAIL+1)); }
26
+
27
+ # --- all deterministic assertions pass → PASS ---
28
+ TMP=$(mktemp -d)
29
+ cat > "$TMP/s.json" <<'EOF'
30
+ { "feature":"support-chat", "cases":[
31
+ { "name":"refund", "output":"We refund within 30 days.", "latency_ms":1200, "cost_usd":0.008,
32
+ "assert":[
33
+ {"type":"contains","value":"30 days"},
34
+ {"type":"not_contains","value":"I cannot help"},
35
+ {"type":"regex","value":"\\b30\\b"},
36
+ {"type":"min_length","value":5},
37
+ {"type":"max_latency_ms","value":2000},
38
+ {"type":"max_cost_usd","value":0.02}
39
+ ] } ] }
40
+ EOF
41
+ $NODE "$ER" "$TMP/s.json" >/dev/null 2>&1
42
+ assert_exit "all deterministic pass → exit 0" 0 $?
43
+ OUT=$($NODE "$ER" "$TMP/s.json" 2>&1)
44
+ assert_contains "reports EVAL PASS" "$OUT" "EVAL PASS"
45
+ rm -rf "$TMP"
46
+
47
+ # --- a failing contains → FAIL with the failing assertion shown ---
48
+ TMP=$(mktemp -d)
49
+ cat > "$TMP/s.json" <<'EOF'
50
+ { "feature":"chat", "cases":[
51
+ { "name":"refusal", "output":"I cannot help with that.",
52
+ "assert":[ {"type":"contains","value":"30 days"}, {"type":"not_contains","value":"I cannot help"} ] } ] }
53
+ EOF
54
+ $NODE "$ER" "$TMP/s.json" >/dev/null 2>&1
55
+ assert_exit "failing assertion → exit 1" 1 $?
56
+ OUT=$($NODE "$ER" "$TMP/s.json" 2>&1)
57
+ assert_contains "shows failing case" "$OUT" "refusal"
58
+ rm -rf "$TMP"
59
+
60
+ # --- latency over budget → FAIL ---
61
+ TMP=$(mktemp -d)
62
+ cat > "$TMP/s.json" <<'EOF'
63
+ { "feature":"chat", "cases":[
64
+ { "name":"slow", "output":"ok", "latency_ms":5000, "assert":[ {"type":"max_latency_ms","value":2000} ] } ] }
65
+ EOF
66
+ $NODE "$ER" "$TMP/s.json" >/dev/null 2>&1
67
+ assert_exit "latency over budget → exit 1" 1 $?
68
+ # missing latency metric but asserted → fail (no silent pass)
69
+ cat > "$TMP/s2.json" <<'EOF'
70
+ { "feature":"chat", "cases":[ { "name":"no-metric", "output":"ok", "assert":[ {"type":"max_latency_ms","value":2000} ] } ] }
71
+ EOF
72
+ $NODE "$ER" "$TMP/s2.json" >/dev/null 2>&1
73
+ assert_exit "asserting latency with none recorded → exit 1" 1 $?
74
+ rm -rf "$TMP"
75
+
76
+ # --- json_path + json_valid ---
77
+ TMP=$(mktemp -d)
78
+ cat > "$TMP/s.json" <<'EOF'
79
+ { "feature":"rag", "cases":[
80
+ { "name":"structured", "output":"{\"answer\":\"yes\",\"sources\":[{\"id\":\"doc1\"}]}",
81
+ "assert":[
82
+ {"type":"json_valid"},
83
+ {"type":"json_path","path":"answer","equals":"yes"},
84
+ {"type":"json_path","path":"sources.0.id","contains":"doc"}
85
+ ] } ] }
86
+ EOF
87
+ $NODE "$ER" "$TMP/s.json" >/dev/null 2>&1
88
+ assert_exit "json_path + json_valid pass → exit 0" 0 $?
89
+ # invalid json under json_valid → fail
90
+ cat > "$TMP/bad.json" <<'EOF'
91
+ { "feature":"rag", "cases":[ { "name":"notjson", "output":"not json", "assert":[ {"type":"json_valid"} ] } ] }
92
+ EOF
93
+ $NODE "$ER" "$TMP/bad.json" >/dev/null 2>&1
94
+ assert_exit "json_valid on non-json → exit 1" 1 $?
95
+ rm -rf "$TMP"
96
+
97
+ # --- llm_rubric: verdict pass / fail / pending ---
98
+ TMP=$(mktemp -d)
99
+ cat > "$TMP/pass.json" <<'EOF'
100
+ { "feature":"chat", "cases":[ { "name":"r", "output":"grounded answer", "assert":[ {"type":"llm_rubric","rubric":"grounded","verdict":"pass"} ] } ] }
101
+ EOF
102
+ $NODE "$ER" "$TMP/pass.json" >/dev/null 2>&1
103
+ assert_exit "llm_rubric verdict=pass → exit 0" 0 $?
104
+ cat > "$TMP/fail.json" <<'EOF'
105
+ { "feature":"chat", "cases":[ { "name":"r", "output":"hallucination", "assert":[ {"type":"llm_rubric","rubric":"grounded","verdict":"fail"} ] } ] }
106
+ EOF
107
+ $NODE "$ER" "$TMP/fail.json" >/dev/null 2>&1
108
+ assert_exit "llm_rubric verdict=fail → exit 1" 1 $?
109
+ cat > "$TMP/pending.json" <<'EOF'
110
+ { "feature":"chat", "cases":[ { "name":"r", "output":"x", "assert":[ {"type":"llm_rubric","rubric":"grounded"} ] } ] }
111
+ EOF
112
+ $NODE "$ER" "$TMP/pending.json" >/dev/null 2>&1
113
+ assert_exit "unjudged llm_rubric → PENDING → exit 1 (no silent pass)" 1 $?
114
+ OUT=$($NODE "$ER" "$TMP/pending.json" 2>&1)
115
+ assert_contains "reports unjudged rubric count" "$OUT" "unjudged"
116
+ rm -rf "$TMP"
117
+
118
+ # --- output_file resolution + --write artifact ---
119
+ TMP=$(mktemp -d); mkdir -p "$TMP/.planning"
120
+ echo -n "the answer is 42" > "$TMP/out.txt"
121
+ cat > "$TMP/s.json" <<'EOF'
122
+ { "feature":"file-feature", "cases":[ { "name":"fromfile", "output_file":"out.txt", "assert":[ {"type":"contains","value":"42"} ] } ] }
123
+ EOF
124
+ (cd "$TMP" && $NODE "$ER" s.json --write >/dev/null 2>&1)
125
+ assert_exit "output_file case passes → exit 0" 0 $?
126
+ [ -f "$TMP/.planning/evals/eval-file-feature.json" ] && { echo " ✓ --write emits eval artifact"; PASS=$((PASS+1)); } || { echo " ✗ no eval artifact"; FAIL=$((FAIL+1)); }
127
+ # missing output_file → fail, not crash
128
+ cat > "$TMP/miss.json" <<'EOF'
129
+ { "feature":"x", "cases":[ { "name":"gone", "output_file":"nope.txt", "assert":[ {"type":"contains","value":"y"} ] } ] }
130
+ EOF
131
+ (cd "$TMP" && $NODE "$ER" miss.json >/dev/null 2>&1)
132
+ assert_exit "missing output_file → exit 1 (graceful)" 1 $?
133
+ rm -rf "$TMP"
134
+
135
+ # --- library: runAssertion unit ---
136
+ U=$($NODE -e "const e=require('$ER'); console.log(e.runAssertion({type:'contains',value:'hi'},{output:'oh hi there'}).ok)" 2>&1)
137
+ assert_contains "runAssertion contains true" "$U" "true"
138
+ U=$($NODE -e "const e=require('$ER'); console.log(e.getPath({a:{b:[{c:9}]}},'a.b.0.c'))" 2>&1)
139
+ assert_contains "getPath nested+index" "$U" "9"
140
+
141
+ # --- malformed suite → exit 2 ---
142
+ $NODE "$ER" /nonexistent-suite.json >/dev/null 2>&1
143
+ assert_exit "missing suite → exit 2" 2 $?
144
+
145
+ echo ""
146
+ echo "=== Results: $PASS passed, $FAIL failed ==="
147
+ [ "$FAIL" -eq 0 ] && exit 0 || exit 1
@@ -0,0 +1,109 @@
1
+ #!/bin/bash
2
+ # instructions.test.sh — single-source instruction compile (R4) + host adapter
3
+ # contract (R5). Run: bash tests/instructions.test.sh
4
+
5
+ PASS=0
6
+ FAIL=0
7
+ FRAMEWORK_DIR="$(cd "$(dirname "$0")/.." && pwd)"
8
+ NODE="${NODE:-node}"
9
+ HA="$FRAMEWORK_DIR/bin/host-adapters.js"
10
+ CI="$FRAMEWORK_DIR/bin/compile-instructions.js"
11
+
12
+ assert_exit() {
13
+ local name="$1" expected="$2" actual="$3"
14
+ if [ "$expected" = "$actual" ]; then echo " ✓ $name"; PASS=$((PASS+1));
15
+ else echo " ✗ $name (expected exit $expected, got $actual)"; FAIL=$((FAIL+1)); fi
16
+ }
17
+ assert_eq() {
18
+ local name="$1" expected="$2" actual="$3"
19
+ if [ "$expected" = "$actual" ]; then echo " ✓ $name"; PASS=$((PASS+1));
20
+ else echo " ✗ $name (expected '$expected', got '$actual')"; FAIL=$((FAIL+1)); fi
21
+ }
22
+ assert_contains() {
23
+ local name="$1" hay="$2" needle="$3"
24
+ if echo "$hay" | grep -qF "$needle"; then echo " ✓ $name"; PASS=$((PASS+1));
25
+ else echo " ✗ $name (missing '$needle')"; FAIL=$((FAIL+1)); fi
26
+ }
27
+ refute_contains() {
28
+ local name="$1" hay="$2" needle="$3"
29
+ if echo "$hay" | grep -qF "$needle"; then echo " ✗ $name (found '$needle')"; FAIL=$((FAIL+1));
30
+ else echo " ✓ $name"; PASS=$((PASS+1)); fi
31
+ }
32
+
33
+ echo "instructions.test.sh — single-source compile (R4) + host contract (R5)"
34
+ echo ""
35
+
36
+ # syntax
37
+ $NODE -c "$HA" 2>/dev/null && { echo " ✓ host-adapters syntax valid"; PASS=$((PASS+1)); } || { echo " ✗ host-adapters syntax"; FAIL=$((FAIL+1)); }
38
+ $NODE -c "$CI" 2>/dev/null && { echo " ✓ compile-instructions syntax valid"; PASS=$((PASS+1)); } || { echo " ✗ compile-instructions syntax"; FAIL=$((FAIL+1)); }
39
+
40
+ # --- R4: drift guard — committed files match the canonical ---
41
+ $NODE "$CI" --check >/dev/null 2>&1
42
+ assert_exit "compile-instructions --check passes (no drift)" 0 $?
43
+
44
+ # A canonical edit not reflected in the artifacts is caught by --check.
45
+ TMPCANON=$(mktemp)
46
+ cp "$FRAMEWORK_DIR/templates/instructions.md" "$TMPCANON"
47
+ printf '\nDRIFT-MARKER\n' >> "$FRAMEWORK_DIR/templates/instructions.md"
48
+ $NODE "$CI" --check >/dev/null 2>&1
49
+ RC=$?
50
+ cp "$TMPCANON" "$FRAMEWORK_DIR/templates/instructions.md" # restore immediately
51
+ rm -f "$TMPCANON"
52
+ assert_exit "drift guard fails (exit 1) when canonical edited but not recompiled" 1 $RC
53
+ # confirm restore worked
54
+ $NODE "$CI" --check >/dev/null 2>&1
55
+ assert_exit "canonical restored, --check green again" 0 $?
56
+
57
+ # --- R4: both artifacts share an identical body, differ only in footer ---
58
+ CLAUDE_BODY=$(sed -n '/# Qualia Framework/,/state router tells you the next command/p' "$FRAMEWORK_DIR/CLAUDE.md")
59
+ AGENTS_BODY=$(sed -n '/# Qualia Framework/,/state router tells you the next command/p' "$FRAMEWORK_DIR/AGENTS.md")
60
+ if [ "$CLAUDE_BODY" = "$AGENTS_BODY" ]; then
61
+ echo " ✓ CLAUDE.md and AGENTS.md bodies are identical (no drift)"; PASS=$((PASS+1));
62
+ else
63
+ echo " ✗ CLAUDE.md and AGENTS.md bodies diverge"; FAIL=$((FAIL+1));
64
+ fi
65
+ # host-specific footer survives
66
+ assert_contains "CLAUDE.md keeps Claude footer" "$(cat "$FRAMEWORK_DIR/CLAUDE.md")" "this file stays under 25 lines"
67
+ assert_contains "AGENTS.md keeps cross-vendor footer" "$(cat "$FRAMEWORK_DIR/AGENTS.md")" "cross-vendor compatibility"
68
+ refute_contains "AGENTS.md does NOT carry the Claude-only footer" "$(cat "$FRAMEWORK_DIR/AGENTS.md")" "this file stays under 25 lines"
69
+ # both carry the generated header + role placeholder for install to fill
70
+ assert_contains "CLAUDE.md has generated header" "$(head -1 "$FRAMEWORK_DIR/CLAUDE.md")" "GENERATED from templates/instructions.md"
71
+ assert_contains "AGENTS.md keeps {{ROLE}} for install" "$(cat "$FRAMEWORK_DIR/AGENTS.md")" "{{ROLE}}"
72
+
73
+ # --- R5: adapter contract is the single source of per-host facts ---
74
+ assert_eq "claude instructionFile" "CLAUDE.md" "$($NODE -e "console.log(require('$HA').adapter('claude').instructionFile)")"
75
+ assert_eq "codex instructionFile" "AGENTS.md" "$($NODE -e "console.log(require('$HA').adapter('codex').instructionFile)")"
76
+ assert_eq "claude configFile" "settings.json" "$($NODE -e "console.log(require('$HA').adapter('claude').configFile)")"
77
+ assert_eq "codex configFile" "config.toml" "$($NODE -e "console.log(require('$HA').adapter('codex').configFile)")"
78
+ assert_eq "codex agentExt" ".toml" "$($NODE -e "console.log(require('$HA').adapter('codex').agentExt)")"
79
+ assert_eq "unknown host throws" "throws" "$($NODE -e "try{require('$HA').adapter('cursor');console.log('no')}catch(e){console.log('throws')}")"
80
+
81
+ # --- R5: stripHostBlocks keeps only the matching host's block ---
82
+ SRC='before
83
+ <!--QUALIA-HOST claude-->
84
+ CLAUDE_ONLY
85
+ <!--/QUALIA-HOST-->
86
+ <!--QUALIA-HOST codex-->
87
+ CODEX_ONLY
88
+ <!--/QUALIA-HOST-->
89
+ after'
90
+ CLAUDE_OUT=$($NODE -e "console.log(require('$HA').stripHostBlocks(process.argv[1],'claude'))" "$SRC")
91
+ assert_contains "claude strip keeps CLAUDE_ONLY" "$CLAUDE_OUT" "CLAUDE_ONLY"
92
+ refute_contains "claude strip drops CODEX_ONLY" "$CLAUDE_OUT" "CODEX_ONLY"
93
+ CODEX_OUT=$($NODE -e "console.log(require('$HA').stripHostBlocks(process.argv[1],'codex'))" "$SRC")
94
+ assert_contains "codex strip keeps CODEX_ONLY" "$CODEX_OUT" "CODEX_ONLY"
95
+ refute_contains "codex strip drops CLAUDE_ONLY" "$CODEX_OUT" "CLAUDE_ONLY"
96
+
97
+ # --- R5: compileInstructions applies naming but leaves tokens/role for install ---
98
+ NAMED=$($NODE -e "console.log(require('$HA').compileInstructions('Use Claude Code now. Role: {{ROLE}} at \${QUALIA_BIN}','codex'))")
99
+ assert_contains "codex compile swaps the product name" "$NAMED" "Use Codex now"
100
+ assert_contains "codex compile leaves {{ROLE}} for install" "$NAMED" "{{ROLE}}"
101
+ assert_contains "codex compile leaves \${QUALIA_BIN} token for install" "$NAMED" '${QUALIA_BIN}'
102
+
103
+ # --- R5: renderText still resolves paths (regression) ---
104
+ RENDERED=$($NODE -e "console.log(require('$HA').renderText('bin at \${QUALIA_BIN}','claude'))")
105
+ assert_contains "renderText resolves QUALIA_BIN" "$RENDERED" "/.claude/bin"
106
+
107
+ echo ""
108
+ echo "=== Results: $PASS passed, $FAIL failed ==="
109
+ [ "$FAIL" -eq 0 ] && exit 0 || exit 1