forgedock 1.27.0 → 1.29.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.
- package/README.md +4 -1
- package/bin/bootstrap.mjs +55 -0
- package/bin/engine/outcome-codes.mjs +39 -0
- package/bin/engine/telemetry-seam.mjs +37 -8
- package/bin/engine/telemetry-seam.test.mjs +31 -0
- package/bin/engine-cli.mjs +601 -42
- package/bin/engine.mjs +8 -4
- package/bin/forgedock.mjs +75 -9
- package/bin/gc.mjs +580 -0
- package/bin/lean/admission.mjs +33 -3
- package/bin/lean/batch.mjs +22 -4
- package/bin/lean/batch.test.mjs +5 -3
- package/bin/lean/checks.mjs +199 -0
- package/bin/lean/checks.test.mjs +183 -0
- package/bin/lean/conformance.mjs +234 -57
- package/bin/lean/conformance.test.mjs +168 -7
- package/bin/lean/context.mjs +333 -0
- package/bin/lean/context.test.mjs +279 -0
- package/bin/lean/credentials.mjs +29 -0
- package/bin/lean/depcache.mjs +312 -0
- package/bin/lean/depcache.test.mjs +298 -0
- package/bin/lean/drift.mjs +198 -0
- package/bin/lean/drift.test.mjs +223 -0
- package/bin/lean/effects.mjs +104 -9
- package/bin/lean/execution.e2e.test.mjs +81 -0
- package/bin/lean/execution.mjs +431 -0
- package/bin/lean/execution.test.mjs +583 -0
- package/bin/lean/heavy.mjs +289 -0
- package/bin/lean/heavy.test.mjs +207 -0
- package/bin/lean/lean.mjs +723 -154
- package/bin/lean/lean.test.mjs +1603 -35
- package/bin/lean/ledger.mjs +27 -2
- package/bin/lean/ledger.test.mjs +36 -1
- package/bin/lean/limits.mjs +58 -0
- package/bin/lean/limits.test.mjs +150 -0
- package/bin/lean/orchestrate.mjs +389 -19
- package/bin/lean/prefilter.mjs +2 -2
- package/bin/lean/prefilter.test.mjs +3 -0
- package/bin/lean/prompts.mjs +106 -17
- package/bin/lean/recovery.test.mjs +230 -0
- package/bin/lean/refactor.mjs +276 -0
- package/bin/lean/refactor.test.mjs +359 -0
- package/bin/lean/render.mjs +49 -10
- package/bin/lean/review-trigger.mjs +134 -0
- package/bin/lean/review-trigger.test.mjs +104 -0
- package/bin/lean/sandbox-provider.contract.test.mjs +257 -0
- package/bin/lean/sandbox-remote.mjs +142 -0
- package/bin/lean/sched.mjs +221 -0
- package/bin/lean/sched.test.mjs +310 -0
- package/bin/lean/schedd.mjs +247 -0
- package/bin/lean/schedd.test.mjs +180 -0
- package/bin/lean/testmap.mjs +84 -0
- package/bin/lean/testmap.test.mjs +58 -0
- package/bin/lean/toolchain.mjs +228 -0
- package/bin/lean/toolchain.test.mjs +154 -0
- package/bin/lean/verify.mjs +66 -10
- package/bin/lean/verify.test.mjs +104 -3
- package/bin/lean/worktree.mjs +69 -0
- package/bin/lean/worktree.test.mjs +141 -0
- package/bin/runner.mjs +53 -10
- package/bin/stats/github-source.mjs +1 -1
- package/bin/stats/index.mjs +24 -2
- package/bin/stats/local-source.mjs +13 -0
- package/bin/stats/query.mjs +99 -0
- package/bin/stats/stats.test.mjs +94 -1
- package/commands/audit.md +2 -0
- package/commands/autopilot.md +2 -0
- package/commands/bootstrap.md +35 -0
- package/commands/cleanup.md +7 -22
- package/commands/forge-stats.md +2 -1
- package/commands/forgedock-init.md +7 -0
- package/commands/issue.md +89 -830
- package/commands/milestone.md +55 -6
- package/commands/orchestrate.md +62 -8
- package/commands/upgrade-deps.md +3 -3
- package/commands/work-on/build/validate.md +4 -0
- package/commands/work-on/close.md +2 -0
- package/commands/work-on/decompose.md +24 -6
- package/docs/CI.md +3 -3
- package/docs/CONFIG.md +192 -2
- package/docs/OUTCOME-CODES.md +22 -0
- package/docs/RELEASE-NOTES.md +9 -0
- package/docs/TELEMETRY.md +1 -1
- package/docs/site/command-reference.md +22 -0
- package/docs/site/getting-started.md +2 -0
- package/docs/site/start-a-new-project.md +327 -0
- package/package.json +1 -1
- package/scripts/affected-files-check.sh +91 -0
- package/scripts/affected-files-check.test.sh +80 -0
- package/scripts/bootstrap-create.mjs +393 -0
- package/scripts/bootstrap-validate.mjs +36 -2
- package/scripts/calibration.mjs +116 -0
- package/scripts/check-devdocs-freshness.sh +9 -4
- package/scripts/check-devdocs-freshness.test.sh +11 -1
- package/scripts/check-spec-markers.sh +4 -0
- package/scripts/check-spec-size.sh +29 -11
- package/scripts/check-spec-size.test.sh +27 -4
- package/scripts/ci-local/lib.mjs +8 -1
- package/scripts/ci-local/parallelism.test.mjs +27 -0
- package/scripts/ci-local/run.mjs +3 -3
- package/scripts/ci-local/scoping.mjs +82 -0
- package/scripts/ci-local.test.sh +1 -1
- package/scripts/fixtures/symbol-index/other/sample.rb +3 -0
- package/scripts/fixtures/symbol-index/py/pkg/__init__.py +1 -0
- package/scripts/fixtures/symbol-index/py/pkg/a.py +5 -0
- package/scripts/fixtures/symbol-index/py/pkg/b.py +14 -0
- package/scripts/fixtures/symbol-index/py/pkg/c.py +1 -0
- package/scripts/fixtures/symbol-index/py/sample.py +41 -0
- package/scripts/fixtures/symbol-index/ts/sample.ts +13 -0
- package/scripts/fixtures/symbol-move/gen_big.py +201 -0
- package/scripts/fixtures/symbol-move-ts/gen_big.mjs +56 -0
- package/scripts/fixtures/test-map/go/.github/workflows/ci.yml +23 -0
- package/scripts/fixtures/test-map/go/Makefile +5 -0
- package/scripts/fixtures/test-map/go/README.md +1 -0
- package/scripts/fixtures/test-map/go/go.mod +3 -0
- package/scripts/fixtures/test-map/go/internal/util/util.go +3 -0
- package/scripts/fixtures/test-map/go/main.go +3 -0
- package/scripts/fixtures/test-map/node/package.json +10 -0
- package/scripts/fixtures/test-map/node/packages/ui/package.json +4 -0
- package/scripts/fixtures/test-map/node/packages/ui/src/button.tsx +1 -0
- package/scripts/fixtures/test-map/node/pnpm-lock.yaml +1 -0
- package/scripts/fixtures/test-map/node/src/a.ts +1 -0
- package/scripts/fixtures/test-map/node/vitest.config.ts +1 -0
- package/scripts/fixtures/test-map/python/pkg/__init__.py +0 -0
- package/scripts/fixtures/test-map/python/pkg/mod.py +2 -0
- package/scripts/fixtures/test-map/python/pyproject.toml +9 -0
- package/scripts/fixtures/test-map/python/tests/test_mod.py +5 -0
- package/scripts/fixtures/test-map/python/uv.lock +1 -0
- package/scripts/forge-root.test.sh +1 -1
- package/scripts/hotspots.sh +100 -0
- package/scripts/hotspots.test.sh +101 -0
- package/scripts/issue-create.sh +197 -0
- package/scripts/issue-create.test.sh +166 -0
- package/scripts/maintenance-scan.sh +185 -0
- package/scripts/maintenance-scan.test.sh +89 -0
- package/scripts/milestone-drift.sh +206 -0
- package/scripts/milestone-drift.test.sh +120 -0
- package/scripts/refactor-verify.sh +368 -0
- package/scripts/refactor-verify.test.sh +202 -0
- package/scripts/repo-facts.sh +146 -0
- package/scripts/repo-facts.test.sh +106 -0
- package/scripts/structure-delta.sh +10 -5
- package/scripts/structure-delta.test.sh +3 -2
- package/scripts/symbol-index.mjs +253 -0
- package/scripts/symbol-index.py +354 -0
- package/scripts/symbol-index.test.sh +119 -0
- package/scripts/symbol-move-ts.test.sh +161 -0
- package/scripts/symbol-move.mjs +379 -0
- package/scripts/symbol-move.py +383 -0
- package/scripts/symbol-move.test.sh +168 -0
- package/scripts/test-map.sh +211 -0
- package/scripts/test-map.test.sh +165 -0
- package/scripts/wait-ci-green.sh +10 -4
- package/scripts/wait-ci-green.test.sh +9 -0
- package/templates/bootstrap/interview.md +34 -0
- package/templates/bootstrap/issue-body.md +42 -0
- package/templates/bootstrap/needs-human-issue.md +45 -0
- package/templates/bootstrap/stack-profiles.md +34 -0
- package/templates/bootstrap/technical-spike.md +75 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# issue-create.test.sh — cases for scripts/issue-create.sh (forge#4164). Hermetic: mock gh and yq on PATH, the real
|
|
3
|
+
# scripts/issue-dedup.sh beside the script under test, no network. Bash 3.2 compatible.
|
|
4
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
S="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/issue-create.sh"
|
|
8
|
+
PASS=0; FAILN=0
|
|
9
|
+
TMP="$(mktemp -d "${TMPDIR:-/tmp}/issue-create-test.XXXXXX")"; trap 'rm -rf "$TMP"' EXIT
|
|
10
|
+
ok() { PASS=$((PASS+1)); }
|
|
11
|
+
bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
|
|
12
|
+
mkdir -p "$TMP/bin" "$TMP/work/scripts" "$TMP/state"
|
|
13
|
+
|
|
14
|
+
# Mock gh. Logs every call to $TMP/state/log (one line per call, args joined by |).
|
|
15
|
+
# issue list -> $MOCK_CANDIDATES (tsv "number\ttitle") when set
|
|
16
|
+
# api .../issues POST -> stores the body field, answers {"number":901,...}; MOCK_POST_FAIL=1 fails, MOCK_NO_NUMBER=1 omits it
|
|
17
|
+
# issue view --json body-> stored body; MOCK_READBACK=no-token|no-marker|missing-sections alters it
|
|
18
|
+
# issue edit / project -> logged, exit 0
|
|
19
|
+
cat > "$TMP/bin/gh" <<'MOCK'
|
|
20
|
+
#!/usr/bin/env bash
|
|
21
|
+
ST="$MOCK_STATE"
|
|
22
|
+
line=""; for a in "$@"; do line="$line|$a"; done; printf '%s\n' "$line" >> "$ST/log"
|
|
23
|
+
if [ "$1" = issue ] && [ "$2" = list ]; then
|
|
24
|
+
[ -n "${MOCK_CANDIDATES:-}" ] && cat "$MOCK_CANDIDATES"; exit 0
|
|
25
|
+
fi
|
|
26
|
+
if [ "$1" = api ]; then
|
|
27
|
+
[ "${MOCK_POST_FAIL:-}" = 1 ] && { echo "HTTP 403: secondary rate limit" >&2; exit 1; }
|
|
28
|
+
prev=""; for a in "$@"; do
|
|
29
|
+
if [ "$prev" = "-f" ]; then case "$a" in body=*) printf '%s' "${a#body=}" > "$ST/body" ;; esac; fi
|
|
30
|
+
prev="$a"
|
|
31
|
+
done
|
|
32
|
+
if [ "${MOCK_NO_NUMBER:-}" = 1 ]; then echo '{}'; else echo '{"number":901,"html_url":"https://github.com/o/r/issues/901"}'; fi
|
|
33
|
+
exit 0
|
|
34
|
+
fi
|
|
35
|
+
if [ "$1" = issue ] && [ "$2" = view ]; then
|
|
36
|
+
b=$(cat "$ST/body")
|
|
37
|
+
case "${MOCK_READBACK:-}" in
|
|
38
|
+
no-token) b=$(printf '%s\n' "$b" | grep -v 'issue-create-token:') ;;
|
|
39
|
+
no-marker) b=$(printf '%s\n' "$b" | grep -v 'FORGE:BODY-INTEGRITY') ;;
|
|
40
|
+
missing-sections) b=$(printf '%s\n' "$b" | grep -v '^## Problem') ;;
|
|
41
|
+
esac
|
|
42
|
+
printf '%s\n' "$b"; exit 0
|
|
43
|
+
fi
|
|
44
|
+
exit 0
|
|
45
|
+
MOCK
|
|
46
|
+
# Mock yq: answers the three expressions issue-create.sh asks of forge.yaml.
|
|
47
|
+
cat > "$TMP/bin/yq" <<'MOCK'
|
|
48
|
+
#!/usr/bin/env bash
|
|
49
|
+
case "$1" in
|
|
50
|
+
*project.repo*) echo "cfg-owner/cfg-repo" ;;
|
|
51
|
+
*project_number*) echo "${MOCK_PROJECT:-}" ;;
|
|
52
|
+
*project_board.owner*) echo "cfg-owner" ;;
|
|
53
|
+
esac
|
|
54
|
+
MOCK
|
|
55
|
+
chmod +x "$TMP/bin/gh" "$TMP/bin/yq"
|
|
56
|
+
export PATH="$TMP/bin:$PATH" MOCK_STATE="$TMP/state"
|
|
57
|
+
|
|
58
|
+
GOOD="$TMP/body.md"
|
|
59
|
+
printf '## Problem\n\np\n\n## Affected Files\n\n- a\n\n## Acceptance Criteria\n\n- [ ] x\n<!-- FORGE:BODY-INTEGRITY:42_review_t1 -->\n' > "$GOOD"
|
|
60
|
+
|
|
61
|
+
run() { # name want_rc args... (cwd = $TMP/work, which has no forge.yaml unless a case writes one)
|
|
62
|
+
local name="$1" want="$2"; shift 2
|
|
63
|
+
: > "$TMP/state/log"; rm -f "$TMP/state/body"
|
|
64
|
+
(cd "$TMP/work" && bash "$S" "$@") >"$TMP/out" 2>"$TMP/err"
|
|
65
|
+
local rc=$?
|
|
66
|
+
if [ "$rc" = "$want" ]; then ok; else bad "$name (rc=$rc want $want)"; sed 's/^/ err: /' "$TMP/err"; fi
|
|
67
|
+
}
|
|
68
|
+
out_has() { grep -qF -- "$2" "$TMP/out" && ok || bad "$1 (stdout lacks: $2)"; }
|
|
69
|
+
log_has() { grep -qF -- "$2" "$TMP/state/log" && ok || bad "$1 (gh log lacks: $2)"; }
|
|
70
|
+
log_lacks() { grep -qF -- "$2" "$TMP/state/log" && bad "$1 (gh log has: $2)" || ok; }
|
|
71
|
+
one_result() { [ "$(grep -c '^ISSUE_CREATE_RESULT:' "$TMP/out")" = 1 ] && ok || bad "$1 (not exactly one result line)"; }
|
|
72
|
+
|
|
73
|
+
# --- created ----------------------------------------------------------------
|
|
74
|
+
run "create exits 0" 0 --repo o/r --title "fix: billing crash" --body-file "$GOOD" --label bug --label "priority:P2"
|
|
75
|
+
out_has "CREATED line" "ISSUE_CREATE_RESULT:CREATED number=901 url=https://github.com/o/r/issues/901"
|
|
76
|
+
one_result "create prints one result line"
|
|
77
|
+
log_has "REST create, not gh issue create" "|api|repos/o/r/issues|--method|POST|"
|
|
78
|
+
log_lacks "never gh issue create" "|issue|create|"
|
|
79
|
+
log_has "labels are distinct fields" "|-f|labels[]=bug|-f|labels[]=priority:P2"
|
|
80
|
+
grep -q 'issue-create-token:forge-create-' "$TMP/state/body" && ok || bad "create token appended to the body"
|
|
81
|
+
log_lacks "no milestone edit when none given" "--milestone"
|
|
82
|
+
|
|
83
|
+
run "argv --body works" 0 --repo o/r --title "fix: a" --body "## Problem
|
|
84
|
+
x"
|
|
85
|
+
grep -q '^## Affected Files' "$TMP/state/body" && grep -q '^## Acceptance Criteria' "$TMP/state/body" && ok || bad "missing sections repaired before POST"
|
|
86
|
+
grep -q '^## Problem' "$TMP/state/body" && [ "$(grep -c '^## Problem' "$TMP/state/body")" = 1 ] && ok || bad "present section not duplicated"
|
|
87
|
+
|
|
88
|
+
run "milestone edit" 0 --repo o/r --title "fix: a" --body-file "$GOOD" --milestone "v2"
|
|
89
|
+
log_has "milestone set on the new issue" "|issue|edit|901|-R|o/r|--milestone|v2"
|
|
90
|
+
|
|
91
|
+
# --- --args: the raw $ARGUMENTS string from the skill --------------------------
|
|
92
|
+
run "--args quoted title" 0 --repo o/r --args "--title \"fix: billing page crash\" --body-file \"$GOOD\" --label bug"
|
|
93
|
+
log_has "multi-word title is one token" "|-f|title=fix: billing page crash|"
|
|
94
|
+
rm -f "$TMP/work/pwned"
|
|
95
|
+
run "--args never executes" 0 --repo o/r --args "--title 'fix: \$(touch pwned) \`touch pwned\`; touch pwned' --body-file \"$GOOD\""
|
|
96
|
+
[ -e "$TMP/work/pwned" ] && bad "command substitution in --args was executed" || ok
|
|
97
|
+
log_has "metacharacters kept literally" 'title=fix: $(touch pwned) `touch pwned`; touch pwned'
|
|
98
|
+
run "unbalanced quote is a usage error" 2 --repo o/r --args "--title \"fix: oops --body x"
|
|
99
|
+
out_has "usage result" "ISSUE_CREATE_RESULT:USAGE"
|
|
100
|
+
|
|
101
|
+
# --- repo from forge.yaml ---------------------------------------------------------
|
|
102
|
+
: > "$TMP/work/forge.yaml"
|
|
103
|
+
run "repo from forge.yaml" 0 --title "fix: a" --body-file "$GOOD"
|
|
104
|
+
log_has "config repo used" "|api|repos/cfg-owner/cfg-repo/issues|"
|
|
105
|
+
MOCK_PROJECT=7 run "project board add" 0 --title "fix: a" --body-file "$GOOD"
|
|
106
|
+
log_has "issue added to the board" "|project|item-add|7|--owner|cfg-owner|--url|https://github.com/o/r/issues/901"
|
|
107
|
+
rm -f "$TMP/work/forge.yaml"
|
|
108
|
+
run "no repo is a usage error" 2 --title "fix: a" --body-file "$GOOD"
|
|
109
|
+
|
|
110
|
+
# --- greenfield flags (forge#4172) are accepted, not unrecognized ---------------------
|
|
111
|
+
run "--greenfield/--spike accepted" 0 --repo o/r --args "--greenfield --spike docs/spike.md --title \"fix: a\" --body-file \"$GOOD\""
|
|
112
|
+
run "--spike without a value" 2 --repo o/r --title "fix: a" --body-file "$GOOD" --spike
|
|
113
|
+
|
|
114
|
+
# --- usage errors: nothing created ---------------------------------------------
|
|
115
|
+
run "--title without a value" 2 --repo o/r --body-file "$GOOD" --title
|
|
116
|
+
log_lacks "no POST on usage error" "|api|"
|
|
117
|
+
run "unrecognized flag" 2 --repo o/r --titel "fix: a" --title "fix: a" --body-file "$GOOD"
|
|
118
|
+
run "both bodies" 2 --repo o/r --title "fix: a" --body "x" --body-file "$GOOD"
|
|
119
|
+
run "neither body" 2 --repo o/r --title "fix: a"
|
|
120
|
+
run "unreadable body file" 2 --repo o/r --title "fix: a" --body-file "$TMP/nope.md"
|
|
121
|
+
log_lacks "no gh call before body checks pass" "|"
|
|
122
|
+
printf '## Problem\nno marker\n' > "$TMP/nomark.md"
|
|
123
|
+
run "body file without marker" 2 --repo o/r --title "fix: a" --body-file "$TMP/nomark.md"
|
|
124
|
+
printf '<!-- FORGE:BODY-INTEGRITY:a_b_c -->\n<!-- FORGE:BODY-INTEGRITY:d_e_f -->\n' > "$TMP/twomark.md"
|
|
125
|
+
run "body file with two markers" 2 --repo o/r --title "fix: a" --body-file "$TMP/twomark.md"
|
|
126
|
+
run "free text is not handled here" 2 --repo o/r --args "the billing page crashes when using --verbose"
|
|
127
|
+
run "no title" 2 --repo o/r --body-file "$GOOD" --label bug
|
|
128
|
+
|
|
129
|
+
# --- dedup STOP (real issue-dedup.sh) -------------------------------------------
|
|
130
|
+
printf '777\tfix: billing crash when credits reach zero\n' > "$TMP/cands.tsv"
|
|
131
|
+
MOCK_CANDIDATES="$TMP/cands.tsv" run "dedup STOP exits 1" 1 --repo o/r --title "fix: billing crash when credits reach zero" --body-file "$GOOD"
|
|
132
|
+
out_has "DEDUP line" "ISSUE_CREATE_RESULT:DEDUP number=777"
|
|
133
|
+
one_result "dedup prints one result line"
|
|
134
|
+
log_lacks "no POST after dedup STOP" "|api|"
|
|
135
|
+
MOCK_CANDIDATES="$TMP/cands.tsv" run "dedup STOP holds under --dry-run" 1 --repo o/r --dry-run --title "fix: billing crash when credits reach zero" --body-file "$GOOD"
|
|
136
|
+
MOCK_CANDIDATES="$TMP/cands.tsv" run "--exclude narrows the candidate set" 0 --repo o/r --exclude 777 --title "fix: billing crash when credits reach zero" --body-file "$GOOD"
|
|
137
|
+
out_has "excluded duplicate creates" "ISSUE_CREATE_RESULT:CREATED number=901"
|
|
138
|
+
MOCK_CANDIDATES="$TMP/cands.tsv" run "unrelated issue still dedups with --exclude" 1 --repo o/r --exclude 12 --title "fix: billing crash when credits reach zero" --body-file "$GOOD"
|
|
139
|
+
run "dedup usage error exits 2" 2 --repo o/r --title "-x" --body-file "$GOOD"
|
|
140
|
+
log_lacks "no POST after dedup usage error" "|api|"
|
|
141
|
+
|
|
142
|
+
# --- dry run ----------------------------------------------------------------------
|
|
143
|
+
run "dry run" 0 --repo o/r --dry-run --title "fix: a" --body-file "$GOOD" --milestone v2
|
|
144
|
+
out_has "dry-run result" "ISSUE_CREATE_RESULT:DRY_RUN"
|
|
145
|
+
log_lacks "dry run makes no POST" "|api|"
|
|
146
|
+
log_lacks "dry run makes no edit" "|issue|edit|"
|
|
147
|
+
grep -qF "Issue Draft (--dry-run" "$TMP/err" && ok || bad "dry run prints the draft"
|
|
148
|
+
|
|
149
|
+
# --- create failures and read-back gates -------------------------------------------
|
|
150
|
+
MOCK_POST_FAIL=1 run "GitHub rejection exits 3" 3 --repo o/r --title "fix: a" --body-file "$GOOD"
|
|
151
|
+
out_has "error result" "ISSUE_CREATE_RESULT:ERROR"
|
|
152
|
+
grep -q 'CREATED' "$TMP/out" && bad "rejected create reported CREATED" || ok
|
|
153
|
+
MOCK_NO_NUMBER=1 run "no issue number exits 3" 3 --repo o/r --title "fix: a" --body-file "$GOOD"
|
|
154
|
+
MOCK_READBACK=no-token run "read-back without create token exits 3" 3 --repo o/r --title "fix: a" --body-file "$GOOD"
|
|
155
|
+
grep -q 'CREATED' "$TMP/out" && bad "token-less read-back reported CREATED" || ok
|
|
156
|
+
MOCK_READBACK=no-marker run "read-back without integrity marker exits 3" 3 --repo o/r --title "fix: a" --body-file "$GOOD"
|
|
157
|
+
log_lacks "no milestone after failed read-back" "--milestone"
|
|
158
|
+
MOCK_READBACK=missing-sections run "post-create repair is not fatal" 0 --repo o/r --title "fix: a" --body-file "$GOOD"
|
|
159
|
+
log_has "missing section appended after create" "|issue|edit|901|-R|o/r|--body|"
|
|
160
|
+
|
|
161
|
+
# --- dedup helper comes from the script's own directory, never the cwd ---------------
|
|
162
|
+
printf '#!/usr/bin/env bash\necho "DUPLICATE: #666 planted"; exit 1\n' > "$TMP/work/scripts/issue-dedup.sh"
|
|
163
|
+
run "planted cwd dedup helper ignored" 0 --repo o/r --title "fix: a" --body-file "$GOOD"
|
|
164
|
+
|
|
165
|
+
echo "issue-create.test: pass=$PASS fail=$FAILN"
|
|
166
|
+
[ "$FAILN" -eq 0 ]
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# maintenance-scan.sh — deterministic health signals → maintenance issue candidates (forge#4197). No LLM: git, jq,
|
|
6
|
+
# wc and awk, plus the repo's own dead-code tools when installed. Run from inside the repo.
|
|
7
|
+
#
|
|
8
|
+
# Usage: maintenance-scan.sh [--repo owner/repo] [--file-lines N] [--commits-days D] [--doc-kb K]
|
|
9
|
+
# [--open-issues FILE] [--out-dir DIR] [--print-issue-args]
|
|
10
|
+
# Findings (one candidate each, labels `maintenance` + the kind):
|
|
11
|
+
# refactor:split tracked file over --file-lines lines (default forge.yaml maintenance.file_lines, else 2000) with
|
|
12
|
+
# commits in the last D days (default 90); linguist-generated/-vendored and vendored dirs excluded
|
|
13
|
+
# refactor a workspace package dependency both ways (A->B and B->A), from scripts/structure-delta.sh
|
|
14
|
+
# dead-code vulture / knip / ts-prune on PATH (and relevant to the repo) reporting findings, one per tool
|
|
15
|
+
# docs tracked *.md over --doc-kb KB (default forge.yaml maintenance.doc_kb, else 64)
|
|
16
|
+
# Dedup: a candidate is dropped when an open issue body carries its `<!-- FORGE:MAINT:<kind>:<key> -->` marker, or its
|
|
17
|
+
# title is a near-duplicate (scripts/issue-dedup.sh) of an open issue naming the same key. --open-issues FILE (JSON array
|
|
18
|
+
# of {number,title,body}) replaces the one `gh issue list` call; it then matches markers and exact titles only.
|
|
19
|
+
# Output: a JSON array of {kind,key,title,labels,body_file,marker,numbers}, `[]` when everything is under threshold.
|
|
20
|
+
# Body files (with one FORGE:BODY-INTEGRITY marker, per /issue's programmatic contract) go to --out-dir (default a
|
|
21
|
+
# mktemp dir under $TMPDIR). --print-issue-args prints one `/issue --title ... --body-file ... --label ...` line per
|
|
22
|
+
# candidate instead, for `Skill(skill="{FORGE_SKILL_PREFIX}issue", args=<line>)`.
|
|
23
|
+
# Exit: 0 ok | 2 usage, not a git repo, or open issues unreadable. bash 3.2 portable.
|
|
24
|
+
set -uo pipefail
|
|
25
|
+
|
|
26
|
+
die() { echo "maintenance-scan: $*" >&2; exit 2; }
|
|
27
|
+
command -v jq >/dev/null 2>&1 || die "jq is required"
|
|
28
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
29
|
+
|
|
30
|
+
REPO="" FILE_LINES="" DAYS=90 DOC_KB="" OPEN_FILE="" OUT_DIR="" PRINT_ARGS=false
|
|
31
|
+
while [ $# -gt 0 ]; do
|
|
32
|
+
case "$1" in
|
|
33
|
+
--repo) REPO="${2:-}"; shift 2 ;;
|
|
34
|
+
--file-lines) FILE_LINES="${2:-}"; shift 2 ;;
|
|
35
|
+
--commits-days) DAYS="${2:-}"; shift 2 ;;
|
|
36
|
+
--doc-kb) DOC_KB="${2:-}"; shift 2 ;;
|
|
37
|
+
--open-issues) OPEN_FILE="${2:-}"; shift 2 ;;
|
|
38
|
+
--out-dir) OUT_DIR="${2:-}"; shift 2 ;;
|
|
39
|
+
--print-issue-args) PRINT_ARGS=true; shift ;;
|
|
40
|
+
-h|--help) sed -n '5,24p' "$0"; exit 0 ;;
|
|
41
|
+
*) die "unknown argument: $1" ;;
|
|
42
|
+
esac
|
|
43
|
+
done
|
|
44
|
+
|
|
45
|
+
ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || die "not inside a git repository"
|
|
46
|
+
cd "$ROOT" || die "cannot cd to $ROOT"
|
|
47
|
+
|
|
48
|
+
# forge.yaml `maintenance:` block (block form only, like bin/forge-utils.mjs parseForgeYaml): direct children only, so
|
|
49
|
+
# a sub-map's keys (` hotspot:` → ` file_lines:`, forge#4195) never stand in for the section's own
|
|
50
|
+
yaml_maint() {
|
|
51
|
+
[ -f forge.yaml ] || return 0
|
|
52
|
+
awk -v k="$1" '
|
|
53
|
+
/^[A-Za-z_][A-Za-z0-9_]*:[[:space:]]*(#.*)?$/ { on = ($0 ~ /^maintenance:/); ci = -1; next }
|
|
54
|
+
/^[^[:space:]#]/ { on = 0 }
|
|
55
|
+
on && /^[[:space:]]*(#|$)/ { next }
|
|
56
|
+
on { match($0, /^[[:space:]]*/); if (ci >= 0 && RLENGTH > ci) next; ci = RLENGTH }
|
|
57
|
+
on && $0 ~ "^[[:space:]]+" k ":" { sub("^[[:space:]]+" k ":[[:space:]]*", ""); sub("[[:space:]]*#.*$", ""); gsub(/"/, ""); print; exit }
|
|
58
|
+
' forge.yaml
|
|
59
|
+
}
|
|
60
|
+
[ -n "$FILE_LINES" ] || FILE_LINES=$(yaml_maint file_lines)
|
|
61
|
+
[ -n "$DOC_KB" ] || DOC_KB=$(yaml_maint doc_kb)
|
|
62
|
+
FILE_LINES=${FILE_LINES:-2000} DOC_KB=${DOC_KB:-64}
|
|
63
|
+
for v in "$FILE_LINES" "$DAYS" "$DOC_KB"; do case "$v" in ''|*[!0-9]*) die "thresholds must be positive integers" ;; esac; done
|
|
64
|
+
|
|
65
|
+
T=$(mktemp -d "${TMPDIR:-/tmp}/maintenance-scan.XXXXXX") || die "mktemp failed"
|
|
66
|
+
trap 'rm -rf "$T"' EXIT
|
|
67
|
+
if [ -z "$OUT_DIR" ]; then OUT_DIR=$(mktemp -d "${TMPDIR:-/tmp}/maintenance-scan-out.XXXXXX") || die "mktemp failed"; fi
|
|
68
|
+
mkdir -p "$OUT_DIR" || die "cannot create $OUT_DIR"
|
|
69
|
+
OUT_DIR=$(cd "$OUT_DIR" && pwd)
|
|
70
|
+
|
|
71
|
+
# ---- open issues, once ----
|
|
72
|
+
if [ -n "$OPEN_FILE" ]; then
|
|
73
|
+
jq -e 'type == "array"' "$OPEN_FILE" >/dev/null 2>&1 || die "--open-issues must be a JSON array"
|
|
74
|
+
cp "$OPEN_FILE" "$T/open.json"
|
|
75
|
+
else
|
|
76
|
+
gh issue list ${REPO:+-R "$REPO"} --state open --limit 1000 --json number,title,body > "$T/open.json" 2>/dev/null || die "cannot list open issues (gh)"
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# ---- tracked files, minus generated/vendored ----
|
|
80
|
+
git ls-files > "$T/all"
|
|
81
|
+
git check-attr --stdin linguist-generated linguist-vendored < "$T/all" \
|
|
82
|
+
| awk -F': ' '$3 == "set" || $3 == "true" { print $1 }' | sort -u > "$T/excluded"
|
|
83
|
+
grep -vE '(^|/)(node_modules|vendor|third_party|dist|build)/' "$T/all" | sort | comm -23 - "$T/excluded" > "$T/files"
|
|
84
|
+
|
|
85
|
+
# candidates: "kind<TAB>key<TAB>title<TAB>numbers-json<TAB>evidence"
|
|
86
|
+
: > "$T/cands"
|
|
87
|
+
add() { printf '%s\t%s\t%s\t%s\t%s\n' "$1" "$2" "$3" "$4" "$5" >> "$T/cands"; }
|
|
88
|
+
|
|
89
|
+
# ---- hotspots: over-threshold files with commits in the window ----
|
|
90
|
+
git log --since="$DAYS days ago" --format= --name-only 2>/dev/null | awk 'NF' | sort | uniq -c \
|
|
91
|
+
| awk '{ n = $1; sub(/^ *[0-9]+ /, ""); print $0 "\t" n }' > "$T/churn"
|
|
92
|
+
tr '\n' '\0' < "$T/files" | xargs -0 wc -l 2>/dev/null \
|
|
93
|
+
| awk -v max="$FILE_LINES" '{ n = $1; p = $0; sub(/^ *[0-9]+ /, "", p); if (n > max && p != "total") print p "\t" n }' > "$T/big"
|
|
94
|
+
while IFS="$(printf '\t')" read -r path lines; do
|
|
95
|
+
[ -f "$path" ] || continue
|
|
96
|
+
grep -Iq . "$path" 2>/dev/null || continue # binary
|
|
97
|
+
commits=$(awk -F'\t' -v p="$path" '$1 == p { print $2; exit }' "$T/churn")
|
|
98
|
+
[ -n "$commits" ] && [ "$commits" -gt 0 ] || continue
|
|
99
|
+
add "refactor:split" "$path" "split $path ($lines loc, $commits commits/${DAYS}d)" \
|
|
100
|
+
"{\"lines\":$lines,\"commits\":$commits,\"days\":$DAYS,\"threshold\":$FILE_LINES}" \
|
|
101
|
+
"\`$path\` has $lines lines (budget $FILE_LINES) and $commits commits in the last $DAYS days."
|
|
102
|
+
done < "$T/big"
|
|
103
|
+
|
|
104
|
+
# ---- backwards edges: workspace packages depending on each other both ways ----
|
|
105
|
+
if git show HEAD:package.json 2>/dev/null | jq -e '.workspaces' >/dev/null 2>&1 || git cat-file -e HEAD:pnpm-workspace.yaml 2>/dev/null; then
|
|
106
|
+
first=$(git rev-list --max-parents=0 HEAD | tail -1)
|
|
107
|
+
if sd=$("$SCRIPT_DIR/structure-delta.sh" "$first" HEAD 2>/dev/null); then
|
|
108
|
+
printf '%s' "$sd" | jq -r '[(.package_edges_added // [])[] | [.from, .to]] | unique as $e
|
|
109
|
+
| $e[] | . as $p | select($p[0] < $p[1] and ($e | index([[$p[1], $p[0]]])) != null) | "\($p[0])\t\($p[1])"' 2>/dev/null \
|
|
110
|
+
| while IFS="$(printf '\t')" read -r a b; do
|
|
111
|
+
add "refactor" "$a->$b" "untangle $a <-> $b (pkg cycle)" "{\"edges\":2}" \
|
|
112
|
+
"Workspace package \`$a\` depends on \`$b\` and \`$b\` depends on \`$a\`."
|
|
113
|
+
done
|
|
114
|
+
fi
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
# ---- dead code: the repo's own tools, only when installed and relevant ----
|
|
118
|
+
count_lines() { awk 'NF' | wc -l | tr -d ' '; }
|
|
119
|
+
dead() { # tool, count
|
|
120
|
+
[ "$2" -gt 0 ] 2>/dev/null || return 0
|
|
121
|
+
add "dead-code" "$1" "remove $2 unused ($1)" "{\"findings\":$2}" "\`$1\` reports $2 unused items."
|
|
122
|
+
}
|
|
123
|
+
if command -v vulture >/dev/null 2>&1 && grep -q '\.py$' "$T/files"; then
|
|
124
|
+
dead vulture "$(grep '\.py$' "$T/files" | tr '\n' '\0' | xargs -0 vulture 2>/dev/null | count_lines)"
|
|
125
|
+
fi
|
|
126
|
+
if [ -f package.json ]; then
|
|
127
|
+
command -v knip >/dev/null 2>&1 && dead knip "$(knip --no-progress --reporter compact 2>/dev/null | count_lines)"
|
|
128
|
+
command -v ts-prune >/dev/null 2>&1 && dead ts-prune "$(ts-prune 2>/dev/null | grep -v '(used in module)' | count_lines)"
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
# ---- oversized docs ----
|
|
132
|
+
grep -E '\.md$' "$T/files" | tr '\n' '\0' | xargs -0 wc -c 2>/dev/null \
|
|
133
|
+
| awk -v max="$((DOC_KB * 1024))" '{ n = $1; p = $0; sub(/^ *[0-9]+ /, "", p); if (n > max && p != "total") print p "\t" n }' \
|
|
134
|
+
| while IFS="$(printf '\t')" read -r path bytes; do
|
|
135
|
+
kb=$(( (bytes + 1023) / 1024 ))
|
|
136
|
+
add "docs" "$path" "trim $path ($kb KB > $DOC_KB KB)" "{\"kb\":$kb,\"threshold_kb\":$DOC_KB}" \
|
|
137
|
+
"\`$path\` is $kb KB; agents load docs under $DOC_KB KB whole."
|
|
138
|
+
done
|
|
139
|
+
|
|
140
|
+
# ---- dedup, body files, output ----
|
|
141
|
+
slug() { printf '%s' "$1" | tr -c 'A-Za-z0-9._-' '_' | cut -c1-80; }
|
|
142
|
+
title_dup() { # title, key: an open near-duplicate title that names the same key
|
|
143
|
+
if [ -n "$OPEN_FILE" ]; then
|
|
144
|
+
jq -e --arg t "$1" 'any(.[]; .title == $t)' "$T/open.json" >/dev/null
|
|
145
|
+
else
|
|
146
|
+
local hit
|
|
147
|
+
hit=$("$SCRIPT_DIR/issue-dedup.sh" "$1" ${REPO:+-R "$REPO"} 2>/dev/null) && return 1
|
|
148
|
+
case "$hit" in *"$2"*) return 0 ;; *) return 1 ;; esac
|
|
149
|
+
fi
|
|
150
|
+
}
|
|
151
|
+
: > "$T/out.jsonl"
|
|
152
|
+
sort -t "$(printf '\t')" -k1,1 -k2,2 -u "$T/cands" | while IFS="$(printf '\t')" read -r kind key title numbers evidence; do
|
|
153
|
+
marker="FORGE:MAINT:$kind:$key"
|
|
154
|
+
jq -e --arg m "<!-- $marker -->" 'any(.[]; (.body // "") | contains($m))' "$T/open.json" >/dev/null && continue
|
|
155
|
+
title_dup "$title" "$key" && continue
|
|
156
|
+
id="maint_$(slug "$kind")_$(printf '%s' "$kind:$key" | cksum | awk '{print $1}')"
|
|
157
|
+
body="$OUT_DIR/${id}_$(slug "${key%.md}").md"
|
|
158
|
+
cat > "$body" <<EOF
|
|
159
|
+
## Problem
|
|
160
|
+
|
|
161
|
+
$evidence Found by \`scripts/maintenance-scan.sh\`; it is dispatched within the maintenance budget (forge.yaml \`maintenance.share\`).
|
|
162
|
+
|
|
163
|
+
## Evidence
|
|
164
|
+
|
|
165
|
+
\`\`\`json
|
|
166
|
+
$numbers
|
|
167
|
+
\`\`\`
|
|
168
|
+
|
|
169
|
+
## Acceptance Criteria
|
|
170
|
+
|
|
171
|
+
- [ ] \`scripts/maintenance-scan.sh\` no longer reports \`$kind\` for \`$key\`
|
|
172
|
+
- [ ] Behaviour is unchanged (existing tests pass)
|
|
173
|
+
|
|
174
|
+
<!-- $marker -->
|
|
175
|
+
<!-- FORGE:BODY-INTEGRITY:$id -->
|
|
176
|
+
EOF
|
|
177
|
+
jq -n -c --arg kind "$kind" --arg key "$key" --arg title "$title" --arg body "$body" --arg marker "$marker" --argjson numbers "$numbers" \
|
|
178
|
+
'{kind: $kind, key: $key, title: $title, labels: ["maintenance", $kind], body_file: $body, marker: $marker, numbers: $numbers}' >> "$T/out.jsonl"
|
|
179
|
+
done
|
|
180
|
+
|
|
181
|
+
if [ "$PRINT_ARGS" = true ]; then
|
|
182
|
+
jq -r '"/issue --title \"" + (.title | gsub("\""; "\\\"")) + "\" --body-file \"" + .body_file + "\" --label maintenance --label " + .kind' "$T/out.jsonl"
|
|
183
|
+
else
|
|
184
|
+
jq -s -c '.' "$T/out.jsonl"
|
|
185
|
+
fi
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# maintenance-scan.test.sh — cases for scripts/maintenance-scan.sh (forge#4197). Throwaway repos under $TMPDIR, no network.
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
SCAN="$SCRIPT_DIR/maintenance-scan.sh"
|
|
9
|
+
TMP="$(mktemp -d "${TMPDIR:-/tmp}/maintenance-scan-test.XXXXXX")"; trap 'rm -rf "$TMP"' EXIT
|
|
10
|
+
PASS=0 FAILN=0
|
|
11
|
+
ok() { PASS=$((PASS + 1)); }
|
|
12
|
+
fail() { FAILN=$((FAILN + 1)); echo "FAIL: $*"; }
|
|
13
|
+
check() { if [ "$2" = "$3" ]; then ok; else fail "$1: expected [$3] got [$2]"; fi; }
|
|
14
|
+
|
|
15
|
+
# A PATH without any dead-code tool (a host install must not leak into the cases).
|
|
16
|
+
BIN="$TMP/bin"; mkdir -p "$BIN"
|
|
17
|
+
for t in git jq awk sed tr wc sort comm uniq cut grep cksum mktemp xargs cat cp mkdir rm dirname head tail env bash; do
|
|
18
|
+
p=$(command -v "$t") && ln -sf "$p" "$BIN/$t"
|
|
19
|
+
done
|
|
20
|
+
export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t
|
|
21
|
+
NONE="$TMP/none.json"; echo '[]' > "$NONE"
|
|
22
|
+
scan() { PATH="$BIN" bash "$SCAN" --open-issues "${OPEN:-$NONE}" --out-dir "$TMP/out" "$@"; }
|
|
23
|
+
|
|
24
|
+
R="$TMP/repo"; mkdir -p "$R"; cd "$R" || exit 1
|
|
25
|
+
git init -q . && echo "# r" > README.md && git add . && git commit -qm init
|
|
26
|
+
|
|
27
|
+
# ---- 1. below thresholds: [] ----
|
|
28
|
+
seq 1 50 > small.py; git add . && git commit -qm small
|
|
29
|
+
check "below thresholds" "$(scan --file-lines 100)" "[]"
|
|
30
|
+
|
|
31
|
+
# ---- 2. one over-threshold file: one refactor:split candidate with numbers ----
|
|
32
|
+
seq 1 300 > big.py; git add . && git commit -qm big1; echo x >> big.py; git commit -qam big2
|
|
33
|
+
seq 1 400 > gen.py; echo "gen.py linguist-generated" > .gitattributes; git add . && git commit -qm gen
|
|
34
|
+
out=$(scan --file-lines 100)
|
|
35
|
+
check "one hotspot" "$(printf '%s' "$out" | jq -c '[.[] | [.kind, .key]]')" '[["refactor:split","big.py"]]'
|
|
36
|
+
check "title numbers" "$(printf '%s' "$out" | jq -r '.[0].title')" "split big.py (301 loc, 2 commits/90d)"
|
|
37
|
+
check "numbers" "$(printf '%s' "$out" | jq -c '.[0].numbers')" '{"lines":301,"commits":2,"days":90,"threshold":100}'
|
|
38
|
+
check "labels" "$(printf '%s' "$out" | jq -c '.[0].labels')" '["maintenance","refactor:split"]'
|
|
39
|
+
bf=$(printf '%s' "$out" | jq -r '.[0].body_file')
|
|
40
|
+
check "body marker" "$(grep -c 'FORGE:MAINT:refactor:split:big.py' "$bf")" "1"
|
|
41
|
+
check "integrity marker" "$(grep -cE '<!-- FORGE:BODY-INTEGRITY:[^>]+ -->' "$bf")" "1"
|
|
42
|
+
# thresholds from forge.yaml (block form)
|
|
43
|
+
printf 'maintenance:\n enabled: true\n file_lines: 100\n' > forge.yaml; git add . && git commit -qm cfg
|
|
44
|
+
check "forge.yaml threshold" "$(scan | jq -c '[.[].key]')" '["big.py"]'
|
|
45
|
+
# a sub-map's key (maintenance.hotspot.file_lines, forge#4195) is not the section's own threshold
|
|
46
|
+
printf 'maintenance:\n enabled: true\n hotspot:\n file_lines: 5000\n file_lines: 100\n' > forge.yaml; git add . && git commit -qm cfg2
|
|
47
|
+
check "sub-map key ignored" "$(scan | jq -c '[.[].key]')" '["big.py"]'
|
|
48
|
+
|
|
49
|
+
# ---- 3. dedup: an open issue carrying the marker, or the exact title ----
|
|
50
|
+
jq -n --arg b "x <!-- FORGE:MAINT:refactor:split:big.py --> y" '[{number: 1, title: "other", body: $b}]' > "$TMP/open1.json"
|
|
51
|
+
check "dedup by marker" "$(OPEN="$TMP/open1.json" scan)" "[]"
|
|
52
|
+
jq -n '[{number: 2, title: "split big.py (301 loc, 2 commits/90d)", body: ""}]' > "$TMP/open2.json"
|
|
53
|
+
check "dedup by title" "$(OPEN="$TMP/open2.json" scan)" "[]"
|
|
54
|
+
|
|
55
|
+
# ---- 4. oversized doc ----
|
|
56
|
+
awk 'BEGIN { for (i = 0; i < 3000; i++) print "doc line padding padding padding" }' > ARCH.md; git add . && git commit -qm doc
|
|
57
|
+
check "doc candidate" "$(scan --doc-kb 64 | jq -c '[.[] | select(.kind == "docs") | [.key, .numbers.kb]]')" '[["ARCH.md",97]]'
|
|
58
|
+
check "doc under budget" "$(scan --doc-kb 200 | jq -c '[.[] | select(.kind == "docs")]')" "[]"
|
|
59
|
+
|
|
60
|
+
# ---- 5. dead code from a stubbed knip (only when package.json exists) ----
|
|
61
|
+
printf '#!/usr/bin/env bash\nprintf "a.ts: x\\nb.ts: y\\nc.ts: z\\n"\n' > "$BIN/knip"; chmod +x "$BIN/knip"
|
|
62
|
+
check "knip needs package.json" "$(scan | jq -c '[.[] | select(.kind == "dead-code")]')" "[]"
|
|
63
|
+
echo '{"name":"r"}' > package.json; git add . && git commit -qm pkg
|
|
64
|
+
check "knip candidate" "$(scan | jq -c '[.[] | select(.kind == "dead-code") | [.key, .numbers.findings, .title]]')" '[["knip",3,"remove 3 unused (knip)"]]'
|
|
65
|
+
rm -f "$BIN/knip"
|
|
66
|
+
|
|
67
|
+
# ---- 6. determinism and /issue argument lines ----
|
|
68
|
+
a=$(scan); b=$(scan)
|
|
69
|
+
check "deterministic" "$a" "$b"
|
|
70
|
+
args=$(scan --print-issue-args)
|
|
71
|
+
check "issue-arg lines" "$(printf '%s\n' "$args" | grep -c '^/issue --title ".*" --body-file ".*" --label maintenance --label ')" "$(printf '%s' "$a" | jq length)"
|
|
72
|
+
|
|
73
|
+
# ---- 7. a workspace package cycle ----
|
|
74
|
+
W="$TMP/ws"; mkdir -p "$W"; cd "$W" || exit 1
|
|
75
|
+
git init -q . && echo "# w" > README.md && git add . && git commit -qm init
|
|
76
|
+
echo '{"name":"root","private":true,"workspaces":["packages/*"]}' > package.json
|
|
77
|
+
mkdir -p packages/a packages/b packages/c
|
|
78
|
+
echo '{"name":"@s/a","dependencies":{"@s/b":"*"}}' > packages/a/package.json
|
|
79
|
+
echo '{"name":"@s/b","dependencies":{"@s/a":"*"}}' > packages/b/package.json
|
|
80
|
+
echo '{"name":"@s/c","dependencies":{"@s/a":"*"}}' > packages/c/package.json
|
|
81
|
+
git add . && git commit -qm ws
|
|
82
|
+
check "package cycle" "$(scan | jq -c '[.[] | select(.kind == "refactor") | [.key, .title]]')" '[["@s/a->@s/b","untangle @s/a <-> @s/b (pkg cycle)"]]'
|
|
83
|
+
|
|
84
|
+
# ---- 8. usage and no model call ----
|
|
85
|
+
PATH="$BIN" bash "$SCAN" --bogus >/dev/null 2>&1; check "usage exit" "$?" "2"
|
|
86
|
+
check "no LLM invocation" "$(grep -nEi '\b(claude|codex|anthropic|openai|ollama)\b' "$SCAN" | grep -v '^[0-9]*:#' || true)" ""
|
|
87
|
+
|
|
88
|
+
echo "maintenance-scan.test.sh: $PASS passed, $FAILN failed"
|
|
89
|
+
[ "$FAILN" -eq 0 ]
|