qualia-framework 4.4.0 → 5.1.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 (70) hide show
  1. package/AGENTS.md +24 -0
  2. package/CLAUDE.md +12 -63
  3. package/README.md +24 -18
  4. package/agents/builder.md +13 -33
  5. package/agents/plan-checker.md +18 -0
  6. package/agents/planner.md +17 -0
  7. package/agents/verifier.md +70 -0
  8. package/agents/visual-evaluator.md +132 -0
  9. package/bin/cli.js +64 -23
  10. package/bin/install.js +375 -29
  11. package/bin/qualia-ui.js +208 -1
  12. package/bin/slop-detect.mjs +362 -0
  13. package/bin/state.js +218 -2
  14. package/docs/erp-contract.md +5 -0
  15. package/docs/install-redesign-builder-prompt.md +290 -0
  16. package/docs/install-redesign-pilot.md +234 -0
  17. package/docs/playwright-loop-builder-prompt.md +185 -0
  18. package/docs/playwright-loop-design-notes.md +108 -0
  19. package/docs/playwright-loop-pilot-results.md +170 -0
  20. package/docs/playwright-loop-review-2026-05-03.md +65 -0
  21. package/docs/playwright-loop-tester-prompt.md +213 -0
  22. package/docs/reviews/matt-pocock-skills-analysis.md +300 -0
  23. package/guide.md +9 -5
  24. package/hooks/env-empty-guard.js +74 -0
  25. package/hooks/pre-compact.js +19 -9
  26. package/hooks/pre-deploy-gate.js +8 -2
  27. package/hooks/pre-push.js +26 -12
  28. package/hooks/supabase-destructive-guard.js +62 -0
  29. package/hooks/vercel-account-guard.js +91 -0
  30. package/package.json +2 -1
  31. package/rules/design-brand.md +114 -0
  32. package/rules/design-laws.md +148 -0
  33. package/rules/design-product.md +114 -0
  34. package/rules/design-rubric.md +157 -0
  35. package/rules/grounding.md +4 -0
  36. package/skills/qualia-build/SKILL.md +40 -46
  37. package/skills/qualia-discuss/SKILL.md +51 -68
  38. package/skills/qualia-handoff/SKILL.md +1 -0
  39. package/skills/qualia-issues/SKILL.md +151 -0
  40. package/skills/qualia-map/SKILL.md +78 -35
  41. package/skills/qualia-new/REFERENCE.md +139 -0
  42. package/skills/qualia-new/SKILL.md +85 -124
  43. package/skills/qualia-optimize/REFERENCE.md +202 -0
  44. package/skills/qualia-optimize/SKILL.md +72 -237
  45. package/skills/qualia-plan/SKILL.md +58 -65
  46. package/skills/qualia-polish/SKILL.md +180 -136
  47. package/skills/qualia-polish-loop/REFERENCE.md +265 -0
  48. package/skills/qualia-polish-loop/SKILL.md +201 -0
  49. package/skills/qualia-polish-loop/fixtures/broken.html +117 -0
  50. package/skills/qualia-polish-loop/fixtures/clean.html +196 -0
  51. package/skills/qualia-polish-loop/scripts/loop.mjs +302 -0
  52. package/skills/qualia-polish-loop/scripts/playwright-capture.mjs +197 -0
  53. package/skills/qualia-polish-loop/scripts/score.mjs +176 -0
  54. package/skills/qualia-report/SKILL.md +141 -180
  55. package/skills/qualia-research/SKILL.md +28 -33
  56. package/skills/qualia-road/SKILL.md +103 -0
  57. package/skills/qualia-ship/SKILL.md +1 -0
  58. package/skills/qualia-task/SKILL.md +1 -1
  59. package/skills/qualia-test/SKILL.md +50 -2
  60. package/skills/qualia-triage/SKILL.md +152 -0
  61. package/skills/qualia-verify/SKILL.md +63 -104
  62. package/skills/qualia-zoom/SKILL.md +51 -0
  63. package/skills/zoho-workflow/SKILL.md +64 -0
  64. package/templates/CONTEXT.md +36 -0
  65. package/templates/DESIGN.md +229 -435
  66. package/templates/PRODUCT.md +95 -0
  67. package/templates/decisions/ADR-template.md +30 -0
  68. package/tests/bin.test.sh +451 -7
  69. package/tests/state.test.sh +58 -0
  70. package/skills/qualia-design/SKILL.md +0 -169
@@ -0,0 +1,95 @@
1
+ # PRODUCT — {Project Name}
2
+
3
+ > **Required.** The "who and why" every Qualia agent reads before designing or building anything.
4
+ > `/qualia-new` generates this from 5 questions. Update as the product clarifies.
5
+
6
+ ## Register
7
+
8
+ **One of:** `brand` · `product`
9
+
10
+ - **brand** — design IS the product (marketing, landing, campaign, portfolio). Distinctiveness is the bar.
11
+ - **product** — design SERVES the product (app, admin, dashboard, tool). Earned familiarity is the bar.
12
+
13
+ A project may have BOTH (e.g., marketing site + app). When that's true, list both, and note which surfaces fall under each.
14
+
15
+ ```
16
+ register: product
17
+ secondary: brand (only for /marketing/* routes)
18
+ ```
19
+
20
+ ## Users (specific, not personas)
21
+
22
+ Real humans with real jobs who will use this. Not "small business owners" — Sarah, who runs a 3-person bookkeeping practice in Limassol and pays for QuickBooks because she trusts it. Three users, named, with the workflow in mind.
23
+
24
+ ```
25
+ 1. {Name} — {role + scenario}. {What they're trying to do when they open this}.
26
+ 2. ...
27
+ 3. ...
28
+ ```
29
+
30
+ ## Brand voice
31
+
32
+ How the product sounds in writing. UI strings, error messages, marketing copy. Three adjectives, then one sentence of "voice in motion."
33
+
34
+ ```
35
+ Voice: {three adjectives — e.g., "direct, confident, dry"}
36
+
37
+ Voice in motion: {one paragraph showing the voice in actual sentences. What an
38
+ error message sounds like. What a confirmation sounds like. What the empty state
39
+ sounds like. Concrete, not adjectives.}
40
+ ```
41
+
42
+ ## Anti-references (mandatory, 3-5)
43
+
44
+ Sites the project should NOT look like. Anti-references pin down what the design is reacting against — more useful than positive references.
45
+
46
+ ```
47
+ - {URL or descriptor} — {what it gets wrong for our use case}
48
+ - {URL or descriptor} — ...
49
+ - {URL or descriptor} — ...
50
+ ```
51
+
52
+ For Brand register, these are usually saturated aesthetic lanes (see `rules/design-brand.md`).
53
+ For Product register, these are usually patterns we don't want to inherit (e.g., "Salesforce Lightning — too dense, too many panels").
54
+
55
+ ## Positive references (optional, ≤3)
56
+
57
+ Sites that demonstrate qualities we want to inherit. Less important than anti-references.
58
+
59
+ ```
60
+ - {URL or app name} — {specific quality, e.g., "Linear's keyboard fluency", "Stripe's empty state copy", "Vercel's color restraint"}
61
+ ```
62
+
63
+ ## Strategic principles
64
+
65
+ 3-5 statements that resolve future arguments. Not feature ideas — design/UX principles that compound.
66
+
67
+ ```
68
+ - {Principle}. {One-line rationale.}
69
+ - {Principle}. {One-line rationale.}
70
+ - {Principle}. {One-line rationale.}
71
+ ```
72
+
73
+ Examples:
74
+ - **Density before delight.** Users live in this app 6+ hours/day. Density wins over flourish.
75
+ - **Power users are the primary persona.** Beginners get onboarding. The app optimizes for power.
76
+ - **Speed of glance, not depth of click.** Information visible without interaction beats information one click away.
77
+
78
+ ## Constraints
79
+
80
+ ```
81
+ Framework: {Next.js 16 / React 19 / Vite / Astro / etc}
82
+ Component library: {shadcn-ui · custom · radix · arco · ...}
83
+ Performance budget: {LCP < 2.5s, INP < 200ms, CLS < 0.1, etc}
84
+ Accessibility: WCAG 2.2 AA minimum
85
+ Browsers: {evergreen / IE11 / mobile-first / ...}
86
+ i18n: {languages supported, RTL needed?}
87
+ ```
88
+
89
+ ## Differentiation (one sentence)
90
+
91
+ What someone will remember about this product 24 hours after using it for the first time. If you can't write this in one sentence, the product hasn't decided what it is yet.
92
+
93
+ ```
94
+ {One sentence. What's the memory hook?}
95
+ ```
@@ -0,0 +1,30 @@
1
+ # ADR-{{NNNN}} — {{Decision title in present tense}}
2
+
3
+ - **Date:** {{YYYY-MM-DD}}
4
+ - **Phase:** {{N}} — {{phase name}}
5
+ - **Status:** Proposed | Accepted | Superseded by ADR-XXXX
6
+ - **Domain terms:** {{any new/affected terms — also update CONTEXT.md}}
7
+
8
+ ## Context
9
+ What is the situation that requires a decision? Why are we deciding this now? What constraints apply (regulatory, performance, team skill, deadlines)?
10
+
11
+ ## Decision
12
+ What we are doing. State it as a single declarative sentence first, then expand.
13
+
14
+ ## Consequences
15
+ - **What becomes easier:** ...
16
+ - **What becomes harder:** ...
17
+ - **What is now load-bearing on this decision:** ...
18
+
19
+ ## Alternatives considered
20
+ Why each alternative was rejected — keep this honest. Future-you will read it.
21
+
22
+ - **Alt A** — {{rejected because…}}
23
+ - **Alt B** — {{rejected because…}}
24
+
25
+ ## Notes
26
+ Any non-obvious context, links to discussion threads, related issues. Optional.
27
+
28
+ ---
29
+
30
+ **When to write an ADR (per Matt Pocock's rule):** only when the decision is hard-to-reverse, surprising-without-context, AND involves real trade-offs. If any of those three is missing, just put it in CONTEXT.md or a phase plan instead. ADRs are scarce on purpose.
package/tests/bin.test.sh CHANGED
@@ -9,6 +9,7 @@ NODE="${NODE:-node}"
9
9
  CLI_JS="$FRAMEWORK_DIR/bin/cli.js"
10
10
  UI_JS="$FRAMEWORK_DIR/bin/qualia-ui.js"
11
11
  INSTALL_JS="$FRAMEWORK_DIR/bin/install.js"
12
+ STATE_JS="$FRAMEWORK_DIR/bin/state.js"
12
13
  PKG_VERSION=$($NODE -e 'console.log(require("'"$FRAMEWORK_DIR"'/package.json").version)')
13
14
 
14
15
  # Track tmp dirs we create so we can clean them up on exit
@@ -476,11 +477,12 @@ else
476
477
  fail_case "CLAUDE.md role substitution"
477
478
  fi
478
479
 
479
- # 31. All 9 hooks installed (block-env-edit removed in v3.2.0;
480
- # git-guardrails + stop-session-log added in v4.2.0)
480
+ # 31. All 12 hooks installed (block-env-edit removed in v3.2.0;
481
+ # git-guardrails + stop-session-log added in v4.2.0;
482
+ # vercel-account-guard + env-empty-guard + supabase-destructive-guard added in v5.0.0)
481
483
  HOOK_COUNT=$(ls "$TMP/.claude/hooks/"*.js 2>/dev/null | wc -l)
482
- if [ "$HOOK_COUNT" -eq 9 ]; then
483
- pass "9 hooks installed in hooks/"
484
+ if [ "$HOOK_COUNT" -eq 12 ]; then
485
+ pass "12 hooks installed in hooks/"
484
486
  else
485
487
  fail_case "hook count" "got $HOOK_COUNT"
486
488
  fi
@@ -496,7 +498,7 @@ else
496
498
  fail_case "settings.json contents"
497
499
  fi
498
500
 
499
- # 33. settings.json contains all 9 hooks wired correctly
501
+ # 33. settings.json contains all 12 hooks wired correctly
500
502
  if grep -q 'branch-guard.js' "$TMP/.claude/settings.json" \
501
503
  && grep -q 'migration-guard.js' "$TMP/.claude/settings.json" \
502
504
  && grep -q 'pre-push.js' "$TMP/.claude/settings.json" \
@@ -505,8 +507,11 @@ if grep -q 'branch-guard.js' "$TMP/.claude/settings.json" \
505
507
  && grep -q 'session-start.js' "$TMP/.claude/settings.json" \
506
508
  && grep -q 'pre-compact.js' "$TMP/.claude/settings.json" \
507
509
  && grep -q 'git-guardrails.js' "$TMP/.claude/settings.json" \
508
- && grep -q 'stop-session-log.js' "$TMP/.claude/settings.json"; then
509
- pass "settings.json has all 9 hooks wired"
510
+ && grep -q 'stop-session-log.js' "$TMP/.claude/settings.json" \
511
+ && grep -q 'vercel-account-guard.js' "$TMP/.claude/settings.json" \
512
+ && grep -q 'env-empty-guard.js' "$TMP/.claude/settings.json" \
513
+ && grep -q 'supabase-destructive-guard.js' "$TMP/.claude/settings.json"; then
514
+ pass "settings.json has all 12 hooks wired"
510
515
  else
511
516
  fail_case "settings.json missing hooks"
512
517
  fi
@@ -992,6 +997,445 @@ else
992
997
  fail_case "help missing flush" "exit=$EXIT"
993
998
  fi
994
999
 
1000
+ echo ""
1001
+ echo "--- v5.0.0 (alignment substrate + new skills) ---"
1002
+
1003
+ # 79. CONTEXT.md template installs to qualia-templates/
1004
+ TMP=$(mktmp)
1005
+ echo "QS-FAWZI-01" | HOME="$TMP" $NODE "$INSTALL_JS" >/dev/null 2>&1
1006
+ if [ -f "$TMP/.claude/qualia-templates/CONTEXT.md" ]; then
1007
+ pass "CONTEXT.md template installs to qualia-templates/"
1008
+ else
1009
+ fail_case "CONTEXT.md template missing"
1010
+ fi
1011
+
1012
+ # 80. decisions/ADR-template.md installs (validates nested template dir copy)
1013
+ if [ -f "$TMP/.claude/qualia-templates/decisions/ADR-template.md" ]; then
1014
+ pass "decisions/ADR-template.md installs (nested template dir)"
1015
+ else
1016
+ fail_case "decisions/ADR-template.md missing"
1017
+ fi
1018
+
1019
+ # 81-84. The 4 new v5 skills install with their SKILL.md
1020
+ for SKILL in qualia-zoom qualia-road qualia-issues qualia-triage; do
1021
+ if [ -f "$TMP/.claude/skills/$SKILL/SKILL.md" ]; then
1022
+ pass "$SKILL skill installs"
1023
+ else
1024
+ fail_case "$SKILL skill missing after install"
1025
+ fi
1026
+ done
1027
+
1028
+ # 85. qualia-discuss SKILL.md mentions "grilling" (validates v5 fold of qualia-grill into discuss)
1029
+ if grep -qi "grilling" "$TMP/.claude/skills/qualia-discuss/SKILL.md"; then
1030
+ pass "qualia-discuss documents grilling pattern (folded from qualia-grill)"
1031
+ else
1032
+ fail_case "qualia-discuss missing grilling reference"
1033
+ fi
1034
+
1035
+ # 86. qualia-discuss SKILL.md references CONTEXT.md (validates v5 substrate wiring)
1036
+ if grep -q "CONTEXT.md" "$TMP/.claude/skills/qualia-discuss/SKILL.md"; then
1037
+ pass "qualia-discuss references CONTEXT.md"
1038
+ else
1039
+ fail_case "qualia-discuss missing CONTEXT.md reference"
1040
+ fi
1041
+
1042
+ # 87. qualia-road has disable-model-invocation: true (pure reference skill, user-only)
1043
+ if grep -q "disable-model-invocation: true" "$TMP/.claude/skills/qualia-road/SKILL.md"; then
1044
+ pass "qualia-road has disable-model-invocation: true"
1045
+ else
1046
+ fail_case "qualia-road missing disable-model-invocation"
1047
+ fi
1048
+
1049
+ # 88. qualia-road has NO dead skill refs (qualia-grill, qualia-deepen, qualia-tdd, qualia-onboard
1050
+ # were folded; if they appear as bare /qualia-* commands the road documentation is broken)
1051
+ if grep -qE '/qualia-(grill|deepen|tdd|onboard)\b' "$TMP/.claude/skills/qualia-road/SKILL.md"; then
1052
+ fail_case "qualia-road still references folded skills as commands"
1053
+ else
1054
+ pass "qualia-road has no dead-skill references"
1055
+ fi
1056
+
1057
+ # 89. CONTEXT.md template has NO /qualia-grill reference (was folded into /qualia-discuss)
1058
+ if grep -q '/qualia-grill\b' "$TMP/.claude/qualia-templates/CONTEXT.md"; then
1059
+ fail_case "CONTEXT.md template still references /qualia-grill"
1060
+ else
1061
+ pass "CONTEXT.md template has no /qualia-grill reference"
1062
+ fi
1063
+
1064
+ # 90. Builder agent has trust-boundary block (defends against CONTEXT.md prompt injection)
1065
+ if grep -q "Trust boundary" "$TMP/.claude/agents/builder.md"; then
1066
+ pass "builder.md has Trust boundary block (prompt-injection defense)"
1067
+ else
1068
+ fail_case "builder.md missing Trust boundary block"
1069
+ fi
1070
+
1071
+ # 91. qualia-issues uses --body-file (not vulnerable heredoc) for gh issue create
1072
+ if grep -q -- "--body-file" "$TMP/.claude/skills/qualia-issues/SKILL.md" \
1073
+ && ! grep -q "gh issue create.*--body \"\$(cat <<" "$TMP/.claude/skills/qualia-issues/SKILL.md"; then
1074
+ pass "qualia-issues uses --body-file (no heredoc shell injection)"
1075
+ else
1076
+ fail_case "qualia-issues still uses heredoc for gh issue body"
1077
+ fi
1078
+
1079
+ # 92-94. v5.0 insights-driven hooks parse as valid Node and install
1080
+ for HOOK in vercel-account-guard env-empty-guard supabase-destructive-guard; do
1081
+ HOOK_FILE="$TMP/.claude/hooks/${HOOK}.js"
1082
+ if [ -f "$HOOK_FILE" ]; then
1083
+ EXIT=0; $NODE -c "$HOOK_FILE" 2>/dev/null || EXIT=$?
1084
+ if [ "$EXIT" -eq 0 ]; then
1085
+ pass "${HOOK}.js installs and parses as valid Node"
1086
+ else
1087
+ fail_case "${HOOK}.js parse error"
1088
+ fi
1089
+ else
1090
+ fail_case "${HOOK}.js missing after install"
1091
+ fi
1092
+ done
1093
+
1094
+ # 95. cmdInit guards against suspicious project names (insights friction: throwaway projects)
1095
+ TMP_PROJ=$(mktemp -d)
1096
+ EXIT=0; OUT=$(cd "$TMP_PROJ" && $NODE "$STATE_JS" init --project "test" --phases '[{"name":"X","goal":"y"}]' 2>&1) || EXIT=$?
1097
+ if [ "$EXIT" -ne 0 ] && echo "$OUT" | grep -q "SUSPICIOUS_NAME"; then
1098
+ pass "state.js init blocks suspicious project name 'test' without --force"
1099
+ else
1100
+ fail_case "state.js init allowed 'test' as project name"
1101
+ fi
1102
+ rm -rf "$TMP_PROJ"
1103
+
1104
+ # 96. cmdInit allows real client name
1105
+ TMP_PROJ=$(mktemp -d)
1106
+ EXIT=0; OUT=$(cd "$TMP_PROJ" && $NODE "$STATE_JS" init --project "RealClient" --phases '[{"name":"X","goal":"y"}]' 2>&1) || EXIT=$?
1107
+ if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q '"action": "init"'; then
1108
+ pass "state.js init allows real project name 'RealClient'"
1109
+ else
1110
+ fail_case "state.js init rejected real name" "exit=$EXIT"
1111
+ fi
1112
+ rm -rf "$TMP_PROJ"
1113
+
1114
+ # 97. cmdInit allows suspicious name with --force (escape hatch)
1115
+ TMP_PROJ=$(mktemp -d)
1116
+ EXIT=0; OUT=$(cd "$TMP_PROJ" && $NODE "$STATE_JS" init --project "test" --phases '[{"name":"X","goal":"y"}]' --force 2>&1) || EXIT=$?
1117
+ if [ "$EXIT" -eq 0 ]; then
1118
+ pass "state.js init --force bypasses suspicious-name guard"
1119
+ else
1120
+ fail_case "state.js init --force was blocked" "exit=$EXIT"
1121
+ fi
1122
+ rm -rf "$TMP_PROJ"
1123
+
1124
+ # 98. pre-deploy-gate honors QUALIA_SKIP_LINT escape (insights friction: lint blocks ship)
1125
+ if grep -q "QUALIA_SKIP_LINT" "$TMP/.claude/hooks/pre-deploy-gate.js"; then
1126
+ pass "pre-deploy-gate.js honors QUALIA_SKIP_LINT escape"
1127
+ else
1128
+ fail_case "pre-deploy-gate missing QUALIA_SKIP_LINT escape"
1129
+ fi
1130
+
1131
+ echo ""
1132
+ echo "--- v5.0.0 (visual-polish loop addendum) ---"
1133
+
1134
+ # 99. qualia-polish-loop SKILL.md installs
1135
+ TMP=$(mktmp)
1136
+ echo "QS-FAWZI-01" | HOME="$TMP" $NODE "$INSTALL_JS" >/dev/null 2>&1
1137
+ if [ -f "$TMP/.claude/skills/qualia-polish-loop/SKILL.md" ]; then
1138
+ pass "qualia-polish-loop SKILL.md installs"
1139
+ else
1140
+ fail_case "qualia-polish-loop SKILL.md missing after install"
1141
+ fi
1142
+
1143
+ # 100. qualia-polish-loop REFERENCE.md installs
1144
+ if [ -f "$TMP/.claude/skills/qualia-polish-loop/REFERENCE.md" ]; then
1145
+ pass "qualia-polish-loop REFERENCE.md installs"
1146
+ else
1147
+ fail_case "qualia-polish-loop REFERENCE.md missing after install"
1148
+ fi
1149
+
1150
+ # 101. qualia-polish-loop SKILL.md references the design rubric
1151
+ if grep -q "design-rubric.md" "$TMP/.claude/skills/qualia-polish-loop/SKILL.md"; then
1152
+ pass "qualia-polish-loop references design-rubric.md"
1153
+ else
1154
+ fail_case "qualia-polish-loop missing rubric reference"
1155
+ fi
1156
+
1157
+ # 102. qualia-polish-loop REFERENCE.md has the anchored vision eval prompt
1158
+ if grep -q "DEFAULT TO 3" "$TMP/.claude/skills/qualia-polish-loop/REFERENCE.md"; then
1159
+ pass "qualia-polish-loop REFERENCE.md has anchored rubric prompt"
1160
+ else
1161
+ fail_case "qualia-polish-loop REFERENCE.md missing anchored rubric"
1162
+ fi
1163
+
1164
+ # 103. qualia-polish-loop SKILL.md has regression detection (LOOP_REGRESSION_DETECTED)
1165
+ if grep -q "LOOP_REGRESSION_DETECTED" "$TMP/.claude/skills/qualia-polish-loop/SKILL.md"; then
1166
+ pass "qualia-polish-loop has regression kill-switch"
1167
+ else
1168
+ fail_case "qualia-polish-loop missing regression kill-switch"
1169
+ fi
1170
+
1171
+ # 104. score.mjs exists in the framework skill scripts
1172
+ if [ -f "$FRAMEWORK_DIR/skills/qualia-polish-loop/scripts/score.mjs" ]; then
1173
+ pass "score.mjs exists in skill scripts"
1174
+ else
1175
+ fail_case "score.mjs missing from skill scripts"
1176
+ fi
1177
+
1178
+ # 105. score.mjs computes pass correctly (all dims >= 3)
1179
+ SCORE_OUT=$(echo '{"typography":3,"color":3,"spatial":3,"layout":3,"shadow":3,"motion":3,"microcopy":3,"container":3}' | $NODE "$FRAMEWORK_DIR/skills/qualia-polish-loop/scripts/score.mjs" 2>&1)
1180
+ EXIT=$?
1181
+ if [ "$EXIT" -eq 0 ] && echo "$SCORE_OUT" | grep -q '"pass": true'; then
1182
+ pass "score.mjs pass on all-3 scores"
1183
+ else
1184
+ fail_case "score.mjs pass computation" "exit=$EXIT"
1185
+ fi
1186
+
1187
+ # 106. score.mjs computes fail correctly (one dim < 3)
1188
+ SCORE_OUT=$(echo '{"typography":2,"color":3,"spatial":3,"layout":3,"shadow":3,"motion":3,"microcopy":3,"container":3}' | $NODE "$FRAMEWORK_DIR/skills/qualia-polish-loop/scripts/score.mjs" 2>&1)
1189
+ EXIT=$?
1190
+ if [ "$EXIT" -eq 1 ] && echo "$SCORE_OUT" | grep -q '"pass": false'; then
1191
+ pass "score.mjs fail on dim < 3"
1192
+ else
1193
+ fail_case "score.mjs fail computation" "exit=$EXIT"
1194
+ fi
1195
+
1196
+ # 107. qualia-road references qualia-polish-loop
1197
+ if grep -q "qualia-polish-loop" "$TMP/.claude/skills/qualia-road/SKILL.md"; then
1198
+ pass "qualia-road references qualia-polish-loop"
1199
+ else
1200
+ fail_case "qualia-road missing qualia-polish-loop reference"
1201
+ fi
1202
+
1203
+ # 108. package.json version is 5.1.x (multi-target install + polish-loop in v5.x line)
1204
+ if grep -qE '"5\.1\.' "$FRAMEWORK_DIR/package.json"; then
1205
+ pass "package.json version is 5.1.x"
1206
+ else
1207
+ fail_case "package.json version not 5.1.x"
1208
+ fi
1209
+
1210
+ # 109. loop.mjs installs (orchestrator)
1211
+ if [ -f "$TMP/.claude/skills/qualia-polish-loop/scripts/loop.mjs" ]; then
1212
+ pass "qualia-polish-loop scripts/loop.mjs installs"
1213
+ else
1214
+ fail_case "scripts/loop.mjs missing — install.js scripts/ subfolder copy broken"
1215
+ fi
1216
+
1217
+ # 110. playwright-capture.mjs installs (capture helper)
1218
+ if [ -f "$TMP/.claude/skills/qualia-polish-loop/scripts/playwright-capture.mjs" ]; then
1219
+ pass "qualia-polish-loop scripts/playwright-capture.mjs installs"
1220
+ else
1221
+ fail_case "scripts/playwright-capture.mjs missing"
1222
+ fi
1223
+
1224
+ # 111. fixtures/ subfolder installs (self-test pages)
1225
+ if [ -f "$TMP/.claude/skills/qualia-polish-loop/fixtures/clean.html" ] && \
1226
+ [ -f "$TMP/.claude/skills/qualia-polish-loop/fixtures/broken.html" ]; then
1227
+ pass "qualia-polish-loop fixtures/ installs (clean.html + broken.html)"
1228
+ else
1229
+ fail_case "fixtures/ subfolder not copied by install.js"
1230
+ fi
1231
+
1232
+ # 112. visual-evaluator agent installs
1233
+ if [ -f "$TMP/.claude/agents/visual-evaluator.md" ]; then
1234
+ pass "agents/visual-evaluator.md installs"
1235
+ else
1236
+ fail_case "visual-evaluator agent missing after install"
1237
+ fi
1238
+
1239
+ # 113. loop.mjs parses as valid Node ESM
1240
+ EXIT=0; $NODE --check "$FRAMEWORK_DIR/skills/qualia-polish-loop/scripts/loop.mjs" 2>/dev/null || EXIT=$?
1241
+ if [ "$EXIT" -eq 0 ]; then
1242
+ pass "loop.mjs parses as valid Node ESM"
1243
+ else
1244
+ fail_case "loop.mjs parse error"
1245
+ fi
1246
+
1247
+ # 114. playwright-capture.mjs parses as valid Node ESM
1248
+ EXIT=0; $NODE --check "$FRAMEWORK_DIR/skills/qualia-polish-loop/scripts/playwright-capture.mjs" 2>/dev/null || EXIT=$?
1249
+ if [ "$EXIT" -eq 0 ]; then
1250
+ pass "playwright-capture.mjs parses as valid Node ESM"
1251
+ else
1252
+ fail_case "playwright-capture.mjs parse error"
1253
+ fi
1254
+
1255
+ # 115. loop.mjs init creates a valid state file
1256
+ TMP_STATE=$(mktmp)/qpl-state.json
1257
+ mkdir -p "$(dirname "$TMP_STATE")"
1258
+ EXIT=0; $NODE "$FRAMEWORK_DIR/skills/qualia-polish-loop/scripts/loop.mjs" init \
1259
+ --state "$TMP_STATE" --url "http://localhost:3000" --max 4 --budget 50000 >/dev/null 2>&1 || EXIT=$?
1260
+ if [ "$EXIT" -eq 0 ] && [ -f "$TMP_STATE" ] && grep -q '"verdict": "pending"' "$TMP_STATE"; then
1261
+ pass "loop.mjs init creates valid state.json"
1262
+ else
1263
+ fail_case "loop.mjs init failed (exit=$EXIT, state present? $([ -f "$TMP_STATE" ] && echo yes || echo no))"
1264
+ fi
1265
+
1266
+ # 116. loop.mjs record kill-switch fires on 3 consecutive recurrences of same fingerprint
1267
+ TMP_STATE2=$(mktmp)/qpl-kill.json
1268
+ TMP_EVAL=$(mktmp)/qpl-eval.json
1269
+ mkdir -p "$(dirname "$TMP_STATE2")" "$(dirname "$TMP_EVAL")"
1270
+ $NODE "$FRAMEWORK_DIR/skills/qualia-polish-loop/scripts/loop.mjs" init \
1271
+ --state "$TMP_STATE2" --url "http://localhost:3000" --max 8 >/dev/null 2>&1
1272
+ # write 3 identical evals
1273
+ KILL_OK=true
1274
+ for ITER in 1 2 3; do
1275
+ cat > "$TMP_EVAL" <<EOF
1276
+ {
1277
+ "iteration": $ITER,
1278
+ "tokens_used": 100,
1279
+ "viewport_results": [],
1280
+ "aggregate_scores": {"typography":1,"color":3,"spatial":3,"layout":3,"shadow":3,"motion":3,"microcopy":3,"container":3},
1281
+ "top_issues": [{"dim":"typography","severity":"critical","description":"banned font Inter visible","likely_file":"src/styles.css","fix":"replace"}],
1282
+ "pass": false
1283
+ }
1284
+ EOF
1285
+ EXIT=0; $NODE "$FRAMEWORK_DIR/skills/qualia-polish-loop/scripts/loop.mjs" record \
1286
+ --state "$TMP_STATE2" --eval "$TMP_EVAL" >/dev/null 2>&1 || EXIT=$?
1287
+ if [ "$ITER" -lt 3 ] && [ "$EXIT" -ne 1 ]; then KILL_OK=false; fi
1288
+ if [ "$ITER" -eq 3 ] && [ "$EXIT" -ne 3 ]; then KILL_OK=false; fi
1289
+ done
1290
+ if [ "$KILL_OK" = true ] && grep -q '"verdict": "killed_regression"' "$TMP_STATE2"; then
1291
+ pass "loop.mjs kill-switch fires after 3 consecutive recurrences"
1292
+ else
1293
+ fail_case "loop.mjs kill-switch did not fire correctly"
1294
+ fi
1295
+
1296
+ # 117. visual-evaluator agent has trust-boundary block (security-critical)
1297
+ if grep -q "Trust boundary" "$TMP/.claude/agents/visual-evaluator.md"; then
1298
+ pass "visual-evaluator has trust-boundary block (refuses inlined-file injection)"
1299
+ else
1300
+ fail_case "visual-evaluator missing trust-boundary block"
1301
+ fi
1302
+
1303
+ echo ""
1304
+ echo "--- v5.1.0 (multi-target install: Claude / Codex / Both) ---"
1305
+
1306
+ # 118. Target=1 (Claude only) installs to ~/.claude/, not ~/.codex/
1307
+ TMP=$(mktmp)
1308
+ printf 'QS-FAWZI-01\n1\n' | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/log.txt" 2>&1
1309
+ EXIT=$?
1310
+ if [ "$EXIT" -eq 0 ] \
1311
+ && [ -f "$TMP/.claude/.qualia-config.json" ] \
1312
+ && [ ! -d "$TMP/.codex" ]; then
1313
+ pass "target=1 → ~/.claude/ populated, ~/.codex/ untouched"
1314
+ else
1315
+ fail_case "target=1 Claude-only" "exit=$EXIT codex_exists=$(test -d "$TMP/.codex" && echo yes || echo no)"
1316
+ fi
1317
+
1318
+ # 119. Target=2 (Codex only) writes ~/.codex/AGENTS.md, skips ~/.claude/
1319
+ TMP=$(mktmp)
1320
+ printf 'QS-FAWZI-01\n2\n' | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/log.txt" 2>&1
1321
+ EXIT=$?
1322
+ if [ "$EXIT" -eq 0 ] \
1323
+ && [ -f "$TMP/.codex/AGENTS.md" ] \
1324
+ && [ ! -d "$TMP/.claude" ] \
1325
+ && grep -q "Role: OWNER" "$TMP/.codex/AGENTS.md" \
1326
+ && ! grep -q "{{ROLE}}" "$TMP/.codex/AGENTS.md"; then
1327
+ pass "target=2 → ~/.codex/AGENTS.md with Role: OWNER, ~/.claude/ skipped"
1328
+ else
1329
+ fail_case "target=2 Codex-only" "exit=$EXIT claude_exists=$(test -d "$TMP/.claude" && echo yes || echo no)"
1330
+ fi
1331
+
1332
+ # 120. Target=3 (Both) populates both directories with the right artifacts
1333
+ TMP=$(mktmp)
1334
+ printf 'QS-FAWZI-01\n3\n' | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/log.txt" 2>&1
1335
+ EXIT=$?
1336
+ if [ "$EXIT" -eq 0 ] \
1337
+ && [ -f "$TMP/.claude/.qualia-config.json" ] \
1338
+ && [ -f "$TMP/.codex/AGENTS.md" ] \
1339
+ && grep -q "Role: OWNER" "$TMP/.codex/AGENTS.md"; then
1340
+ pass "target=3 → both ~/.claude/ and ~/.codex/AGENTS.md populated"
1341
+ else
1342
+ fail_case "target=3 Both" "exit=$EXIT"
1343
+ fi
1344
+
1345
+ # 121. Backward compat: legacy single-line piped install (no target line)
1346
+ # defaults to Claude only. Same exact invocation as pre-v5.1.
1347
+ TMP=$(mktmp)
1348
+ echo "QS-FAWZI-01" | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/log.txt" 2>&1
1349
+ EXIT=$?
1350
+ if [ "$EXIT" -eq 0 ] \
1351
+ && [ -f "$TMP/.claude/.qualia-config.json" ] \
1352
+ && [ ! -d "$TMP/.codex" ]; then
1353
+ pass "legacy single-line piped install → defaults to Claude only"
1354
+ else
1355
+ fail_case "backward compat" "exit=$EXIT"
1356
+ fi
1357
+
1358
+ # 122. Codex AGENTS.md is backed up before overwrite (matches v5.0 CLAUDE.md
1359
+ # / settings.json discipline — never silently destroy user-edited files).
1360
+ TMP=$(mktmp)
1361
+ mkdir -p "$TMP/.codex"
1362
+ echo "OLD USER CONTENT" > "$TMP/.codex/AGENTS.md"
1363
+ printf 'QS-FAWZI-01\n2\n' | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/log.txt" 2>&1
1364
+ EXIT=$?
1365
+ BAK_COUNT=$(ls "$TMP/.codex/"AGENTS.md.bak.* 2>/dev/null | wc -l)
1366
+ if [ "$EXIT" -eq 0 ] \
1367
+ && [ "$BAK_COUNT" -ge 1 ] \
1368
+ && grep -q "OLD USER CONTENT" "$TMP/.codex/"AGENTS.md.bak.* \
1369
+ && ! grep -q "OLD USER CONTENT" "$TMP/.codex/AGENTS.md"; then
1370
+ pass "Codex AGENTS.md backup-before-overwrite preserves prior content"
1371
+ else
1372
+ fail_case "Codex backup discipline" "exit=$EXIT bak_count=$BAK_COUNT"
1373
+ fi
1374
+
1375
+ # 123. Codex install with same content does NOT create a redundant .bak
1376
+ # (the v5.0 CLAUDE.md backup discipline only backs up if content differs).
1377
+ TMP=$(mktmp)
1378
+ printf 'QS-FAWZI-01\n2\n' | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/log.txt" 2>&1
1379
+ # Re-run with same input — content should be identical, no new backup.
1380
+ printf 'QS-FAWZI-01\n2\n' | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/log2.txt" 2>&1
1381
+ BAK_COUNT=$(ls "$TMP/.codex/"AGENTS.md.bak.* 2>/dev/null | wc -l)
1382
+ if [ "$BAK_COUNT" -eq 0 ]; then
1383
+ pass "Codex re-install with identical content → no redundant .bak"
1384
+ else
1385
+ fail_case "Codex backup over-zealous" "bak_count=$BAK_COUNT"
1386
+ fi
1387
+
1388
+ # 124. Non-TTY install log is free of cursor-control / line-clear escape
1389
+ # sequences (\r, \x1b[?25l/h hide-cursor, \x1b[2K clear-line). Spinner +
1390
+ # overwrite primitives must degrade cleanly when output is piped.
1391
+ TMP=$(mktmp)
1392
+ printf 'QS-FAWZI-01\n3\n' | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/log.txt" 2>&1
1393
+ # Look for any of: bare CR, hide-cursor (?25), clear-line (\x1b[2K).
1394
+ if [ -s "$TMP/log.txt" ] \
1395
+ && ! grep -q $'\r' "$TMP/log.txt" \
1396
+ && ! grep -q '?25' "$TMP/log.txt" \
1397
+ && ! grep -q '\[2K' "$TMP/log.txt"; then
1398
+ pass "non-TTY install log degrades cleanly (no cursor-control escapes)"
1399
+ else
1400
+ fail_case "non-TTY log has orphan escape sequences"
1401
+ fi
1402
+
1403
+ # 125. Final summary card includes the new "Targets" + "Time" rows
1404
+ TMP=$(mktmp)
1405
+ printf 'QS-FAWZI-01\n3\n' | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/log.txt" 2>&1
1406
+ CLEAN=$(strip_ansi < "$TMP/log.txt")
1407
+ if echo "$CLEAN" | grep -q "Targets" \
1408
+ && echo "$CLEAN" | grep -q "Claude Code · Codex" \
1409
+ && echo "$CLEAN" | grep -qE "Time +[0-9]+(\.[0-9]+s|ms|s)"; then
1410
+ pass "summary card shows Targets + Time rows"
1411
+ else
1412
+ fail_case "summary card missing v5.1 rows"
1413
+ fi
1414
+
1415
+ # 126. qualia-ui.js exports the v5.1 live-progress primitives
1416
+ EXPORTED=$($NODE -e 'const u=require("'"$UI_JS"'"); console.log(["step","spinner","progress","box","kv","divider","section","sectionClose"].every(k=>typeof u[k]==="function") ? "ok" : "missing")')
1417
+ if [ "$EXPORTED" = "ok" ]; then
1418
+ pass "qualia-ui.js exports step/spinner/progress/box/kv/divider/section/sectionClose"
1419
+ else
1420
+ fail_case "qualia-ui exports missing" "got=$EXPORTED"
1421
+ fi
1422
+
1423
+ # 127. qualia-ui.js still works as CLI when invoked directly (require-vs-CLI gate)
1424
+ OUT=$($NODE "$UI_JS" ok "test" 2>&1)
1425
+ if echo "$OUT" | grep -q "test"; then
1426
+ pass "qualia-ui.js CLI dispatch still works after exports refactor"
1427
+ else
1428
+ fail_case "qualia-ui CLI broke"
1429
+ fi
1430
+
1431
+ # 128. package.json bumped to 5.1.x
1432
+ PKG_V=$($NODE -e 'console.log(require("'"$FRAMEWORK_DIR"'/package.json").version)')
1433
+ if echo "$PKG_V" | grep -qE "^5\.1\."; then
1434
+ pass "package.json version bumped to 5.1.x ($PKG_V)"
1435
+ else
1436
+ fail_case "package.json version not bumped to 5.1.x" "got=$PKG_V"
1437
+ fi
1438
+
995
1439
  echo ""
996
1440
  echo "=== Results: $PASS passed, $FAIL failed ==="
997
1441
  [ "$FAIL" -eq 0 ] && exit 0 || exit 1