@drunkcoding/agents-and-skills 0.0.18 → 0.0.19

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 (52) hide show
  1. package/.claude-plugin/marketplace.json +5 -5
  2. package/README.md +23 -0
  3. package/package.json +1 -1
  4. package/plugins/auto-power/.claude-plugin/plugin.json +1 -1
  5. package/plugins/html-effectiveness/.claude-plugin/plugin.json +1 -1
  6. package/plugins/plugin-validator/.claude-plugin/plugin.json +1 -1
  7. package/plugins/team-superpower/.claude-plugin/plugin.json +1 -1
  8. package/plugins/team-superpower/agents/backend-developer.md +93 -3
  9. package/plugins/team-superpower/agents/designer.md +17 -2
  10. package/plugins/team-superpower/agents/frontend-developer.md +94 -3
  11. package/plugins/team-superpower/agents/planner.md +62 -6
  12. package/plugins/team-superpower/agents/qa-engineer.md +73 -17
  13. package/plugins/team-superpower/agents/reviewer.md +71 -6
  14. package/plugins/team-superpower/agents/security-engineer.md +16 -1
  15. package/plugins/team-superpower/agents/software-architect.md +16 -1
  16. package/plugins/team-superpower/assets/AGENTS.md.template +23 -0
  17. package/plugins/team-superpower/assets/CLAUDE.md.template +17 -0
  18. package/plugins/team-superpower/assets/SESSION_README.md +47 -0
  19. package/plugins/team-superpower/commands/team-feature.md +217 -11
  20. package/plugins/team-superpower/hooks/task-completed.sh +203 -0
  21. package/plugins/team-superpower/hooks/task-created.sh +42 -1
  22. package/plugins/team-superpower/scripts/assess-complexity.sh +194 -0
  23. package/plugins/team-superpower/scripts/detect-stack.sh +39 -0
  24. package/plugins/team-superpower/scripts/wave-collision-check.sh +60 -0
  25. package/plugins/tech-graph/.claude-plugin/plugin.json +1 -1
  26. package/plugins/html-effectiveness/tests/_tmp.test.js +0 -58
  27. package/plugins/html-effectiveness/tests/fixtures/01-exploration-code-approaches.data.json +0 -3
  28. package/plugins/html-effectiveness/tests/fixtures/02-exploration-visual-designs.data.json +0 -3
  29. package/plugins/html-effectiveness/tests/fixtures/03-code-review-pr.data.json +0 -3
  30. package/plugins/html-effectiveness/tests/fixtures/04-code-understanding.data.json +0 -3
  31. package/plugins/html-effectiveness/tests/fixtures/05-design-system.data.json +0 -3
  32. package/plugins/html-effectiveness/tests/fixtures/06-component-variants.data.json +0 -3
  33. package/plugins/html-effectiveness/tests/fixtures/07-prototype-animation.data.json +0 -3
  34. package/plugins/html-effectiveness/tests/fixtures/08-prototype-interaction.data.json +0 -3
  35. package/plugins/html-effectiveness/tests/fixtures/09-slide-deck.data.json +0 -3
  36. package/plugins/html-effectiveness/tests/fixtures/10-svg-illustrations.data.json +0 -3
  37. package/plugins/html-effectiveness/tests/fixtures/11-status-report.data.json +0 -37
  38. package/plugins/html-effectiveness/tests/fixtures/12-incident-report.data.json +0 -3
  39. package/plugins/html-effectiveness/tests/fixtures/13-flowchart-diagram.data.json +0 -3
  40. package/plugins/html-effectiveness/tests/fixtures/14-research-feature-explainer.data.json +0 -3
  41. package/plugins/html-effectiveness/tests/fixtures/15-research-concept-explainer.data.json +0 -3
  42. package/plugins/html-effectiveness/tests/fixtures/16-implementation-plan.data.json +0 -3
  43. package/plugins/html-effectiveness/tests/fixtures/17-pr-writeup.data.json +0 -3
  44. package/plugins/html-effectiveness/tests/fixtures/18-editor-triage-board.data.json +0 -3
  45. package/plugins/html-effectiveness/tests/fixtures/19-editor-feature-flags.data.json +0 -3
  46. package/plugins/html-effectiveness/tests/fixtures/20-editor-prompt-tuner.data.json +0 -3
  47. package/plugins/html-effectiveness/tests/fixtures/_canned.data.json +0 -7
  48. package/plugins/html-effectiveness/tests/fixtures/_canned.html.tmpl +0 -7
  49. package/plugins/html-effectiveness/tests/fixtures/_canned.manifest.json +0 -18
  50. package/plugins/html-effectiveness/tests/manifest.test.js +0 -61
  51. package/plugins/html-effectiveness/tests/mustache.test.js +0 -47
  52. package/plugins/html-effectiveness/tests/render.test.js +0 -118
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env bash
2
+ # assess-complexity.sh — encode the team-superpower v3 heuristic ladder.
3
+ #
4
+ # Inputs:
5
+ # $1 — owner's launch message (required)
6
+ # $2 — repo root (optional, default $PWD); reads CLAUDE.md's
7
+ # security.domain to bias size and stack to seed `shape:`.
8
+ #
9
+ # Output: YAML on stdout. Always emits `mode:`, `shape:`, `mode_reasoning:`.
10
+ # Emits `size:` only when mode == team.
11
+ #
12
+ # Exit codes:
13
+ # 0 — confident decision
14
+ # 1 — ambiguous (mode could plausibly be single-agent OR team; defaults to
15
+ # team and notes the ambiguity in mode_reasoning).
16
+ #
17
+ # This script never spawns or writes outside stdout. The lead handles writes.
18
+
19
+ set -euo pipefail
20
+
21
+ if [ "${1:-}" = "" ]; then
22
+ echo "usage: assess-complexity.sh <launch-message> [repo-root]" >&2
23
+ exit 2
24
+ fi
25
+
26
+ MSG="$1"
27
+ ROOT="${2:-$PWD}"
28
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
29
+ DETECT="$SCRIPT_DIR/detect-stack.sh"
30
+ PARSE="$SCRIPT_DIR/parse-claudemd.sh"
31
+
32
+ lc="$(printf '%s' "$MSG" | tr '[:upper:]' '[:lower:]')"
33
+ length="${#MSG}"
34
+
35
+ # --- Discovery / size keyword tables --------------------------------------
36
+
37
+ discovery_keywords=(
38
+ "design" "architecture" "system" "flow" "feature" "epic" "refactor"
39
+ "migrate" "migrate to" "replace with" "replace" "rewrite" "redesign"
40
+ "overhaul"
41
+ )
42
+
43
+ trivial_keywords=(
44
+ "fix typo" "typo in" "rename" "rename variable" "update copy"
45
+ "update text" "change wording" "bump version" "update readme"
46
+ "comment out" "add comment" "remove unused" "format" "prettify"
47
+ "lint fix"
48
+ )
49
+
50
+ tiny_scope_phrases=(
51
+ "one line" "single line" "one file" "just change" "quick fix"
52
+ "tiny" "trivial"
53
+ )
54
+
55
+ small_scope_verbs=(
56
+ "add" "create" "new " "introduce" "fix"
57
+ )
58
+
59
+ small_scope_nouns=(
60
+ "endpoint" "/" "component" "field" "column" "validation"
61
+ "bug" "error" "test" "migration" "button" "page" "route"
62
+ "controller" "service"
63
+ )
64
+
65
+ regulated_keywords=(
66
+ "compliance" "audit" "regulatory" "pii" "pci" "gdpr" "hipaa"
67
+ )
68
+
69
+ low_stakes_keywords=(
70
+ "prototype" "spike" "internal-only" "experiment" "poc"
71
+ )
72
+
73
+ team_request_keywords=(
74
+ "team" "agents" "full feature"
75
+ )
76
+
77
+ contains_any() {
78
+ # contains_any <text-lc> <array-name>
79
+ # Compatible with Bash 3.2 (no namerefs).
80
+ local txt="$1"
81
+ local arr_name="$2"
82
+ eval 'local kw; for kw in "${'"$arr_name"'[@]}"; do
83
+ case "$txt" in *"$kw"*) return 0 ;; esac
84
+ done'
85
+ return 1
86
+ }
87
+
88
+ # Rung 1: trivial / tiny-scope / single named file
89
+ single_file_match=0
90
+ # Detect a single path literal in the message (very rough: looks like x/y or x.y).
91
+ if printf '%s' "$MSG" | grep -qE '(^|[[:space:]])[A-Za-z0-9_./-]+\.[A-Za-z0-9]+([[:space:]]|$)'; then
92
+ # Count distinct file-ish tokens; if exactly 1, treat as named file.
93
+ count="$(printf '%s\n' "$MSG" | grep -oE '(^|[[:space:]])[A-Za-z0-9_./-]+\.[A-Za-z0-9]+([[:space:]]|$)' | tr -d '[:space:]' | sort -u | wc -l | tr -d ' ')"
94
+ if [ "$count" = "1" ]; then single_file_match=1; fi
95
+ fi
96
+
97
+ solo=0
98
+ solo_why=""
99
+ if contains_any "$lc" trivial_keywords; then solo=1; solo_why="trivial keyword match";
100
+ elif contains_any "$lc" tiny_scope_phrases; then solo=1; solo_why="tiny-scope phrase match";
101
+ elif [ "$single_file_match" = "1" ]; then solo=1; solo_why="single named file in message";
102
+ fi
103
+
104
+ # Rung 2: small-scope + single-side + no discovery language
105
+ side="none"
106
+ if [ -x "$DETECT" ] || [ -f "$DETECT" ]; then
107
+ side="$(bash "$DETECT" detect-side "$MSG" 2>/dev/null || echo none)"
108
+ fi
109
+ single_agent=0
110
+ single_agent_why=""
111
+ if [ "$solo" = "0" ]; then
112
+ if contains_any "$lc" small_scope_verbs \
113
+ && contains_any "$lc" small_scope_nouns \
114
+ && { [ "$side" = "be-only" ] || [ "$side" = "fe-only" ]; } \
115
+ && ! contains_any "$lc" discovery_keywords; then
116
+ single_agent=1
117
+ single_agent_why="small-scope verb+noun + single-side ($side) + no discovery language"
118
+ fi
119
+ fi
120
+
121
+ # Rung 3: team (default)
122
+ verb_count=0
123
+ # Cheap multi-verb heuristic: count occurrences of " and " between action words.
124
+ verb_count="$(printf '%s' "$lc" | { grep -oE '[[:space:]]and[[:space:]]' || true; } | wc -l | tr -d ' ')"
125
+ long_message=0
126
+ if [ "$length" -gt 200 ]; then long_message=1; fi
127
+
128
+ team=0
129
+ team_why=""
130
+ if [ "$solo" = "0" ] && [ "$single_agent" = "0" ]; then
131
+ team=1
132
+ if [ "$side" = "mixed" ]; then team_why="multi-side signal (BE+FE)";
133
+ elif contains_any "$lc" discovery_keywords; then team_why="discovery language match";
134
+ elif [ "$verb_count" -ge 1 ]; then team_why="multi-verb message ($verb_count 'and' joiners)";
135
+ elif [ "$long_message" = "1" ]; then team_why="long message (>200 chars)";
136
+ elif contains_any "$lc" team_request_keywords; then team_why="explicit team request";
137
+ else team_why="rung-1 and rung-2 did not match; defaulting to team";
138
+ fi
139
+ fi
140
+
141
+ # Resolve mode
142
+ if [ "$solo" = "1" ]; then mode="solo"; reason="Rung 1 matched: $solo_why."
143
+ elif [ "$single_agent" = "1" ]; then mode="single-agent"; reason="Rung 2 matched: $single_agent_why."
144
+ else mode="team"; reason="Rung 3 matched: $team_why."
145
+ fi
146
+
147
+ # Size (team only)
148
+ size=""
149
+ size_why=""
150
+ if [ "$mode" = "team" ]; then
151
+ domain=""
152
+ if [ -f "$ROOT/CLAUDE.md" ] && [ -f "$PARSE" ]; then
153
+ domain="$(bash "$PARSE" get security.domain "$ROOT/CLAUDE.md" 2>/dev/null || true)"
154
+ fi
155
+ if [ "$domain" = "payments" ] || [ "$domain" = "healthcare" ]; then
156
+ size="full"; size_why="security.domain=$domain"
157
+ elif contains_any "$lc" regulated_keywords; then
158
+ size="full"; size_why="regulated keyword match"
159
+ elif contains_any "$lc" low_stakes_keywords; then
160
+ size="minimal"; size_why="low-stakes keyword match"
161
+ else
162
+ size="standard"; size_why="default"
163
+ fi
164
+ fi
165
+
166
+ # Shape: prefer CLAUDE.md, fall back to mixed.
167
+ shape=""
168
+ if [ -f "$ROOT/CLAUDE.md" ] && [ -f "$PARSE" ]; then
169
+ shape="$(bash "$PARSE" shape "$ROOT/CLAUDE.md" 2>/dev/null || true)"
170
+ fi
171
+ if [ -z "$shape" ] || [ "$shape" = "none" ]; then
172
+ # Fall back to side detection: mixed → full-stack; be-only / fe-only as-is.
173
+ case "$side" in
174
+ mixed) shape="full-stack" ;;
175
+ be-only) shape="be-only" ;;
176
+ fe-only) shape="fe-only" ;;
177
+ *) shape="full-stack" ;;
178
+ esac
179
+ fi
180
+
181
+ # Emit
182
+ printf 'mode: %s\n' "$mode"
183
+ if [ "$mode" = "team" ]; then
184
+ printf 'size: %s\n' "$size"
185
+ fi
186
+ printf 'shape: %s\n' "$shape"
187
+ printf 'mode_reasoning: |\n'
188
+ printf ' %s\n' "$reason"
189
+ if [ "$mode" = "team" ]; then
190
+ printf ' Size: %s — %s.\n' "$size" "$size_why"
191
+ fi
192
+ printf ' side_signal: %s; verb_joiners: %d; length: %d chars.\n' "$side" "$verb_count" "$length"
193
+
194
+ exit 0
@@ -22,6 +22,45 @@
22
22
 
23
23
  set -euo pipefail
24
24
 
25
+ # ---- subcommand: detect-side ---------------------------------------------
26
+ # Usage:
27
+ # bash detect-stack.sh detect-side "<launch message>"
28
+ # Outputs one of: be-only | fe-only | mixed | none
29
+ # Used by scripts/assess-complexity.sh for the rung-2 single-side signal.
30
+ if [ "${1:-}" = "detect-side" ]; then
31
+ shift
32
+ text="${1:-}"
33
+ # Normalize to lowercase for case-insensitive matching.
34
+ lc="$(printf '%s' "$text" | tr '[:upper:]' '[:lower:]')"
35
+
36
+ be_keywords=(
37
+ "endpoint" "api" "/api" "/auth" "/health" "route" "controller" "service"
38
+ "repository" "schema" "migration" "database" "column" "table"
39
+ "queue" "worker" "cron" "webhook" "grpc" "rpc"
40
+ )
41
+ fe_keywords=(
42
+ "component" "page" "form" "button" "modal" "input" "field"
43
+ "ui" "layout" "css" "tailwind" "tsx" "jsx" "react" "vue"
44
+ "svelte" "stylesheet" "wireframe" "design system" "screen"
45
+ )
46
+
47
+ has_be=0
48
+ has_fe=0
49
+ for kw in "${be_keywords[@]}"; do
50
+ case "$lc" in *"$kw"*) has_be=1; break ;; esac
51
+ done
52
+ for kw in "${fe_keywords[@]}"; do
53
+ case "$lc" in *"$kw"*) has_fe=1; break ;; esac
54
+ done
55
+
56
+ if [ "$has_be" -eq 1 ] && [ "$has_fe" -eq 1 ]; then echo "mixed"
57
+ elif [ "$has_be" -eq 1 ]; then echo "be-only"
58
+ elif [ "$has_fe" -eq 1 ]; then echo "fe-only"
59
+ else echo "none"
60
+ fi
61
+ exit 0
62
+ fi
63
+
25
64
  ROOT="${1:-$PWD}"
26
65
  cd "$ROOT"
27
66
 
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env bash
2
+ # wave-collision-check.sh — detect file-scope collisions inside a wave.
3
+ #
4
+ # Input (stdin): one task per line, space-separated:
5
+ # <task-id> <file1> [<file2> ...]
6
+ # Files are normalized (lowercase, leading ./ stripped, slashes preserved).
7
+ #
8
+ # Exit codes:
9
+ # 0 — no collision
10
+ # 1 — at least one pair of tasks share a normalized file
11
+ #
12
+ # Stdout on collision: one line per colliding pair:
13
+ # COLLISION <task-i> <task-j> <file>
14
+
15
+ set -euo pipefail
16
+
17
+ normalize() {
18
+ # lowercase + strip leading ./
19
+ printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's|^\./||'
20
+ }
21
+
22
+ tasks=()
23
+ files_list=()
24
+
25
+ while IFS= read -r line || [ -n "$line" ]; do
26
+ [ -z "$line" ] && continue
27
+ # shellcheck disable=SC2206
28
+ parts=($line)
29
+ task="${parts[0]}"
30
+ files=()
31
+ for ((i=1; i<${#parts[@]}; i++)); do
32
+ files+=("$(normalize "${parts[$i]}")")
33
+ done
34
+ tasks+=("$task")
35
+ files_list+=("${files[*]}")
36
+ done
37
+
38
+ collisions=0
39
+ n=${#tasks[@]}
40
+ for ((i=0; i<n; i++)); do
41
+ for ((j=i+1; j<n; j++)); do
42
+ # shellcheck disable=SC2206
43
+ fi=(${files_list[$i]})
44
+ # shellcheck disable=SC2206
45
+ fj=(${files_list[$j]})
46
+ for f1 in "${fi[@]}"; do
47
+ [ -z "$f1" ] && continue
48
+ for f2 in "${fj[@]}"; do
49
+ [ -z "$f2" ] && continue
50
+ if [ "$f1" = "$f2" ]; then
51
+ printf 'COLLISION %s %s %s\n' "${tasks[$i]}" "${tasks[$j]}" "$f1"
52
+ collisions=$((collisions+1))
53
+ fi
54
+ done
55
+ done
56
+ done
57
+ done
58
+
59
+ if [ "$collisions" -gt 0 ]; then exit 1; fi
60
+ exit 0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tech-graph",
3
- "version": "0.0.18",
3
+ "version": "0.0.19",
4
4
  "description": "Step-by-step wizard for generating technical diagrams as SVG+PNG.",
5
5
  "author": {
6
6
  "name": "steven"
@@ -1,58 +0,0 @@
1
- import { test } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { existsSync, writeFileSync } from 'node:fs';
4
- import { join } from 'node:path';
5
- import { getTmpDir, cleanup, withTmp, ROOT } from '../scripts/_tmp.js';
6
-
7
- test('getTmpDir creates a fresh dir under .tmp/html-effectiveness/', () => {
8
- const dir = getTmpDir('unit-a');
9
- assert.ok(dir.endsWith('/.tmp/html-effectiveness/unit-a'));
10
- assert.ok(existsSync(dir));
11
- cleanup('unit-a');
12
- });
13
-
14
- test('getTmpDir wipes existing dir contents', () => {
15
- const dir = getTmpDir('unit-b');
16
- writeFileSync(join(dir, 'old.txt'), 'stale');
17
- const dir2 = getTmpDir('unit-b');
18
- assert.equal(dir, dir2);
19
- assert.equal(existsSync(join(dir2, 'old.txt')), false);
20
- cleanup('unit-b');
21
- });
22
-
23
- test('cleanup removes dir', () => {
24
- const dir = getTmpDir('unit-c');
25
- cleanup('unit-c');
26
- assert.equal(existsSync(dir), false);
27
- });
28
-
29
- test('cleanup is idempotent on missing dir', () => {
30
- cleanup('unit-never-created');
31
- assert.ok(true);
32
- });
33
-
34
- test('withTmp runs callback and cleans up on success', () => {
35
- let observed;
36
- const result = withTmp('unit-d', (dir) => {
37
- observed = dir;
38
- writeFileSync(join(dir, 'x'), 'y');
39
- return 'done';
40
- });
41
- assert.equal(result, 'done');
42
- assert.equal(existsSync(observed), false);
43
- });
44
-
45
- test('withTmp cleans up even when callback throws', () => {
46
- let observed;
47
- assert.throws(() => {
48
- withTmp('unit-e', (dir) => {
49
- observed = dir;
50
- throw new Error('boom');
51
- });
52
- }, /boom/);
53
- assert.equal(existsSync(observed), false);
54
- });
55
-
56
- test('ROOT is anchored at repo .tmp/html-effectiveness', () => {
57
- assert.ok(ROOT.endsWith('/.tmp/html-effectiveness'));
58
- });
@@ -1,3 +0,0 @@
1
- {
2
- "title": "Code-Approaches Exploration"
3
- }
@@ -1,3 +0,0 @@
1
- {
2
- "title": "Visual Design Exploration"
3
- }
@@ -1,3 +0,0 @@
1
- {
2
- "title": "PR Code Review Board"
3
- }
@@ -1,3 +0,0 @@
1
- {
2
- "title": "Code Walkthrough"
3
- }
@@ -1,3 +0,0 @@
1
- {
2
- "title": "Design System Sheet"
3
- }
@@ -1,3 +0,0 @@
1
- {
2
- "title": "Component Variant Matrix"
3
- }
@@ -1,3 +0,0 @@
1
- {
2
- "title": "Animation Prototype"
3
- }
@@ -1,3 +0,0 @@
1
- {
2
- "title": "Interaction Prototype"
3
- }
@@ -1,3 +0,0 @@
1
- {
2
- "title": "Slide Deck"
3
- }
@@ -1,3 +0,0 @@
1
- {
2
- "title": "SVG Illustration Gallery"
3
- }
@@ -1,37 +0,0 @@
1
- {
2
- "title": "Engineering Status",
3
- "auto_pill": "Auto-generated",
4
- "date_range": "Apr 29 — May 13, 2026",
5
- "repo": "drunkcoding/agents-and-skills",
6
- "summary": [
7
- { "num": "12", "label": "Shipped", "delta": "+3 vs prev", "delta_kind": "up" },
8
- { "num": "47", "label": "Open bugs", "delta": "-8", "delta_kind": "down" },
9
- { "num": "14", "label": "On-call", "delta": "+2", "delta_kind": "up", "warn": true },
10
- { "num": "92%","label": "CI green", "delta": "flat", "delta_kind": "flat" }
11
- ],
12
- "highlights": {
13
- "items": [
14
- { "strong": "Auth v2 cutover.", "body": "All traffic on new identity stack; p95 latency down <em>30%</em>." },
15
- { "strong": "Search reindex.", "body": "Full corpus rebuilt in 11h, down from 26h." },
16
- { "strong": "Mobile beta.", "body": "iOS 17 build live to internal testers." }
17
- ]
18
- },
19
- "shipped": {
20
- "rows": [
21
- { "pr": "#1042", "title": "feat(auth): rotate session keys", "author": "alice", "risk": "low" },
22
- { "pr": "#1048", "title": "fix(search): off-by-one in date filter", "author": "bob", "risk": "low" },
23
- { "pr": "#1055", "title": "perf(api): cache permission lookups", "author": "charlie", "risk": "med" },
24
- { "pr": "#1061", "title": "feat(mobile): push notif opt-in", "author": "dana", "risk": "high" }
25
- ]
26
- },
27
- "velocity_svg": "<svg viewBox=\"0 0 600 160\" xmlns=\"http://www.w3.org/2000/svg\"><polyline fill=\"none\" stroke=\"#D97757\" stroke-width=\"2\" points=\"0,120 75,90 150,100 225,70 300,60 375,80 450,40 525,30 600,50\"/><line x1=\"0\" y1=\"150\" x2=\"600\" y2=\"150\" stroke=\"#D1CFC5\"/></svg>",
28
- "velocity_caption": "Merged PRs per day, 2-week window. Source: GitHub API.",
29
- "carryover": {
30
- "items": [
31
- { "tag": "BLOCKED", "body": "App Store review for iOS 17 build.", "who": "dana" },
32
- { "tag": "REVIEW", "body": "Permission cache invalidation edge case.", "who": "charlie" },
33
- { "tag": "SCOPE", "body": "Q3 platform RFC pending review.", "who": "alice" }
34
- ]
35
- },
36
- "footer": "Generated 2026-05-13 · <a href=\"#\">runbook</a>"
37
- }
@@ -1,3 +0,0 @@
1
- {
2
- "title": "Incident Report"
3
- }
@@ -1,3 +0,0 @@
1
- {
2
- "title": "Flowchart Diagram"
3
- }
@@ -1,3 +0,0 @@
1
- {
2
- "title": "Feature Explainer"
3
- }
@@ -1,3 +0,0 @@
1
- {
2
- "title": "Concept Explainer"
3
- }
@@ -1,3 +0,0 @@
1
- {
2
- "title": "Implementation Plan"
3
- }
@@ -1,3 +0,0 @@
1
- {
2
- "title": "PR Writeup"
3
- }
@@ -1,3 +0,0 @@
1
- {
2
- "title": "Triage Board"
3
- }
@@ -1,3 +0,0 @@
1
- {
2
- "title": "Feature Flags Matrix"
3
- }
@@ -1,3 +0,0 @@
1
- {
2
- "title": "Prompt Tuner"
3
- }
@@ -1,7 +0,0 @@
1
- {
2
- "title": "Demo <Report>",
3
- "items": [
4
- { "name": "alpha", "html": "<b>bold</b>" },
5
- { "name": "beta", "html": "<i>italic</i>" }
6
- ]
7
- }
@@ -1,7 +0,0 @@
1
- <main class="report">
2
- <header><h1>{{title}}</h1></header>
3
- <ul>
4
- {{#items}}<li>{{name}}: {{{html}}}</li>{{/items}}
5
- </ul>
6
- {{^items}}<p>No items.</p>{{/items}}
7
- </main>
@@ -1,18 +0,0 @@
1
- {
2
- "_meta": { "upstream_repo": "test", "upstream_sha": "test", "extracted_at": "test" },
3
- "_canned": {
4
- "title": "Canned",
5
- "use_cases": ["test"],
6
- "pattern": "test",
7
- "tmpl": "fixtures/_canned.html.tmpl",
8
- "slots": {
9
- "title": { "type": "string", "required": true },
10
- "items": {
11
- "type": "array",
12
- "of": { "name": "string", "html": "html" }
13
- }
14
- },
15
- "asset_bundles": [],
16
- "extra_js": null
17
- }
18
- }
@@ -1,61 +0,0 @@
1
- import { test } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { existsSync, readFileSync } from 'node:fs';
4
- import { dirname, join } from 'node:path';
5
- import { fileURLToPath } from 'node:url';
6
-
7
- const HERE = dirname(fileURLToPath(import.meta.url));
8
- const PLUGIN_ROOT = dirname(HERE);
9
- const MANIFEST = JSON.parse(readFileSync(join(PLUGIN_ROOT, 'templates/manifest.json'), 'utf8'));
10
- const VALID_BUNDLES = new Set(['base', 'components', 'charts']);
11
-
12
- const entries = Object.entries(MANIFEST).filter(([k]) => k !== '_meta');
13
-
14
- test('manifest declares 20 vendored templates', () => {
15
- assert.equal(entries.length, 20, `expected 20 entries, got ${entries.length}`);
16
- });
17
-
18
- test('_meta records upstream SHA and fetch date', () => {
19
- const meta = MANIFEST._meta;
20
- assert.ok(meta, '_meta missing');
21
- assert.match(meta.upstream_sha, /^[0-9a-f]{40}$/, 'upstream_sha must be 40-char hex');
22
- assert.match(meta.fetched_at, /^\d{4}-\d{2}-\d{2}$/, 'fetched_at must be YYYY-MM-DD');
23
- assert.equal(meta.upstream_repo, 'ThariqS/html-effectiveness');
24
- });
25
-
26
- for (const [id, entry] of entries) {
27
- test(`${id}: required fields present`, () => {
28
- for (const f of ['title', 'use_cases', 'pattern', 'slots', 'asset_bundles']) {
29
- assert.ok(f in entry, `${id} missing field "${f}"`);
30
- }
31
- assert.ok(Array.isArray(entry.use_cases), `${id} use_cases must be array`);
32
- assert.ok(entry.use_cases.length > 0, `${id} use_cases must be non-empty`);
33
- assert.equal(typeof entry.pattern, 'string', `${id} pattern must be string`);
34
- assert.equal(typeof entry.slots, 'object', `${id} slots must be object`);
35
- });
36
-
37
- test(`${id}: asset_bundles valid`, () => {
38
- assert.ok(Array.isArray(entry.asset_bundles), `${id} asset_bundles not array`);
39
- for (const b of entry.asset_bundles) {
40
- assert.ok(VALID_BUNDLES.has(b), `${id} asset_bundles contains invalid "${b}"`);
41
- }
42
- });
43
-
44
- test(`${id}: template file exists`, () => {
45
- const tmplRel = entry.tmpl || `templates/${id}.html.tmpl`;
46
- const tmplAbs = join(PLUGIN_ROOT, tmplRel);
47
- assert.ok(existsSync(tmplAbs), `${id} template file missing: ${tmplRel}`);
48
- });
49
-
50
- test(`${id}: fixture file exists`, () => {
51
- const fix = join(PLUGIN_ROOT, `tests/fixtures/${id}.data.json`);
52
- assert.ok(existsSync(fix), `${id} fixture missing: tests/fixtures/${id}.data.json`);
53
- const data = JSON.parse(readFileSync(fix, 'utf8'));
54
- for (const [slot, schemaRaw] of Object.entries(entry.slots)) {
55
- const schema = typeof schemaRaw === 'string' ? { type: schemaRaw } : schemaRaw;
56
- if (schema.required) {
57
- assert.ok(slot in data, `${id} fixture missing required slot "${slot}"`);
58
- }
59
- }
60
- });
61
- }
@@ -1,47 +0,0 @@
1
- import { test } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { render } from '../scripts/mustache.js';
4
-
5
- test('renders bare variable, escaped by default', () => {
6
- assert.equal(render('Hello {{name}}', { name: 'World' }), 'Hello World');
7
- });
8
-
9
- test('escapes HTML special chars in {{var}}', () => {
10
- assert.equal(
11
- render('{{x}}', { x: '<script>alert(1)</script>' }),
12
- '&lt;script&gt;alert(1)&lt;/script&gt;'
13
- );
14
- });
15
-
16
- test('triple-stash {{{raw}}} bypasses escaping', () => {
17
- assert.equal(render('{{{html}}}', { html: '<b>x</b>' }), '<b>x</b>');
18
- });
19
-
20
- test('section iterates array with sub-context', () => {
21
- const tmpl = '{{#items}}[{{name}}]{{/items}}';
22
- assert.equal(render(tmpl, { items: [{ name: 'a' }, { name: 'b' }] }), '[a][b]');
23
- });
24
-
25
- test('inverted section renders only when falsy/empty', () => {
26
- assert.equal(render('{{^items}}empty{{/items}}', { items: [] }), 'empty');
27
- assert.equal(render('{{^items}}empty{{/items}}', { items: [{}] }), '');
28
- });
29
-
30
- test('missing key renders as empty string, not "undefined"', () => {
31
- assert.equal(render('x={{missing}}', {}), 'x=');
32
- });
33
-
34
- test('nested sections respect inherited context', () => {
35
- const tmpl = '{{#a}}{{#b}}{{x}}{{/b}}{{/a}}';
36
- assert.equal(render(tmpl, { a: { b: [{ x: 'ok' }] } }), 'ok');
37
- });
38
-
39
- test('falsy section (false, null, undefined, empty array) renders nothing', () => {
40
- for (const val of [false, null, undefined, []]) {
41
- assert.equal(render('{{#x}}body{{/x}}', { x: val }), '');
42
- }
43
- });
44
-
45
- test('preserves literal text around tags', () => {
46
- assert.equal(render('a {{x}} b', { x: '1' }), 'a 1 b');
47
- });