qualia-framework 6.22.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.
@@ -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,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 ]
package/tests/lib.test.sh CHANGED
@@ -365,6 +365,31 @@ console.log(c.includes(".codex/bin/state.js") && c.includes(".codex/agents/plann
365
365
  ')
366
366
  [ "$RES" = "HOST-RENDER-OK" ] && ok "host-adapters renders Qualia path tokens per host" || fail "host-adapters render: $RES"
367
367
 
368
+ RES=$($NODE -e '
369
+ const { adapter, hostForHome } = require("'"$HA"'");
370
+ const c = adapter("claude"), x = adapter("codex");
371
+ const ok =
372
+ c.agentCli === "claude" && JSON.stringify(c.agentExec("P")) === JSON.stringify(["-p","P"]) &&
373
+ x.agentCli === "codex" && JSON.stringify(x.agentExec("P")) === JSON.stringify(["exec","P"]) &&
374
+ hostForHome("/x/.codex") === "codex" && hostForHome("/x/.claude") === "claude";
375
+ console.log(ok ? "ADAPTER-EXEC-OK" : JSON.stringify({c:c.agentExec("P"),x:x.agentExec("P")}));
376
+ ')
377
+ [ "$RES" = "ADAPTER-EXEC-OK" ] && ok "host-adapters owns agent-CLI invocation (agentCli/agentExec) + hostForHome" || fail "host-adapters agent-exec: $RES"
378
+
379
+ # ─── R9 design dials: the 3 dials exist in the reference AND the DESIGN template ─
380
+ DIALS="$FRAMEWORK_DIR/qualia-design/design-dials.md"
381
+ DTMPL="$FRAMEWORK_DIR/templates/DESIGN.md"
382
+ RES=$($NODE -e '
383
+ const fs = require("fs");
384
+ const ref = fs.readFileSync("'"$DIALS"'", "utf8");
385
+ const tpl = fs.readFileSync("'"$DTMPL"'", "utf8");
386
+ const dials = ["DESIGN_VARIANCE", "MOTION_INTENSITY", "VISUAL_DENSITY"];
387
+ const refOk = dials.every(d => ref.includes(d)) && /ban\b/i.test(ref) && /do instead/i.test(ref);
388
+ const tplOk = dials.every(d => tpl.includes(d));
389
+ console.log(refOk && tplOk ? "DIALS-OK" : "FAIL ref="+refOk+" tpl="+tplOk);
390
+ ')
391
+ [ "$RES" = "DIALS-OK" ] && ok "design dials + ban→do-instead present in design-dials.md AND DESIGN.md template" || fail "design dials: $RES"
392
+
368
393
  CS="$FRAMEWORK_DIR/bin/command-surface.js"
369
394
  $NODE --check "$CS" >/dev/null 2>&1 && ok "command-surface.js parses" || fail "command-surface.js parse"
370
395
  RES=$($NODE -e '
@@ -507,14 +532,14 @@ TMP=$(mktmp)
507
532
  mkdir -p "$TMP/home/.claude/bin" "$TMP/home/.claude/hooks" "$TMP/home/.claude/knowledge/daily-log" "$TMP/home/.claude/qualia-design" "$TMP/home/.claude/agents" "$TMP/home/.claude/qualia-templates" "$TMP/project"
508
533
  echo '{"installed_by":"Test","role":"OWNER","version":"6.3.0","erp":{"enabled":false}}' > "$TMP/home/.claude/.qualia-config.json"
509
534
  touch "$TMP/home/.claude/CLAUDE.md" "$TMP/home/.claude/settings.json"
510
- for f in runtime-manifest.js command-surface.js host-adapters.js state.js qualia-ui.js statusline.js knowledge.js knowledge-flush.js state-ledger.js plan-contract.js contract-runner.js agent-status.js analyze-gate.js verify-panel.js wave-plan.js eval-runner.js branch-hygiene.js last-report.js harness-eval.js trust-score.js agent-runs.js slop-detect.mjs erp-retry.js work-packet.js report-payload.js project-snapshot.js project-sync.js codex-goal.js planning-hygiene.js prune-deprecated.js learning-candidates.js status-snapshot.js security-scan.js auto-report.js; do
535
+ for f in runtime-manifest.js command-surface.js host-adapters.js state.js qualia-ui.js statusline.js knowledge.js knowledge-flush.js recall.js vault-access.js repo-map.js design-tokens.js batch-plan.js state-ledger.js plan-contract.js contract-runner.js agent-status.js analyze-gate.js verify-panel.js wave-plan.js eval-runner.js branch-hygiene.js last-report.js harness-eval.js trust-score.js agent-runs.js slop-detect.mjs erp-retry.js erp-event.js work-packet.js report-payload.js project-snapshot.js project-sync.js codex-goal.js planning-hygiene.js prune-deprecated.js learning-candidates.js status-snapshot.js security-scan.js auto-report.js; do
511
536
  touch "$TMP/home/.claude/bin/$f"
512
537
  done
513
538
  for h in session-start.js auto-update.js branch-guard.js pre-push.js pre-deploy-gate.js migration-guard.js git-guardrails.js stop-session-log.js fawzi-approval-guard.js vercel-account-guard.js env-empty-guard.js supabase-destructive-guard.js; do
514
539
  touch "$TMP/home/.claude/hooks/$h"
515
540
  done
516
541
  touch "$TMP/home/.claude/knowledge/index.md" "$TMP/home/.claude/knowledge/agents.md" "$TMP/home/.claude/agents/visual-evaluator.md" "$TMP/home/.claude/qualia-guide.md" "$TMP/home/.claude/qualia-templates/help.html"
517
- for f in design-laws.md design-rubric.md design-brand.md design-product.md design-reference.md frontend.md graphics.md; do
542
+ for f in design-laws.md design-rubric.md design-brand.md design-product.md design-reference.md design-dials.md frontend.md graphics.md; do
518
543
  touch "$TMP/home/.claude/qualia-design/$f"
519
544
  done
520
545
  for s in $($NODE -e 'console.log(require(process.argv[1]).ACTIVE_SKILLS.join(" "))' "$CS"); do
@@ -622,14 +647,14 @@ TMP=$(mktmp)
622
647
  mkdir -p "$TMP/.claude/bin" "$TMP/.claude/hooks" "$TMP/.claude/knowledge/daily-log" "$TMP/.claude/qualia-design" "$TMP/.claude/agents" "$TMP/.claude/qualia-templates" "$TMP/project/.planning"
623
648
  echo '{"installed_by":"Test","role":"OWNER","erp":{"enabled":false}}' > "$TMP/.claude/.qualia-config.json"
624
649
  touch "$TMP/.claude/CLAUDE.md" "$TMP/.claude/settings.json"
625
- for f in runtime-manifest.js command-surface.js host-adapters.js state.js qualia-ui.js statusline.js knowledge.js knowledge-flush.js state-ledger.js plan-contract.js contract-runner.js agent-status.js analyze-gate.js verify-panel.js wave-plan.js eval-runner.js branch-hygiene.js last-report.js harness-eval.js trust-score.js agent-runs.js slop-detect.mjs erp-retry.js work-packet.js report-payload.js project-snapshot.js project-sync.js codex-goal.js planning-hygiene.js prune-deprecated.js learning-candidates.js status-snapshot.js security-scan.js auto-report.js; do
650
+ for f in runtime-manifest.js command-surface.js host-adapters.js state.js qualia-ui.js statusline.js knowledge.js knowledge-flush.js recall.js vault-access.js repo-map.js design-tokens.js batch-plan.js state-ledger.js plan-contract.js contract-runner.js agent-status.js analyze-gate.js verify-panel.js wave-plan.js eval-runner.js branch-hygiene.js last-report.js harness-eval.js trust-score.js agent-runs.js slop-detect.mjs erp-retry.js erp-event.js work-packet.js report-payload.js project-snapshot.js project-sync.js codex-goal.js planning-hygiene.js prune-deprecated.js learning-candidates.js status-snapshot.js security-scan.js auto-report.js; do
626
651
  touch "$TMP/.claude/bin/$f"
627
652
  done
628
653
  for h in session-start.js auto-update.js branch-guard.js pre-push.js pre-deploy-gate.js migration-guard.js git-guardrails.js stop-session-log.js fawzi-approval-guard.js vercel-account-guard.js env-empty-guard.js supabase-destructive-guard.js; do
629
654
  touch "$TMP/.claude/hooks/$h"
630
655
  done
631
656
  touch "$TMP/.claude/knowledge/index.md" "$TMP/.claude/knowledge/agents.md"
632
- for f in design-laws.md design-rubric.md design-brand.md design-product.md design-reference.md frontend.md graphics.md; do
657
+ for f in design-laws.md design-rubric.md design-brand.md design-product.md design-reference.md design-dials.md frontend.md graphics.md; do
633
658
  touch "$TMP/.claude/qualia-design/$f"
634
659
  done
635
660
  for s in $($NODE -e 'console.log(require(process.argv[1]).ACTIVE_SKILLS.join(" "))' "$CS"); do
@@ -0,0 +1,91 @@
1
+ #!/bin/bash
2
+ # recall.test.sh — bin/recall.js (read-side memory recall, role-filtered vault)
3
+ # Run: bash tests/recall.test.sh
4
+
5
+ PASS=0
6
+ FAIL=0
7
+ BIN_DIR="$(cd "$(dirname "$0")/../bin" && pwd)"
8
+ NODE="${NODE:-node}"
9
+ RECALL="$BIN_DIR/recall.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 (LEAK: found '$3')"; FAIL=$((FAIL+1)); else echo " ✓ $1"; PASS=$((PASS+1)); fi; }
14
+
15
+ echo "recall.test.sh — bin/recall.js"
16
+ echo ""
17
+
18
+ $NODE -c "$RECALL" 2>/dev/null && { echo " ✓ syntax valid"; PASS=$((PASS+1)); } || { echo " ✗ syntax invalid"; FAIL=$((FAIL+1)); }
19
+
20
+ # ── Fixture: a temp knowledge home + a temp vault with an access manifest ──
21
+ HOME_DIR=$(mktemp -d)
22
+ VAULT=$(mktemp -d)
23
+ mkdir -p "$HOME_DIR/knowledge" "$VAULT/wiki/concepts" "$VAULT/wiki/_meta"
24
+
25
+ # knowledge layer hit
26
+ cat > "$HOME_DIR/knowledge/learned-patterns.md" <<'EOF'
27
+ # Patterns
28
+ - The zebrafish pattern: batch writes before fanout.
29
+ EOF
30
+ # vault ALL_ROLES hit
31
+ cat > "$VAULT/wiki/concepts/notes.md" <<'EOF'
32
+ A lesson about the zebrafish approach to retries.
33
+ EOF
34
+ # vault OWNER_ONLY hit (the access manifest itself names wiki/_meta/access.md)
35
+ cat > "$VAULT/wiki/_meta/access.md" <<'EOF'
36
+ # Vault Access Manifest
37
+ ## OWNER_ONLY
38
+ | Path | Why |
39
+ |---|---|
40
+ | `Clients/*.md` | commercial |
41
+ | `wiki/_meta/access.md` | this file (zebrafish marker) |
42
+ ## ALL_ROLES
43
+ | `wiki/concepts/` | lessons |
44
+ EOF
45
+
46
+ export QUALIA_MEMORY_ROOT="$VAULT"
47
+ RUN() { QUALIA_HOME="$HOME_DIR" "$@"; }
48
+
49
+ # ── Invocation guards ──
50
+ RUN $NODE "$RECALL" >/dev/null 2>&1; assert_exit "no query → exit 2" 2 $?
51
+ RUN $NODE "$RECALL" zebrafish --scope bogus >/dev/null 2>&1; assert_exit "bad scope → exit 2" 2 $?
52
+
53
+ # ── Hits across both stores ──
54
+ OUT=$(QUALIA_ROLE=OWNER RUN $NODE "$RECALL" zebrafish 2>&1); assert_exit "valid query → exit 0" 0 $?
55
+ assert_contains "knowledge-layer hit" "$OUT" "learned-patterns.md"
56
+ assert_contains "vault ALL_ROLES hit" "$OUT" "concepts/notes.md"
57
+
58
+ # ── Role enforcement (the security boundary) ──
59
+ OWNER_OUT=$(QUALIA_ROLE=OWNER RUN $NODE "$RECALL" zebrafish --scope vault 2>&1)
60
+ assert_contains "OWNER sees OWNER_ONLY path" "$OWNER_OUT" "_meta/access.md"
61
+ EMP_OUT=$(QUALIA_ROLE=EMPLOYEE RUN $NODE "$RECALL" zebrafish --scope vault 2>&1)
62
+ assert_contains "EMPLOYEE still sees ALL_ROLES" "$EMP_OUT" "concepts/notes.md"
63
+ assert_absent "EMPLOYEE blocked from OWNER_ONLY" "$EMP_OUT" "_meta/access.md"
64
+ # fail-closed: no role config + no QUALIA_ROLE → RESTRICTED
65
+ NOROLE_OUT=$(QUALIA_HOME="$VAULT" env -u QUALIA_ROLE $NODE "$RECALL" zebrafish --scope vault 2>&1)
66
+ assert_absent "RESTRICTED (fail-closed) blocked from OWNER_ONLY" "$NOROLE_OUT" "_meta/access.md"
67
+
68
+ # ── JSON shape ──
69
+ JSON=$(QUALIA_ROLE=OWNER RUN $NODE "$RECALL" zebrafish --json 2>&1)
70
+ echo "$JSON" | $NODE -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const j=JSON.parse(s);if(j.role&&j.scope==='all'&&typeof j.total==='number'&&Array.isArray(j.knowledge)&&Array.isArray(j.vault))process.exit(0);process.exit(1)})" \
71
+ && { echo " ✓ --json valid shape (role/scope/total/knowledge/vault)"; PASS=$((PASS+1)); } \
72
+ || { echo " ✗ --json shape"; FAIL=$((FAIL+1)); }
73
+
74
+ # ── Scope isolation ──
75
+ KONLY=$(QUALIA_ROLE=OWNER RUN $NODE "$RECALL" zebrafish --scope knowledge 2>&1)
76
+ assert_contains "scope=knowledge includes knowledge" "$KONLY" "learned-patterns.md"
77
+ assert_absent "scope=knowledge excludes vault" "$KONLY" "concepts/notes.md"
78
+ VONLY=$(QUALIA_ROLE=OWNER RUN $NODE "$RECALL" zebrafish --scope vault 2>&1)
79
+ assert_absent "scope=vault excludes knowledge" "$VONLY" "learned-patterns.md"
80
+
81
+ # ── Graceful degradation ──
82
+ NOVAULT=$(QUALIA_HOME="$HOME_DIR" QUALIA_MEMORY_ROOT="/tmp/qualia-no-such-vault-$$" QUALIA_ROLE=OWNER $NODE "$RECALL" zebrafish 2>&1); RC=$?
83
+ assert_exit "missing vault → still exit 0" 0 $RC
84
+ assert_contains "missing vault → knowledge still searched" "$NOVAULT" "learned-patterns.md"
85
+ ZERO=$(QUALIA_ROLE=OWNER RUN $NODE "$RECALL" "noyzzqxmatch" 2>&1); assert_exit "zero hits → exit 0" 0 $?
86
+ assert_contains "zero hits → friendly message" "$ZERO" "no matches"
87
+
88
+ rm -rf "$HOME_DIR" "$VAULT"
89
+ echo ""
90
+ echo "=== Results: $PASS passed, $FAIL failed ==="
91
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,70 @@
1
+ #!/bin/bash
2
+ # repo-map.test.sh — bin/repo-map.js (zero-dep symbol map for /qualia-map)
3
+ # Run: bash tests/repo-map.test.sh
4
+
5
+ PASS=0
6
+ FAIL=0
7
+ BIN_DIR="$(cd "$(dirname "$0")/../bin" && pwd)"
8
+ NODE="${NODE:-node}"
9
+ RM="$BIN_DIR/repo-map.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 "repo-map.test.sh — bin/repo-map.js"
16
+ echo ""
17
+
18
+ $NODE -c "$RM" 2>/dev/null && { echo " ✓ syntax valid"; PASS=$((PASS+1)); } || { echo " ✗ syntax invalid"; FAIL=$((FAIL+1)); }
19
+
20
+ # ── Fixture repo ──
21
+ REPO=$(mktemp -d)
22
+ mkdir -p "$REPO/src" "$REPO/node_modules/pkg" "$REPO/api"
23
+ cat > "$REPO/src/widget.ts" <<'EOF'
24
+ export function makeWidget() {}
25
+ export class WidgetStore {}
26
+ export const WIDGET_LIMIT = 5;
27
+ export type WidgetId = string;
28
+ function internalHelper() {}
29
+ EOF
30
+ cat > "$REPO/api/handler.py" <<'EOF'
31
+ class Handler:
32
+ pass
33
+ def serve():
34
+ pass
35
+ async def serve_async():
36
+ pass
37
+ EOF
38
+ cat > "$REPO/node_modules/pkg/index.js" <<'EOF'
39
+ export function shouldBeIgnored() {}
40
+ EOF
41
+
42
+ # ── Symbol extraction ──
43
+ OUT=$($NODE "$RM" "$REPO" 2>&1); assert_exit "valid dir → exit 0" 0 $?
44
+ assert_contains "extracts JS export function" "$OUT" "makeWidget"
45
+ assert_contains "extracts JS export class" "$OUT" "WidgetStore"
46
+ assert_contains "extracts JS export const" "$OUT" "WIDGET_LIMIT"
47
+ assert_contains "extracts JS export type" "$OUT" "WidgetId"
48
+ assert_contains "extracts Python def" "$OUT" "serve"
49
+ assert_contains "extracts Python class" "$OUT" "Handler"
50
+ assert_absent "ignores node_modules" "$OUT" "shouldBeIgnored"
51
+
52
+ # ── JSON shape ──
53
+ JSON=$($NODE "$RM" "$REPO" --json 2>&1)
54
+ echo "$JSON" | $NODE -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const j=JSON.parse(s);if(typeof j.total_files==='number'&&typeof j.total_symbols==='number'&&Array.isArray(j.files)&&j.files[0].symbols)process.exit(0);process.exit(1)})" \
55
+ && { echo " ✓ --json valid shape (total_files/total_symbols/files[].symbols)"; PASS=$((PASS+1)); } \
56
+ || { echo " ✗ --json shape"; FAIL=$((FAIL+1)); }
57
+ # densest file first (widget.ts has 5 symbols, handler.py has 3)
58
+ FIRST=$(echo "$JSON" | $NODE -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{console.log(JSON.parse(s).files[0].file)})")
59
+ [ "$FIRST" = "src/widget.ts" ] && { echo " ✓ ranks densest file first"; PASS=$((PASS+1)); } || { echo " ✗ ranking (got '$FIRST')"; FAIL=$((FAIL+1)); }
60
+
61
+ # ── Edge cases ──
62
+ $NODE "$RM" /no/such/dir-xyz >/dev/null 2>&1; assert_exit "missing dir → exit 2" 2 $?
63
+ EMPTY=$(mktemp -d)
64
+ EOUT=$($NODE "$RM" "$EMPTY" 2>&1); assert_exit "empty dir → exit 0" 0 $?
65
+ assert_contains "empty dir → 0 source files" "$EOUT" "0 source files"
66
+
67
+ rm -rf "$REPO" "$EMPTY"
68
+ echo ""
69
+ echo "=== Results: $PASS passed, $FAIL failed ==="
70
+ [ "$FAIL" -eq 0 ]
package/tests/run-all.sh CHANGED
@@ -27,6 +27,11 @@ SUITES=(
27
27
  "branch-hygiene"
28
28
  "last-report"
29
29
  "project-sync"
30
+ "erp-event"
31
+ "recall"
32
+ "repo-map"
33
+ "design-tokens"
34
+ "batch-plan"
30
35
  )
31
36
 
32
37
  FAILED=()