@intentsolutionsio/contributing-clanker 0.1.1

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 (69) hide show
  1. package/README.md +173 -0
  2. package/hooks/.gitkeep +0 -0
  3. package/hooks/install.sh +115 -0
  4. package/hooks/uninstall.sh +70 -0
  5. package/package.json +42 -0
  6. package/skills/contribute/SKILL.md +457 -0
  7. package/skills/contribute/agents/draft-writer.md +110 -0
  8. package/skills/contribute/agents/repo-analyzer.md +68 -0
  9. package/skills/contribute/agents/researcher.md +246 -0
  10. package/skills/contribute/agents/scout.md +182 -0
  11. package/skills/contribute/agents/test-runner.md +70 -0
  12. package/skills/contribute/assets/claim-template.md +22 -0
  13. package/skills/contribute/assets/evidence-template.md +32 -0
  14. package/skills/contribute/assets/pr-template.md +49 -0
  15. package/skills/contribute/references/candidate-file-format.md +259 -0
  16. package/skills/contribute/references/workflow-guide.md +153 -0
  17. package/skills/contribute/scripts/audit-overrides.sh +180 -0
  18. package/skills/contribute/scripts/catalog-coverage.sh +99 -0
  19. package/skills/contribute/scripts/gate-runner.sh +191 -0
  20. package/skills/contribute/scripts/gates/a01-already-assigned.sh +24 -0
  21. package/skills/contribute/scripts/gates/a02-already-shipped.sh +31 -0
  22. package/skills/contribute/scripts/gates/a03-duplicate-flagged.sh +22 -0
  23. package/skills/contribute/scripts/gates/a04-issue-age.sh +80 -0
  24. package/skills/contribute/scripts/gates/a05-issue-still-open.sh +33 -0
  25. package/skills/contribute/scripts/gates/a06-claim-etiquette-required.sh +36 -0
  26. package/skills/contribute/scripts/gates/a09-mention-routing.sh +63 -0
  27. package/skills/contribute/scripts/gates/b01-base-branch.sh +29 -0
  28. package/skills/contribute/scripts/gates/b02-branch-naming.sh +34 -0
  29. package/skills/contribute/scripts/gates/b03-clone-fresh.sh +33 -0
  30. package/skills/contribute/scripts/gates/b05-dco-signoff.sh +50 -0
  31. package/skills/contribute/scripts/gates/b06-commit-format.sh +44 -0
  32. package/skills/contribute/scripts/gates/b07-scope-files.sh +68 -0
  33. package/skills/contribute/scripts/gates/b12-new-deps.sh +40 -0
  34. package/skills/contribute/scripts/gates/b14-local-checks.sh +55 -0
  35. package/skills/contribute/scripts/gates/b16-local-check-allowlist.sh +32 -0
  36. package/skills/contribute/scripts/gates/c01-draft-first.sh +23 -0
  37. package/skills/contribute/scripts/gates/c02-pr-title-format.sh +36 -0
  38. package/skills/contribute/scripts/gates/c03-pr-body-sections.sh +58 -0
  39. package/skills/contribute/scripts/gates/c04-ui-screenshots.sh +38 -0
  40. package/skills/contribute/scripts/gates/c05-test-evidence.sh +31 -0
  41. package/skills/contribute/scripts/gates/c07-coauthor-banned.sh +30 -0
  42. package/skills/contribute/scripts/gates/c09-issue-link.sh +31 -0
  43. package/skills/contribute/scripts/gates/c11-no-force-push.sh +32 -0
  44. package/skills/contribute/scripts/gates/c12-ci-green.sh +29 -0
  45. package/skills/contribute/scripts/gates/c13-bots-passed.sh +62 -0
  46. package/skills/contribute/scripts/gates/c16-no-self-merge.sh +24 -0
  47. package/skills/contribute/scripts/gates/c19-body-claim-vs-diff.sh +64 -0
  48. package/skills/contribute/scripts/gates/d02-no-ai-bug-reports.sh +48 -0
  49. package/skills/contribute/scripts/gates/d03-no-ai-pr-reviews.sh +42 -0
  50. package/skills/contribute/scripts/gates/d05-no-reopen.sh +25 -0
  51. package/skills/contribute/scripts/gates/e02-ai-strike-track.sh +57 -0
  52. package/skills/contribute/scripts/gates/e04-fork-target.sh +39 -0
  53. package/skills/contribute/scripts/gates/f01-license-compat.sh +92 -0
  54. package/skills/contribute/scripts/gates/f03-fixtures-clean.sh +30 -0
  55. package/skills/contribute/scripts/gates/f04-override-disclosure.sh +49 -0
  56. package/skills/contribute/scripts/gates/g01-no-vendored-edits.sh +52 -0
  57. package/skills/contribute/scripts/gates/g02-protected-paths.sh +30 -0
  58. package/skills/contribute/scripts/gates/g03-no-changelog-edits.sh +37 -0
  59. package/skills/contribute/scripts/gates/g04-no-version-bump.sh +36 -0
  60. package/skills/contribute/scripts/gates/g06-override-rate-limit.sh +31 -0
  61. package/skills/contribute/scripts/gates/lib/preamble.sh +105 -0
  62. package/skills/contribute/scripts/lint-candidate.sh +149 -0
  63. package/skills/contribute/scripts/researcher-build.sh +456 -0
  64. package/skills/contribute/scripts/test-known-traps.sh +142 -0
  65. package/skills/contribute/scripts/test-override-audit.sh +102 -0
  66. package/skills/contribute/scripts/test-plug-in.sh +113 -0
  67. package/skills/contribute/scripts/test-scout-refresh.sh +157 -0
  68. package/skills/contribute/scripts/test-stale-dossier-refresh.sh +96 -0
  69. package/skills/contribute/scripts/transition.sh +260 -0
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env bash
2
+ # test-override-audit.sh — regression test for the override audit trail.
3
+ #
4
+ # Promise: every `transition.sh --override-gate <ID> "reason"` call
5
+ # 1. appends a `gate_override` event to ~/.contribute-system/log.jsonl
6
+ # 2. writes the override into the candidate's frontmatter overrides: array
7
+ # 3. allows the transition to proceed (BLOCK gate becomes effective-PASS)
8
+ #
9
+ # This validates the audit trail that lets engineers see WHY a gate was bypassed.
10
+ #
11
+ # Usage: test-override-audit.sh [--verbose]
12
+ # Exit 0: all assertions hold. Exit 1: any failure.
13
+
14
+ set -uo pipefail
15
+
16
+ VERBOSE="${1:-}"
17
+ SYS="$HOME/.contribute-system"
18
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
+ TMPDIR=$(/usr/bin/mktemp -d)
20
+ trap 'rm -rf "$TMPDIR"' EXIT
21
+
22
+ PASS=0
23
+ FAIL=0
24
+ red() { /usr/bin/printf '\033[31m%s\033[0m' "$1"; }
25
+ green() { /usr/bin/printf '\033[32m%s\033[0m' "$1"; }
26
+
27
+ assert() {
28
+ local name="$1" expr="$2"
29
+ /usr/bin/printf ' %-60s ' "$name"
30
+ if eval "$expr" >/dev/null 2>&1; then
31
+ green "PASS"; /usr/bin/echo
32
+ PASS=$((PASS + 1))
33
+ else
34
+ red "FAIL"; /usr/bin/echo
35
+ [[ "$VERBOSE" == "--verbose" ]] && /usr/bin/printf ' expr: %s\n' "$expr" >&2
36
+ FAIL=$((FAIL + 1))
37
+ fi
38
+ }
39
+
40
+ # Build a synthetic candidate (closed issue, will trigger A05 BLOCK)
41
+ SYNTH="$TMPDIR/synth-override.md"
42
+ /usr/bin/cat > "$SYNTH" <<'EOF'
43
+ ---
44
+ discovered_at: 2026-05-03T00:00:00Z
45
+ repo: lingdojo/kana-dojo
46
+ issue_number: 15441
47
+ issue_url: https://github.com/lingdojo/kana-dojo/issues/15441
48
+ star_tier: mainstream
49
+ star_count: 2231
50
+ repo_lang: TypeScript
51
+ competing_prs: 0
52
+ primary_label: bug
53
+ scout_score: 0.5
54
+ status: open
55
+ last_refreshed: 2026-05-03T00:00:00Z
56
+ ---
57
+
58
+ # synthetic — override audit test
59
+ EOF
60
+
61
+ LOG_BEFORE_LINES=$(/usr/bin/wc -l < "$SYS/log.jsonl" 2>/dev/null || /usr/bin/echo "0")
62
+
63
+ /usr/bin/printf '\n=== override audit regression ===\n\n'
64
+
65
+ # --- TEST 1: dry-run override pre-records WITHOUT mutating candidate ---
66
+ "$SCRIPT_DIR/transition.sh" "shortlist→claimed" "$SYNTH" \
67
+ --dry-run \
68
+ --override-gate A05 "regression test: dry-run should not write" \
69
+ >/dev/null 2>&1
70
+
71
+ assert "1. dry-run override does NOT write 'overrides:' to candidate" \
72
+ '! /usr/bin/grep -q "^overrides:" "$SYNTH"'
73
+
74
+ # --- TEST 2: real override (no --dry-run) writes to candidate frontmatter ---
75
+ "$SCRIPT_DIR/transition.sh" "shortlist→claimed" "$SYNTH" \
76
+ --override-gate A05 "test 2: real override audit" \
77
+ >/dev/null 2>&1 || true # gate block exit-code may be 0 or 1; we don't care
78
+
79
+ assert "2. real override DOES write 'overrides:' to candidate" \
80
+ '/usr/bin/grep -q "^overrides:" "$SYNTH"'
81
+
82
+ assert "3. override entry contains gate ID A05" \
83
+ '/usr/bin/grep -q "gate: A05" "$SYNTH"'
84
+
85
+ assert "4. override entry contains the reason text" \
86
+ '/usr/bin/grep -q "test 2: real override audit" "$SYNTH"'
87
+
88
+ # --- TEST 3: log.jsonl gets gate_override event ---
89
+ LOG_AFTER_LINES=$(/usr/bin/wc -l < "$SYS/log.jsonl" 2>/dev/null || /usr/bin/echo "0")
90
+
91
+ assert "5. log.jsonl grew (new event lines appended)" \
92
+ '[[ "$LOG_AFTER_LINES" -gt "$LOG_BEFORE_LINES" ]]'
93
+
94
+ assert "6. log.jsonl contains a gate_override event for A05" \
95
+ '/usr/bin/tail -20 "$SYS/log.jsonl" | /usr/bin/grep -q "\"event\":\"gate_override\".*\"gate\":\"A05\""'
96
+
97
+ /usr/bin/echo
98
+ /usr/bin/printf '=== summary: %s passed · %s failed ===\n\n' \
99
+ "$(green "$PASS")" "$([ "$FAIL" -gt 0 ] && red "$FAIL" || /usr/bin/echo 0)"
100
+
101
+ [[ "$FAIL" -eq 0 ]] || exit 1
102
+ exit 0
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env bash
2
+ # test-plug-in.sh — regression test for the gate-runner plug-in / discovery contract.
3
+ #
4
+ # Promise: gate-runner discovers gate scripts by glob from
5
+ # 1. its own scripts/gates/ dir (bundled canonical set)
6
+ # 2. ~/.contribute-system/gates/ (user-override dir)
7
+ # Drop a new executable .sh in either dir and the runner picks it up
8
+ # automatically — no orchestrator changes needed.
9
+ #
10
+ # This is the load-bearing property that makes gates pluggable.
11
+ #
12
+ # Usage: test-plug-in.sh [--verbose]
13
+ # Exit 0: all assertions hold. Exit 1: any failure.
14
+
15
+ set -uo pipefail
16
+
17
+ VERBOSE="${1:-}"
18
+ SYS="$HOME/.contribute-system"
19
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20
+ USER_GATES="$SYS/gates"
21
+
22
+ # Plant a no-op gate in the user-override dir. Use a phase letter that runs
23
+ # at shortlist→claimed (phase A). gate-runner globs phase-letter-prefix.sh.
24
+ PLUGIN_GATE_NAME="azz99-plugin-test-$$.sh"
25
+ PLUGIN_GATE="$USER_GATES/$PLUGIN_GATE_NAME"
26
+ TMPDIR=$(/usr/bin/mktemp -d)
27
+ trap 'rm -f "$PLUGIN_GATE"; rm -rf "$TMPDIR"' EXIT
28
+
29
+ # Construct the no-op gate. Just emits PASS.
30
+ /usr/bin/cat > "$PLUGIN_GATE" <<'EOF'
31
+ #!/usr/bin/env bash
32
+ # Catalog: AZZ99 — plug-in regression test no-op gate
33
+ source "$(dirname "$0")/lib/preamble.sh"
34
+ gate_read_input
35
+ gate_pass "no-op test gate discovered via plug-in mechanism"
36
+ EOF
37
+ /usr/bin/chmod +x "$PLUGIN_GATE"
38
+
39
+ # Build a synthetic candidate (won't trigger any real gate findings)
40
+ SYNTH="$TMPDIR/synth-plugin.md"
41
+ /usr/bin/cat > "$SYNTH" <<'EOF'
42
+ ---
43
+ discovered_at: 2026-05-03T00:00:00Z
44
+ repo: example-org/example-repo
45
+ issue_number: 1
46
+ issue_url: https://github.com/example-org/example-repo/issues/1
47
+ star_tier: mainstream
48
+ star_count: 1000
49
+ repo_lang: TypeScript
50
+ competing_prs: 0
51
+ primary_label: bug
52
+ scout_score: 0.5
53
+ status: open
54
+ last_refreshed: 2026-05-03T00:00:00Z
55
+ ---
56
+
57
+ # synthetic — plug-in test
58
+ EOF
59
+
60
+ PASS=0
61
+ FAIL=0
62
+ red() { /usr/bin/printf '\033[31m%s\033[0m' "$1"; }
63
+ green() { /usr/bin/printf '\033[32m%s\033[0m' "$1"; }
64
+
65
+ /usr/bin/printf '\n=== gate-runner plug-in discovery regression ===\n\n'
66
+
67
+ # Run transition with stderr captured (gate output goes to stderr)
68
+ TRANSITION_OUT=$("$SCRIPT_DIR/transition.sh" "shortlist→claimed" "$SYNTH" --dry-run 2>&1 || true)
69
+
70
+ if [[ "$VERBOSE" == "--verbose" ]] ; then
71
+ /usr/bin/printf 'transition output:\n%s\n\n' "$TRANSITION_OUT" >&2
72
+ fi
73
+
74
+ # Test 1: the plug-in gate appears in the output
75
+ /usr/bin/printf ' %-60s ' "1. user-override gate AZZ99 was discovered"
76
+ if /usr/bin/echo "$TRANSITION_OUT" | /usr/bin/grep -q "AZZ99"; then
77
+ green "PASS"; /usr/bin/echo; PASS=$((PASS+1))
78
+ else
79
+ red "FAIL"; /usr/bin/echo; FAIL=$((FAIL+1))
80
+ fi
81
+
82
+ # Test 2: the plug-in gate emitted PASS (proves the gate ran, not just listed)
83
+ /usr/bin/printf ' %-60s ' "2. plug-in gate executed and emitted PASS"
84
+ if /usr/bin/echo "$TRANSITION_OUT" | /usr/bin/grep -qE "AZZ99.*PASS"; then
85
+ green "PASS"; /usr/bin/echo; PASS=$((PASS+1))
86
+ else
87
+ red "FAIL"; /usr/bin/echo; FAIL=$((FAIL+1))
88
+ fi
89
+
90
+ # Test 3: bundled canonical gates still ran (a01 should appear)
91
+ /usr/bin/printf ' %-60s ' "3. bundled canonical gate A01 still ran (dual-dir)"
92
+ if /usr/bin/echo "$TRANSITION_OUT" | /usr/bin/grep -q "A01"; then
93
+ green "PASS"; /usr/bin/echo; PASS=$((PASS+1))
94
+ else
95
+ red "FAIL"; /usr/bin/echo; FAIL=$((FAIL+1))
96
+ fi
97
+
98
+ # Test 4: removing the plug-in gate makes it disappear
99
+ /usr/bin/rm -f "$PLUGIN_GATE"
100
+ TRANSITION_OUT2=$("$SCRIPT_DIR/transition.sh" "shortlist→claimed" "$SYNTH" --dry-run 2>&1 || true)
101
+ /usr/bin/printf ' %-60s ' "4. removing the plug-in gate stops it from running"
102
+ if ! /usr/bin/echo "$TRANSITION_OUT2" | /usr/bin/grep -q "AZZ99"; then
103
+ green "PASS"; /usr/bin/echo; PASS=$((PASS+1))
104
+ else
105
+ red "FAIL"; /usr/bin/echo; FAIL=$((FAIL+1))
106
+ fi
107
+
108
+ /usr/bin/echo
109
+ /usr/bin/printf '=== summary: %s passed · %s failed ===\n\n' \
110
+ "$(green "$PASS")" "$([ "$FAIL" -gt 0 ] && red "$FAIL" || /usr/bin/echo 0)"
111
+
112
+ [[ "$FAIL" -eq 0 ]] || exit 1
113
+ exit 0
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env bash
2
+ # test-scout-refresh.sh — regression test for @scout refresh idempotency.
3
+ #
4
+ # Closes contributing-clanker-bzq.3.
5
+ #
6
+ # Promise of @scout refresh mode:
7
+ # 1. Re-running scout on an existing candidate file UPDATES frontmatter
8
+ # (scout_score, last_seen, momentum_signal) without rewriting the body.
9
+ # 2. The body of the candidate (pet peeves observed, manual notes, draft
10
+ # claim text) is preserved across refreshes.
11
+ # 3. status: never moves backward — a `claimed` candidate doesn't get
12
+ # reverted to `open` by a refresh.
13
+ #
14
+ # Why this matters: if refresh clobbers manual body content, every dossier
15
+ # enrichment Jeremy does manually gets lost on the next scout run. Hard
16
+ # constraint per the @scout spec.
17
+ #
18
+ # Test approach: synthesize a candidate file with manual body content + a
19
+ # "claimed" status, run the equivalent of refresh against it, assert
20
+ # preservation.
21
+ #
22
+ # Usage: test-scout-refresh.sh [--verbose]
23
+ # Exit 0: all assertions hold. Exit 1: any failure.
24
+
25
+ set -uo pipefail
26
+
27
+ VERBOSE="${1:-}"
28
+ TMPDIR=$(/usr/bin/mktemp -d)
29
+ trap 'rm -rf "$TMPDIR"' EXIT
30
+
31
+ PASS=0
32
+ FAIL=0
33
+ red() { /usr/bin/printf '\033[31m%s\033[0m' "$1"; }
34
+ green() { /usr/bin/printf '\033[32m%s\033[0m' "$1"; }
35
+
36
+ assert() {
37
+ local name="$1" expr="$2"
38
+ /usr/bin/printf ' %-60s ' "$name"
39
+ if eval "$expr" >/dev/null 2>&1; then
40
+ /usr/bin/printf '%s\n' "$(green PASS)"
41
+ PASS=$(( PASS + 1 ))
42
+ else
43
+ /usr/bin/printf '%s\n' "$(red FAIL)"
44
+ FAIL=$(( FAIL + 1 ))
45
+ if [[ "$VERBOSE" == "--verbose" ]]; then
46
+ eval "$expr"
47
+ fi
48
+ fi
49
+ }
50
+
51
+ # Synthesize an existing candidate with manual body content
52
+ CANDIDATE="$TMPDIR/example__repo__issue42.md"
53
+ /usr/bin/cat > "$CANDIDATE" <<'EOF'
54
+ ---
55
+ status: claimed
56
+ repo: example/repo
57
+ issue_number: 42
58
+ scout_score: 0.65
59
+ last_seen: 2026-04-15T00:00:00Z
60
+ research_path: ~/.contribute-system/research/example__repo.md
61
+ overrides: []
62
+ ---
63
+
64
+ # Issue #42 — Add bulk export feature
65
+
66
+ ## Manual notes (engineer-curated, must survive refresh)
67
+ - Maintainer @alice prefers small PRs (<200 LOC)
68
+ - Has CLA via dev.intentsolutions.io/cla
69
+ - Follow-up planned: add CSV export after JSON export ships
70
+
71
+ ## Draft claim comment
72
+ I'd like to take this. I've reviewed CONTRIBUTING.md and will follow the
73
+ small-PR convention noted by @alice. Plan: JSON export in PR1, CSV in PR2.
74
+
75
+ ## Pet peeves observed for this repo
76
+ - Don't @-mention @alice on weekends
77
+ - Run `make precommit` before pushing — repo CI is slow
78
+ EOF
79
+
80
+ # Snapshot original body (everything after the second `---`)
81
+ ORIG_BODY=$(/usr/bin/awk '/^---$/{c++; next} c>=2' "$CANDIDATE")
82
+
83
+ # Simulate a refresh: update only frontmatter fields scout would write
84
+ # (scout_score, last_seen, momentum_signal). This is what the real
85
+ # scout-refresh logic should do — never touch body, never regress status.
86
+ /usr/bin/awk '
87
+ BEGIN { in_fm = 0; fm_count = 0 }
88
+ /^---$/ {
89
+ fm_count++
90
+ in_fm = (fm_count == 1)
91
+ print
92
+ if (fm_count == 2 && !momentum_added) {
93
+ # closing frontmatter — too late, never mind
94
+ }
95
+ next
96
+ }
97
+ in_fm && /^scout_score:/ { print "scout_score: 0.78"; next }
98
+ in_fm && /^last_seen:/ { print "last_seen: 2026-05-03T18:00:00Z"; next }
99
+ in_fm && /^status:/ {
100
+ # Status never goes backward. Verify the synthesized status is preserved.
101
+ print
102
+ next
103
+ }
104
+ { print }
105
+ ' "$CANDIDATE" > "$CANDIDATE.refreshed"
106
+
107
+ # Add a new frontmatter field (momentum_signal) — simulates a real scout
108
+ # enhancement that should land at the end of frontmatter, before the second ---
109
+ /usr/bin/awk '
110
+ BEGIN { fm_count = 0 }
111
+ /^---$/ {
112
+ fm_count++
113
+ if (fm_count == 2) {
114
+ print "momentum_signal: rising"
115
+ }
116
+ print
117
+ next
118
+ }
119
+ { print }
120
+ ' "$CANDIDATE.refreshed" > "$CANDIDATE"
121
+
122
+ # Assertions
123
+ assert "candidate file still exists after refresh" "[[ -f \"$CANDIDATE\" ]]"
124
+
125
+ assert "scout_score updated to 0.78" \
126
+ "/usr/bin/grep -q '^scout_score: 0.78' \"$CANDIDATE\""
127
+
128
+ assert "last_seen updated to 2026-05-03T18:00:00Z" \
129
+ "/usr/bin/grep -q '^last_seen: 2026-05-03T18:00:00Z' \"$CANDIDATE\""
130
+
131
+ assert "momentum_signal field added" \
132
+ "/usr/bin/grep -q '^momentum_signal: rising' \"$CANDIDATE\""
133
+
134
+ assert "status: claimed preserved (never regresses to open)" \
135
+ "/usr/bin/grep -q '^status: claimed' \"$CANDIDATE\""
136
+
137
+ assert "research_path preserved" \
138
+ "/usr/bin/grep -q '^research_path:' \"$CANDIDATE\""
139
+
140
+ NEW_BODY=$(/usr/bin/awk '/^---$/{c++; next} c>=2' "$CANDIDATE")
141
+ assert "manual body content preserved verbatim" \
142
+ "[[ \"\$NEW_BODY\" == \"\$ORIG_BODY\" ]]"
143
+
144
+ assert "manual notes section preserved" \
145
+ "/usr/bin/grep -q 'Maintainer @alice prefers small PRs' \"$CANDIDATE\""
146
+
147
+ assert "draft claim comment preserved" \
148
+ "/usr/bin/grep -q 'JSON export in PR1, CSV in PR2' \"$CANDIDATE\""
149
+
150
+ assert "pet peeves section preserved" \
151
+ "/usr/bin/grep -q \"Don't @-mention @alice on weekends\" \"$CANDIDATE\""
152
+
153
+ # Summary
154
+ /usr/bin/printf '\n scout-refresh: %s passed, %s failed\n\n' \
155
+ "$(green "$PASS")" "$([[ $FAIL -eq 0 ]] && green 0 || red "$FAIL")"
156
+
157
+ [[ $FAIL -eq 0 ]] && exit 0 || exit 1
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env bash
2
+ # test-stale-dossier-refresh.sh — regression test for dossier freshness signals.
3
+ #
4
+ # Promise: researcher-build.sh emits a `last_refreshed:` ISO-8601 timestamp
5
+ # matching "now" (within 60 seconds). The 14-day staleness check is a
6
+ # downstream consumer concern (SKILL.md Step 0.5 + dossier_age helpers);
7
+ # this test validates the BUILDER side of the contract.
8
+ #
9
+ # Note: the actual "auto-refresh at 14 days" trigger logic lives in
10
+ # /contribute SKILL.md as agent instructions (not executable code). What
11
+ # we CAN test deterministically:
12
+ # 1. researcher-build.sh writes last_refreshed: to current time
13
+ # 2. an old timestamp is detectable as stale by simple shell math
14
+ # 3. the dossier path slug (owner__repo) matches what gates expect
15
+ #
16
+ # Usage: test-stale-dossier-refresh.sh [--verbose]
17
+ # Exit 0: all assertions hold. Exit 1: any failure.
18
+
19
+ set -uo pipefail
20
+
21
+ VERBOSE="${1:-}"
22
+ SYS="$HOME/.contribute-system"
23
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
24
+ TMPDIR=$(/usr/bin/mktemp -d)
25
+ trap 'rm -rf "$TMPDIR"' EXIT
26
+
27
+ PASS=0
28
+ FAIL=0
29
+ red() { /usr/bin/printf '\033[31m%s\033[0m' "$1"; }
30
+ green() { /usr/bin/printf '\033[32m%s\033[0m' "$1"; }
31
+
32
+ assert() {
33
+ local name="$1" expr="$2"
34
+ /usr/bin/printf ' %-60s ' "$name"
35
+ if eval "$expr" >/dev/null 2>&1; then
36
+ green "PASS"; /usr/bin/echo
37
+ PASS=$((PASS + 1))
38
+ else
39
+ red "FAIL"; /usr/bin/echo
40
+ [[ "$VERBOSE" == "--verbose" ]] && /usr/bin/printf ' expr: %s\n' "$expr" >&2
41
+ FAIL=$((FAIL + 1))
42
+ fi
43
+ }
44
+
45
+ /usr/bin/printf '\n=== dossier freshness regression ===\n\n'
46
+
47
+ # --- TEST 1: researcher-build emits last_refreshed: matching now ---
48
+ DOSSIER="$TMPDIR/lingdojo__kana-dojo.md"
49
+ "$SCRIPT_DIR/researcher-build.sh" lingdojo/kana-dojo --no-link-follow > "$DOSSIER" 2>/dev/null
50
+
51
+ LAST_REFRESHED=$(/usr/bin/awk '/^last_refreshed:/{print $2; exit}' "$DOSSIER" 2>/dev/null)
52
+ NOW_EPOCH=$(/usr/bin/date +%s)
53
+ THEN_EPOCH=$(/usr/bin/date -d "$LAST_REFRESHED" +%s 2>/dev/null || /usr/bin/echo 0)
54
+ DELTA=$((NOW_EPOCH - THEN_EPOCH))
55
+
56
+ assert "1. dossier has last_refreshed frontmatter field" \
57
+ '[[ -n "$LAST_REFRESHED" ]]'
58
+
59
+ assert "2. last_refreshed is within 60 seconds of now" \
60
+ '[[ "$DELTA" -ge 0 && "$DELTA" -le 60 ]]'
61
+
62
+ # --- TEST 2: stale detection math works (synthetic 30-day-old timestamp) ---
63
+ THIRTY_DAYS_AGO=$(/usr/bin/date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ)
64
+ THIRTY_EPOCH=$(/usr/bin/date -d "$THIRTY_DAYS_AGO" +%s)
65
+ AGE_DAYS=$(( (NOW_EPOCH - THIRTY_EPOCH) / 86400 ))
66
+
67
+ assert "3. shell-math correctly identifies 30d-old as stale (>14d)" \
68
+ '[[ "$AGE_DAYS" -gt 14 ]]'
69
+
70
+ # --- TEST 3: dossier filename slug uses double underscore (matches gate expectations) ---
71
+ EXPECTED_SLUG="lingdojo__kana-dojo"
72
+ ACTUAL_SLUG=$(/usr/bin/basename "$DOSSIER" .md)
73
+
74
+ assert "4. dossier filename slug matches owner__repo convention" \
75
+ '[[ "$ACTUAL_SLUG" == "$EXPECTED_SLUG" ]]'
76
+
77
+ # --- TEST 4: the dossier has the manual sections that survive refresh ---
78
+ assert "5. dossier has Pet peeves section (manual, append-only)" \
79
+ '/usr/bin/grep -q "^## Pet peeves" "$DOSSIER"'
80
+
81
+ assert "6. dossier has Failure log section (manual, append-only)" \
82
+ '/usr/bin/grep -q "^## Failure log" "$DOSSIER"'
83
+
84
+ assert "7. dossier has Notes section (manual, append-only)" \
85
+ '/usr/bin/grep -q "^## Notes" "$DOSSIER"'
86
+
87
+ # --- TEST 5: dossier has issue_templates field (added 2026-05-03) ---
88
+ assert "8. dossier has issue_templates frontmatter (per skill-creator)" \
89
+ '/usr/bin/grep -q "^issue_templates:" "$DOSSIER"'
90
+
91
+ /usr/bin/echo
92
+ /usr/bin/printf '=== summary: %s passed · %s failed ===\n\n' \
93
+ "$(green "$PASS")" "$([ "$FAIL" -gt 0 ] && red "$FAIL" || /usr/bin/echo 0)"
94
+
95
+ [[ "$FAIL" -eq 0 ]] || exit 1
96
+ exit 0