qualia-framework 5.3.0 → 5.5.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 (60) hide show
  1. package/README.md +54 -30
  2. package/agents/builder.md +33 -8
  3. package/agents/plan-checker.md +60 -3
  4. package/agents/planner.md +26 -2
  5. package/agents/qa-browser.md +10 -0
  6. package/agents/research-synthesizer.md +10 -0
  7. package/agents/researcher.md +38 -2
  8. package/agents/roadmapper.md +10 -0
  9. package/agents/verifier.md +15 -3
  10. package/agents/visual-evaluator.md +1 -1
  11. package/bin/install.js +44 -2
  12. package/bin/plan-contract.js +32 -1
  13. package/bin/state.js +155 -133
  14. package/docs/archive/v4.0.0-review.md +288 -0
  15. package/docs/erp-contract.md +11 -0
  16. package/guide.md +14 -7
  17. package/hooks/session-start.js +1 -1
  18. package/package.json +5 -2
  19. package/rules/architecture.md +125 -0
  20. package/rules/infrastructure.md +1 -2
  21. package/rules/speed.md +55 -0
  22. package/skills/qualia-discuss/SKILL.md +17 -3
  23. package/skills/qualia-help/SKILL.md +1 -1
  24. package/skills/qualia-map/SKILL.md +1 -1
  25. package/skills/qualia-milestone/SKILL.md +1 -1
  26. package/skills/qualia-new/SKILL.md +2 -2
  27. package/skills/qualia-optimize/REFERENCE.md +2 -2
  28. package/skills/qualia-optimize/SKILL.md +1 -1
  29. package/skills/qualia-polish/SKILL.md +3 -3
  30. package/skills/qualia-polish-loop/REFERENCE.md +1 -1
  31. package/skills/qualia-polish-loop/SKILL.md +3 -3
  32. package/skills/qualia-polish-loop/fixtures/broken.html +2 -2
  33. package/skills/qualia-polish-loop/scripts/score.mjs +1 -1
  34. package/skills/qualia-postmortem/SKILL.md +1 -1
  35. package/skills/qualia-quick/SKILL.md +1 -1
  36. package/skills/qualia-report/SKILL.md +8 -6
  37. package/skills/qualia-research/SKILL.md +5 -3
  38. package/skills/qualia-road/SKILL.md +15 -5
  39. package/skills/qualia-task/SKILL.md +1 -1
  40. package/templates/CONTEXT.md +3 -2
  41. package/templates/PRODUCT.md +1 -1
  42. package/templates/help.html +1 -1
  43. package/templates/phase-context.md +5 -4
  44. package/tests/bin.test.sh +33 -3
  45. package/tests/lib.test.sh +21 -0
  46. package/tests/skills.test.sh +143 -0
  47. package/tests/slop-detect.test.sh +160 -0
  48. package/docs/install-redesign-builder-prompt.md +0 -290
  49. package/docs/install-redesign-pilot.md +0 -234
  50. package/docs/journey-demo.html +0 -1008
  51. package/docs/playwright-loop-builder-prompt.md +0 -185
  52. package/docs/playwright-loop-design-notes.md +0 -108
  53. package/docs/playwright-loop-tester-prompt.md +0 -213
  54. package/docs/polish-loop-supervised-run.md +0 -111
  55. /package/{rules → qualia-design}/design-brand.md +0 -0
  56. /package/{rules → qualia-design}/design-laws.md +0 -0
  57. /package/{rules → qualia-design}/design-product.md +0 -0
  58. /package/{rules → qualia-design}/design-reference.md +0 -0
  59. /package/{rules → qualia-design}/design-rubric.md +0 -0
  60. /package/{rules → qualia-design}/frontend.md +0 -0
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: qualia-road
3
- description: "Show the Qualia workflow map in the terminal — Project → Journey → Milestones → Phases → Tasks. Lists every command, when to use it, and how phases chain. Use when user asks 'how does Qualia work', 'what's the workflow', 'show me the road', 'what command does X', 'how do projects flow', or is new to the framework. (For an interactive HTML reference instead, use /qualia-help.)"
3
+ description: "TERMINAL workflow map — Project → Journey → Milestones → Phases → Tasks. Use this in headless/SSH/no-browser sessions or when the user asks for the road in chat. For the HTML reference (default when a browser is available), use /qualia-help. Triggers: 'how does Qualia work', 'what's the workflow', 'show me the road', 'what command does X', 'how do projects flow', 'in terminal please', SSH context."
4
4
  disable-model-invocation: true
5
5
  allowed-tools:
6
6
  - Read
@@ -45,14 +45,24 @@ Every road agent loads `PRODUCT.md + DESIGN.md + design-laws.md` substrate. Buil
45
45
  /qualia-polish --quick ~1m gates only
46
46
  ```
47
47
 
48
- ## /qualia-polish-loop -- autonomous visual QA (v5.1+)
48
+ ## /qualia-polish-loop -- autonomous visual QA (v5.1+, hardened in v5.2)
49
49
  ```
50
- /qualia-polish-loop http://localhost:3000 screenshot + eval + fix loop
51
- /qualia-polish-loop {url} --max 4 cap iterations
52
- /qualia-polish-loop {url} --ref design.png anchor to reference image
50
+ /qualia-polish-loop http://localhost:3000 screenshot + eval + fix loop
51
+ /qualia-polish-loop {url} --max 4 cap iterations
52
+ /qualia-polish-loop {url} --ref design.png anchor to reference image
53
+ /qualia-polish-loop {url} --reduced-motion force prefers-reduced-motion (v5.2+)
54
+ /qualia-polish-loop --routes /a,/b,/c multi-route sweep (v5.2+)
53
55
  ```
54
56
  Screenshots at 3 viewports (375/768/1440), scores 8 design dimensions using vision, fixes issues, re-screenshots, loops until all dims >= 3 or kill-switch triggers. Per-iteration git commits for clean revert.
55
57
 
58
+ ## v5.3+ skills (Matt Pocock gaps closed)
59
+ ```
60
+ /qualia-prd synthesize current conversation → .planning/PRD-{slug}.md (durable feature spec)
61
+ /qualia-hook-gen convert a CLAUDE.md/rules instruction into a deterministic pre-tool-use hook
62
+ /qualia-optimize --deepen now spawns 3 parallel interface-design variants per candidate (Step 5b)
63
+ ```
64
+ `/qualia-prd` pairs with `/qualia-issues` to form the PRD → vertical-slice → execute loop. `/qualia-hook-gen` reduces lifetime token cost (each migrated rule frees ~50-200 tokens per request). `/qualia-optimize --deepen` produces dramatically better refactor RFCs because 3 radically-different interfaces are surfaced and the human picks/hybridizes.
65
+
56
66
  ## Alignment substrate (v5.0+)
57
67
  Before high-stakes phases, run alignment skills against `.planning/CONTEXT.md` (domain glossary) and `.planning/decisions/` (ADRs):
58
68
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: qualia-task
3
- description: "Builds a single focused task in a fresh builder context with atomic commit and validation. More structured than /qualia-quick, lighter than /qualia-build (no phase plan needed). Use when the user says 'build this one thing', 'add a component', 'implement this feature', 'qualia-task', or for any 1-5 file change outside a full phase."
3
+ description: "Single focused task with a FRESH builder subagent spawn — 1-3 hours, 1-5 files, atomic commit, validation contract. Heavier than /qualia-quick (which runs inline with no spawn) but lighter than /qualia-build (which needs a phase plan). Use when the user says 'build this one thing', 'add a component', 'implement this feature', 'qualia-task', or for any 1-5 file feature outside a full phase."
4
4
  allowed-tools:
5
5
  - Bash
6
6
  - Read
@@ -18,14 +18,15 @@ A unit of work inside a milestone. 2–5 tasks. Ends in a verification gate.
18
18
  **Avoid:** epic, story, ticket, sprint.
19
19
 
20
20
  ### Task
21
- A single commit-sized unit with one verification contract.
22
- **Avoid:** subtask, chore, todo.
21
+ A framework-internal execution unit: one commit-sized work item with one verification contract.
22
+ **Avoid:** using "task" as an ERP assignment or employee performance label unless the product domain explicitly needs it.
23
23
 
24
24
  ## Relationships
25
25
  - Project holds many Milestones
26
26
  - Milestone holds many Phases
27
27
  - Phase holds many Tasks
28
28
  - Task carries one Verification Contract
29
+ - ERP tracks project deadlines, milestone deadlines, and employee shift submissions; framework tasks stay internal.
29
30
  - {{add domain-specific relationships, e.g. "Customer holds many Orders"}}
30
31
 
31
32
  ## Flagged ambiguities
@@ -49,7 +49,7 @@ Sites the project should NOT look like. Anti-references pin down what the design
49
49
  - {URL or descriptor} — ...
50
50
  ```
51
51
 
52
- For Brand register, these are usually saturated aesthetic lanes (see `rules/design-brand.md`).
52
+ For Brand register, these are usually saturated aesthetic lanes (see `qualia-design/design-brand.md`).
53
53
  For Product register, these are usually patterns we don't want to inherit (e.g., "Salesforce Lightning — too dense, too many panels").
54
54
 
55
55
  ## Positive references (optional, ≤3)
@@ -479,7 +479,7 @@
479
479
  <li><span class="rule-icon">1</span> Feature branches by default &mdash; OWNER overrides must be explicit</li>
480
480
  <li><span class="rule-icon">2</span> Read before write &mdash; understand files before editing</li>
481
481
  <li><span class="rule-icon">3</span> MVP first &mdash; build what's asked, nothing extra</li>
482
- <li><span class="rule-icon">4</span> /qualia-report before clock-out &mdash; mandatory, enforced by ERP</li>
482
+ <li><span class="rule-icon">4</span> /qualia-report before clock-out &mdash; mandatory shift submission in ERP</li>
483
483
  <li><span class="rule-icon">5</span> Secrets through approved flows &mdash; use set-erp-key or ask Fawzi</li>
484
484
  <li><span class="rule-icon">6</span> Stuck 30+ minutes? Ask Fawzi</li>
485
485
  </ul>
@@ -13,11 +13,12 @@ Captured during `/qualia-discuss {N}` — decisions, trade-offs, and constraints
13
13
 
14
14
  ## Locked Decisions
15
15
 
16
- Non-negotiable choices. Planner must honor these exactly.
16
+ Non-negotiable choices. Planner must honor these exactly. Every row has a stable ID (`D-NN`) — the planner's Decision Coverage Audit checks each is implemented; the plan-checker BLOCKS if any is missing.
17
17
 
18
- | Decision | Rationale | Source |
19
- |----------|-----------|--------|
20
- | {e.g., "Use Supabase RLS for authorization, not middleware"} | {e.g., "Client compliance requires database-level checks"} | {who/when} |
18
+ | ID | Decision | Rationale | Source |
19
+ |----|----------|-----------|--------|
20
+ | D-01 | {e.g., "Use Supabase RLS for authorization, not middleware"} | {e.g., "Client compliance requires database-level checks"} | {who/when} |
21
+ | D-02 | {next decision} | {rationale} | {source} |
21
22
 
22
23
  ## Discretion (Planner Chooses)
23
24
 
package/tests/bin.test.sh CHANGED
@@ -1201,7 +1201,7 @@ else
1201
1201
  fi
1202
1202
 
1203
1203
  # 108. package.json version is 5.x (5.1+ accepted; v5.1 / v5.2 share the v5 line)
1204
- if grep -qE '"5\.[123]\.' "$FRAMEWORK_DIR/package.json"; then
1204
+ if grep -qE '"5\.[1234]\.' "$FRAMEWORK_DIR/package.json"; then
1205
1205
  pass "package.json version is 5.x"
1206
1206
  else
1207
1207
  fail_case "package.json version not 5.x"
@@ -1430,7 +1430,7 @@ fi
1430
1430
 
1431
1431
  # 128. package.json bumped to 5.x (5.1+ accepted; 5.2 is the v5.2 release)
1432
1432
  PKG_V=$($NODE -e 'console.log(require("'"$FRAMEWORK_DIR"'/package.json").version)')
1433
- if echo "$PKG_V" | grep -qE "^5\.[123]\."; then
1433
+ if echo "$PKG_V" | grep -qE "^5\.[1234]\."; then
1434
1434
  pass "package.json version bumped to 5.x ($PKG_V)"
1435
1435
  else
1436
1436
  fail_case "package.json version not 5.x" "got=$PKG_V"
@@ -1577,12 +1577,42 @@ fi
1577
1577
 
1578
1578
  # 143. package.json version is 5.x (5.1+ accepted; v5.3 is the v5.3 release)
1579
1579
  PKG_V=$($NODE -e 'console.log(require("'"$FRAMEWORK_DIR"'/package.json").version)')
1580
- if echo "$PKG_V" | grep -qE "^5\.[123]\."; then
1580
+ if echo "$PKG_V" | grep -qE "^5\.[1234]\."; then
1581
1581
  pass "package.json version is 5.x ($PKG_V) — v5.3 accepted"
1582
1582
  else
1583
1583
  fail_case "package.json version not 5.x" "got=$PKG_V"
1584
1584
  fi
1585
1585
 
1586
+ echo ""
1587
+ echo "--- ERP shift-report contract ---"
1588
+
1589
+ # 144. qualia-report describes clock-out as truthful shift submission, not task completion
1590
+ if grep -qi "daily shift report" "$TMP/.claude/skills/qualia-report/SKILL.md" \
1591
+ && grep -qi "not a task-completion ceremony" "$TMP/.claude/skills/qualia-report/SKILL.md" \
1592
+ && grep -qi "What happened during your shift" "$TMP/.claude/skills/qualia-report/SKILL.md"; then
1593
+ pass "qualia-report frames clock-out as shift submission, not task completion"
1594
+ else
1595
+ fail_case "qualia-report missing shift-submission contract"
1596
+ fi
1597
+
1598
+ # 145. ERP contract documents the date model: project, milestone, employee submission
1599
+ if grep -q "Project deadline" "$FRAMEWORK_DIR/docs/erp-contract.md" \
1600
+ && grep -q "Milestone deadline" "$FRAMEWORK_DIR/docs/erp-contract.md" \
1601
+ && grep -q "Employee submission date" "$FRAMEWORK_DIR/docs/erp-contract.md" \
1602
+ && grep -q "Phase and task counters remain framework telemetry" "$FRAMEWORK_DIR/docs/erp-contract.md"; then
1603
+ pass "ERP contract documents project/milestone/submission date model"
1604
+ else
1605
+ fail_case "ERP contract missing date-model clarification"
1606
+ fi
1607
+
1608
+ # 146. Project glossary keeps framework tasks internal to agent execution
1609
+ if grep -q "framework-internal execution unit" "$FRAMEWORK_DIR/templates/CONTEXT.md" \
1610
+ && grep -q "ERP tracks project deadlines, milestone deadlines, and employee shift submissions" "$FRAMEWORK_DIR/templates/CONTEXT.md"; then
1611
+ pass "CONTEXT template distinguishes internal tasks from ERP workflow"
1612
+ else
1613
+ fail_case "CONTEXT template missing framework-task vs ERP-workflow distinction"
1614
+ fi
1615
+
1586
1616
  echo ""
1587
1617
  echo "=== Results: $PASS passed, $FAIL failed ==="
1588
1618
  [ "$FAIL" -eq 0 ] && exit 0 || exit 1
package/tests/lib.test.sh CHANGED
@@ -57,6 +57,27 @@ console.log(errs.length > 0 ? "REJECTED" : "ACCEPTED");
57
57
  ')
58
58
  [ "$OUT" = "REJECTED" ] && ok "rejects malformed contract" || fail "malformed accepted"
59
59
 
60
+ OUT=$($NODE -e '
61
+ const pc = require("'"$PC"'");
62
+ const slop = {
63
+ version: 1, phase: 1, goal: "x", why: "y",
64
+ generated_at: "t", generated_by: "planner", source_plan_hash: "h",
65
+ success_criteria: ["sc"],
66
+ tasks: [{
67
+ id: "T1", title: "t", wave: 1, depends_on: [],
68
+ files_modify: [], files_create: [], files_delete: [],
69
+ acceptance_criteria: ["minimal implementation of login"],
70
+ action: "Add hardcoded for now placeholder, will be wired later",
71
+ context_files: [],
72
+ verification: [{ type: "file-exists", path: "a.ts" }]
73
+ }]
74
+ };
75
+ const errs = pc.validate(slop);
76
+ const hits = errs.filter(e => /scope-reduction/.test(e));
77
+ console.log(hits.length >= 2 ? "DETECTED" : "MISSED:" + errs.join(";"));
78
+ ')
79
+ [ "$OUT" = "DETECTED" ] && ok "detects scope-reduction phrases in action + acceptance_criteria" || fail "scope-reduction missed: $OUT"
80
+
60
81
  OUT=$($NODE -e '
61
82
  const pc = require("'"$PC"'");
62
83
  const c = {
@@ -0,0 +1,143 @@
1
+ #!/bin/bash
2
+ # Qualia Framework — skill smoke tests
3
+ # Verifies every skills/*/SKILL.md is well-formed:
4
+ # - YAML frontmatter present and parseable
5
+ # - name field matches folder name
6
+ # - description present and substantive
7
+ # - description has trigger phrases (or skill is disable-model-invocation)
8
+ # - body has at least one h1 heading and 2+ sections
9
+ #
10
+ # Run: bash tests/skills.test.sh
11
+
12
+ PASS=0
13
+ FAIL=0
14
+ SKILLS_DIR="$(cd "$(dirname "$0")/../skills" && pwd)"
15
+
16
+ # Skills allowed to ship without trigger phrases — disable-model-invocation
17
+ # skills only fire on explicit slash command, so triggers are optional.
18
+ SKIP_TRIGGER_CHECK=("qualia-road" "qualia-handoff")
19
+
20
+ pass() {
21
+ echo " ✓ $1"
22
+ PASS=$((PASS + 1))
23
+ }
24
+
25
+ fail_case() {
26
+ echo " ✗ $1"
27
+ echo " $2"
28
+ FAIL=$((FAIL + 1))
29
+ }
30
+
31
+ is_in_skip_list() {
32
+ local needle="$1"
33
+ for x in "${SKIP_TRIGGER_CHECK[@]}"; do
34
+ [ "$x" = "$needle" ] && return 0
35
+ done
36
+ return 1
37
+ }
38
+
39
+ echo "skills.test.sh — smoke tests for every skills/*/SKILL.md"
40
+ echo ""
41
+
42
+ for skill_dir in "$SKILLS_DIR"/*/; do
43
+ name=$(basename "$skill_dir")
44
+ skill_md="$skill_dir/SKILL.md"
45
+
46
+ # Existence
47
+ if [ ! -f "$skill_md" ]; then
48
+ fail_case "$name" "SKILL.md not found at $skill_md"
49
+ continue
50
+ fi
51
+
52
+ # Frontmatter present
53
+ if ! head -1 "$skill_md" | grep -q "^---$"; then
54
+ fail_case "$name: frontmatter" "first line is not '---'"
55
+ continue
56
+ fi
57
+ if ! sed -n '2,30p' "$skill_md" | grep -q "^---$"; then
58
+ fail_case "$name: frontmatter" "no closing --- within first 30 lines"
59
+ continue
60
+ fi
61
+
62
+ # name field matches folder
63
+ fm_name=$(grep "^name:" "$skill_md" | head -1 | sed 's/^name:[[:space:]]*//' | tr -d '"')
64
+ if [ "$fm_name" != "$name" ]; then
65
+ fail_case "$name: name field" "frontmatter says name=\"$fm_name\", folder is \"$name\""
66
+ continue
67
+ fi
68
+ pass "$name: frontmatter name matches folder"
69
+
70
+ # description field present + substantive
71
+ fm_desc=$(grep "^description:" "$skill_md" | head -1 | sed 's/^description:[[:space:]]*//')
72
+ desc_len=${#fm_desc}
73
+ if [ "$desc_len" -lt 50 ]; then
74
+ fail_case "$name: description" "description is $desc_len chars, expected >= 50"
75
+ continue
76
+ fi
77
+ pass "$name: description present (${desc_len} chars)"
78
+
79
+ # Trigger phrases (unless disable-model-invocation or in transitional skip list)
80
+ if ! is_in_skip_list "$name"; then
81
+ has_disable=$(grep -c "disable-model-invocation:[[:space:]]*true" "$skill_md")
82
+ if [ "$has_disable" = "0" ]; then
83
+ if echo "$fm_desc" | grep -qiE "trigger|when user|use when|invoke|says|use this|fire on|user types"; then
84
+ pass "$name: description has trigger guidance"
85
+ else
86
+ fail_case "$name: triggers" "description lacks trigger phrases (Trigger:/Use when:/'says'/etc.) and skill is not disable-model-invocation"
87
+ fi
88
+ fi
89
+ fi
90
+
91
+ # Body has an h1 heading
92
+ h1_count=$(grep -cE "^# " "$skill_md")
93
+ if [ "$h1_count" -ge 1 ]; then
94
+ pass "$name: body has h1 heading"
95
+ else
96
+ fail_case "$name: body" "no h1 heading (^# ) in body"
97
+ fi
98
+
99
+ # Body has at least one section (any ## heading)
100
+ h2_count=$(grep -cE "^## " "$skill_md")
101
+ if [ "$h2_count" -ge 1 ]; then
102
+ pass "$name: body has section heading (${h2_count} found)"
103
+ else
104
+ fail_case "$name: body" "no '## ' section heading; every skill needs at least one"
105
+ fi
106
+
107
+ # Cache-aware spawn audit (per rules/grounding.md):
108
+ # Every spawn to a CUSTOM (qualia-*) agent must anchor the prompt with
109
+ # `@~/.claude/agents/{name}.md` (either `Role: @...` or `Read your role:
110
+ # @...` — both forms accepted). The role file is session-stable; placing
111
+ # it first lets Anthropic's prompt cache reuse the prefix across spawns
112
+ # (documented 81-90% cost reduction). If task-specific content lands
113
+ # before the role anchor, the entire prefix recomputes on every spawn.
114
+ #
115
+ # Built-in subagent types (Explore, general-purpose, Plan, etc.) have
116
+ # stable system-prompt baselines on Anthropic's side; no Role anchor
117
+ # required. We count only `subagent_type="qualia-*"` spawns.
118
+ #
119
+ # Some skills follow progressive-disclosure discipline (e.g.
120
+ # qualia-polish-loop) and put the literal spawn template in REFERENCE.md
121
+ # while SKILL.md mentions the spawn in prose. We scan both.
122
+ custom_spawn_count=$(grep -c 'subagent_type="qualia-' "$skill_md")
123
+ ref_md="$skill_dir/REFERENCE.md"
124
+ if [ -f "$ref_md" ]; then
125
+ custom_spawn_count=$((custom_spawn_count + $(grep -c 'subagent_type="qualia-' "$ref_md")))
126
+ fi
127
+ if [ "${custom_spawn_count:-0}" -gt 0 ]; then
128
+ role_count=$(grep -cE '@~/\.claude/agents/' "$skill_md")
129
+ if [ -f "$ref_md" ]; then
130
+ role_count=$((role_count + $(grep -cE '@~/\.claude/agents/' "$ref_md")))
131
+ fi
132
+ if [ "${role_count:-0}" -ge "$custom_spawn_count" ]; then
133
+ pass "$name: spawn audit ($custom_spawn_count custom spawn(s), all role-anchored for cache)"
134
+ else
135
+ fail_case "$name: spawn audit" "$custom_spawn_count custom spawn(s) but only ${role_count:-0} '@~/.claude/agents/' anchors — prompt cache will miss"
136
+ fi
137
+ fi
138
+ done
139
+
140
+ echo ""
141
+ echo "=== Results: $PASS passed, $FAIL failed ==="
142
+
143
+ [ "$FAIL" = "0" ]
@@ -0,0 +1,160 @@
1
+ #!/bin/bash
2
+ # Qualia Framework — bin/slop-detect.mjs behavior tests
3
+ # Verifies the AI-tells gatekeeper actually catches what it claims to catch.
4
+ #
5
+ # Run: bash tests/slop-detect.test.sh
6
+
7
+ PASS=0
8
+ FAIL=0
9
+ SLOP_DETECT="$(cd "$(dirname "$0")/../bin" && pwd)/slop-detect.mjs"
10
+ NODE="${NODE:-node}"
11
+
12
+ TMP_DIRS=()
13
+ cleanup() {
14
+ for d in "${TMP_DIRS[@]}"; do
15
+ [ -d "$d" ] && rm -rf "$d"
16
+ done
17
+ }
18
+ trap cleanup EXIT
19
+
20
+ mktmp() {
21
+ local TMP
22
+ TMP=$(mktemp -d)
23
+ TMP_DIRS+=("$TMP")
24
+ echo "$TMP"
25
+ }
26
+
27
+ pass() {
28
+ echo " ✓ $1"
29
+ PASS=$((PASS + 1))
30
+ }
31
+
32
+ fail_case() {
33
+ echo " ✗ $1"
34
+ echo " $2"
35
+ FAIL=$((FAIL + 1))
36
+ }
37
+
38
+ echo "slop-detect.test.sh — bin/slop-detect.mjs behavioral tests"
39
+ echo ""
40
+
41
+ # ── Sanity: file exists and parses ────────────────────────────────────
42
+ if [ ! -f "$SLOP_DETECT" ]; then
43
+ fail_case "slop-detect exists" "$SLOP_DETECT not found"
44
+ echo "=== Results: $PASS passed, $FAIL failed ==="
45
+ exit 1
46
+ fi
47
+ pass "slop-detect.mjs exists at expected path"
48
+
49
+ if ! $NODE --check "$SLOP_DETECT" 2>&1 | head -1; then
50
+ pass "slop-detect.mjs parses as valid JS"
51
+ fi
52
+ # Above is a heredoc test — node --check succeeds silently on valid JS.
53
+ # Re-test explicitly so a parse error fails the suite.
54
+ if $NODE --check "$SLOP_DETECT" 2>/dev/null; then
55
+ pass "slop-detect.mjs syntax is valid"
56
+ else
57
+ fail_case "syntax check" "node --check failed on $SLOP_DETECT"
58
+ fi
59
+
60
+ # ── Clean file: should exit 0 ─────────────────────────────────────────
61
+ TMP=$(mktmp)
62
+ cat > "$TMP/clean.tsx" <<'EOF'
63
+ import { Button } from '@/components/ui/button';
64
+
65
+ export default function Page() {
66
+ return (
67
+ <div className="bg-surface text-foreground">
68
+ <h1 className="text-display">Welcome</h1>
69
+ <Button variant="primary">Continue setup</Button>
70
+ </div>
71
+ );
72
+ }
73
+ EOF
74
+ if $NODE "$SLOP_DETECT" "$TMP/clean.tsx" >/dev/null 2>&1; then
75
+ pass "exits 0 on a clean .tsx file"
76
+ else
77
+ fail_case "clean file" "exit non-zero on a deliberately clean file"
78
+ fi
79
+
80
+ # ── Em-dash detection (HIGH severity — reported, doesn't block) ──────
81
+ # Em-dash is HIGH not CRITICAL, so default exit is 0; we verify the
82
+ # FINDING is reported to stdout/stderr, not the exit code.
83
+ TMP2=$(mktmp)
84
+ cat > "$TMP2/emdash.tsx" <<'EOF'
85
+ export default function Page() {
86
+ return <p>Welcome — to our amazing platform</p>;
87
+ }
88
+ EOF
89
+ OUT=$($NODE "$SLOP_DETECT" "$TMP2/emdash.tsx" 2>&1 || true)
90
+ if echo "$OUT" | grep -qiE "em.?dash|—"; then
91
+ pass "reports em-dash finding (HIGH severity, non-blocking)"
92
+ else
93
+ fail_case "em-dash detection" "no em-dash mention in output: $(echo "$OUT" | head -c 120)"
94
+ fi
95
+
96
+ # ── Banned-font detection ─────────────────────────────────────────────
97
+ TMP3=$(mktmp)
98
+ cat > "$TMP3/font.css" <<'EOF'
99
+ body { font-family: "Inter", sans-serif; }
100
+ EOF
101
+ EXIT_CODE=0
102
+ $NODE "$SLOP_DETECT" "$TMP3/font.css" >/dev/null 2>&1 || EXIT_CODE=$?
103
+ if [ "$EXIT_CODE" = "1" ]; then
104
+ pass "exits 1 on banned font (Inter) in CSS"
105
+ else
106
+ fail_case "banned-font detection" "expected exit 1, got $EXIT_CODE"
107
+ fi
108
+
109
+ # ── Purple-blue gradient detection ────────────────────────────────────
110
+ TMP4=$(mktmp)
111
+ cat > "$TMP4/gradient.tsx" <<'EOF'
112
+ export default function Hero() {
113
+ return <div className="bg-gradient-to-r from-blue-500 to-purple-600">Hi</div>;
114
+ }
115
+ EOF
116
+ EXIT_CODE=0
117
+ $NODE "$SLOP_DETECT" "$TMP4/gradient.tsx" >/dev/null 2>&1 || EXIT_CODE=$?
118
+ if [ "$EXIT_CODE" = "1" ]; then
119
+ pass "exits 1 on purple-blue gradient (the #1 AI-design tell)"
120
+ else
121
+ fail_case "gradient detection" "expected exit 1, got $EXIT_CODE"
122
+ fi
123
+
124
+ # ── Existing fixture: skills/qualia-polish-loop/fixtures/broken.html ──
125
+ FIXTURE="$(cd "$(dirname "$0")/.." && pwd)/skills/qualia-polish-loop/fixtures/broken.html"
126
+ if [ -f "$FIXTURE" ]; then
127
+ EXIT_CODE=0
128
+ $NODE "$SLOP_DETECT" "$FIXTURE" >/dev/null 2>&1 || EXIT_CODE=$?
129
+ if [ "$EXIT_CODE" = "1" ]; then
130
+ pass "exits 1 on the broken.html fixture (designed to hit critical bans)"
131
+ else
132
+ fail_case "fixture detection" "broken.html fixture exited $EXIT_CODE; expected 1"
133
+ fi
134
+ else
135
+ echo " - broken.html fixture not present, skipping"
136
+ fi
137
+
138
+ # ── --json flag produces JSON output ─────────────────────────────────
139
+ TMP5=$(mktmp)
140
+ cp "$TMP3/font.css" "$TMP5/font.css"
141
+ JSON_OUT=$($NODE "$SLOP_DETECT" --json "$TMP5/font.css" 2>/dev/null || true)
142
+ if echo "$JSON_OUT" | head -1 | grep -qE "^[\{\[]"; then
143
+ pass "--json flag produces JSON-shaped output"
144
+ else
145
+ fail_case "--json output" "first line is not JSON-shaped: '$(echo "$JSON_OUT" | head -c 80)'"
146
+ fi
147
+
148
+ # ── Invocation error: no path provided AND no default repo ───────────
149
+ EXIT_CODE=0
150
+ $NODE "$SLOP_DETECT" /nonexistent/path/that/cannot/exist >/dev/null 2>&1 || EXIT_CODE=$?
151
+ if [ "$EXIT_CODE" = "2" ] || [ "$EXIT_CODE" = "0" ]; then
152
+ pass "handles missing path gracefully (exit=$EXIT_CODE — 0=skip, 2=invocation error)"
153
+ else
154
+ fail_case "missing path" "unexpected exit $EXIT_CODE on /nonexistent path"
155
+ fi
156
+
157
+ echo ""
158
+ echo "=== Results: $PASS passed, $FAIL failed ==="
159
+
160
+ [ "$FAIL" = "0" ]