forgedock 1.27.0 → 1.28.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 (127) hide show
  1. package/README.md +4 -1
  2. package/bin/bootstrap.mjs +55 -0
  3. package/bin/engine/outcome-codes.mjs +10 -0
  4. package/bin/engine/telemetry-seam.mjs +37 -8
  5. package/bin/engine/telemetry-seam.test.mjs +31 -0
  6. package/bin/engine-cli.mjs +542 -38
  7. package/bin/engine.mjs +8 -4
  8. package/bin/forgedock.mjs +75 -9
  9. package/bin/gc.mjs +580 -0
  10. package/bin/lean/admission.mjs +33 -3
  11. package/bin/lean/batch.mjs +22 -4
  12. package/bin/lean/batch.test.mjs +5 -3
  13. package/bin/lean/checks.mjs +199 -0
  14. package/bin/lean/checks.test.mjs +183 -0
  15. package/bin/lean/conformance.mjs +190 -58
  16. package/bin/lean/conformance.test.mjs +116 -7
  17. package/bin/lean/credentials.mjs +29 -0
  18. package/bin/lean/depcache.mjs +312 -0
  19. package/bin/lean/depcache.test.mjs +298 -0
  20. package/bin/lean/drift.mjs +198 -0
  21. package/bin/lean/drift.test.mjs +223 -0
  22. package/bin/lean/effects.mjs +85 -8
  23. package/bin/lean/execution.e2e.test.mjs +81 -0
  24. package/bin/lean/execution.mjs +431 -0
  25. package/bin/lean/execution.test.mjs +583 -0
  26. package/bin/lean/heavy.mjs +289 -0
  27. package/bin/lean/heavy.test.mjs +207 -0
  28. package/bin/lean/lean.mjs +572 -139
  29. package/bin/lean/lean.test.mjs +1193 -31
  30. package/bin/lean/ledger.mjs +27 -2
  31. package/bin/lean/ledger.test.mjs +36 -1
  32. package/bin/lean/limits.mjs +58 -0
  33. package/bin/lean/limits.test.mjs +150 -0
  34. package/bin/lean/orchestrate.mjs +312 -19
  35. package/bin/lean/prompts.mjs +89 -15
  36. package/bin/lean/refactor.mjs +276 -0
  37. package/bin/lean/refactor.test.mjs +359 -0
  38. package/bin/lean/render.mjs +24 -5
  39. package/bin/lean/review-trigger.mjs +134 -0
  40. package/bin/lean/review-trigger.test.mjs +104 -0
  41. package/bin/lean/sandbox-provider.contract.test.mjs +257 -0
  42. package/bin/lean/sandbox-remote.mjs +142 -0
  43. package/bin/lean/sched.mjs +221 -0
  44. package/bin/lean/sched.test.mjs +310 -0
  45. package/bin/lean/schedd.mjs +247 -0
  46. package/bin/lean/schedd.test.mjs +180 -0
  47. package/bin/lean/toolchain.mjs +228 -0
  48. package/bin/lean/toolchain.test.mjs +154 -0
  49. package/bin/lean/verify.mjs +20 -9
  50. package/bin/lean/verify.test.mjs +26 -3
  51. package/bin/lean/worktree.mjs +69 -0
  52. package/bin/lean/worktree.test.mjs +141 -0
  53. package/bin/runner.mjs +53 -10
  54. package/bin/stats/github-source.mjs +1 -1
  55. package/bin/stats/index.mjs +14 -2
  56. package/bin/stats/query.mjs +72 -0
  57. package/bin/stats/stats.test.mjs +54 -0
  58. package/commands/audit.md +2 -0
  59. package/commands/autopilot.md +2 -0
  60. package/commands/bootstrap.md +35 -0
  61. package/commands/cleanup.md +7 -22
  62. package/commands/forge-stats.md +2 -1
  63. package/commands/forgedock-init.md +7 -0
  64. package/commands/issue.md +89 -830
  65. package/commands/milestone.md +55 -6
  66. package/commands/orchestrate.md +10 -2
  67. package/commands/upgrade-deps.md +3 -3
  68. package/commands/work-on/build/validate.md +4 -0
  69. package/commands/work-on/close.md +2 -0
  70. package/commands/work-on/decompose.md +24 -6
  71. package/docs/CI.md +3 -3
  72. package/docs/CONFIG.md +177 -2
  73. package/docs/OUTCOME-CODES.md +8 -0
  74. package/docs/TELEMETRY.md +1 -1
  75. package/docs/site/command-reference.md +22 -0
  76. package/docs/site/getting-started.md +2 -0
  77. package/docs/site/start-a-new-project.md +327 -0
  78. package/package.json +1 -1
  79. package/scripts/affected-files-check.sh +91 -0
  80. package/scripts/affected-files-check.test.sh +80 -0
  81. package/scripts/bootstrap-create.mjs +393 -0
  82. package/scripts/bootstrap-validate.mjs +36 -2
  83. package/scripts/calibration.mjs +116 -0
  84. package/scripts/check-devdocs-freshness.sh +9 -4
  85. package/scripts/check-devdocs-freshness.test.sh +11 -1
  86. package/scripts/check-spec-markers.sh +3 -0
  87. package/scripts/check-spec-size.sh +29 -11
  88. package/scripts/check-spec-size.test.sh +27 -4
  89. package/scripts/ci-local/lib.mjs +8 -1
  90. package/scripts/ci-local/parallelism.test.mjs +27 -0
  91. package/scripts/ci-local/run.mjs +3 -3
  92. package/scripts/ci-local/scoping.mjs +82 -0
  93. package/scripts/ci-local.test.sh +1 -1
  94. package/scripts/fixtures/symbol-index/other/sample.rb +3 -0
  95. package/scripts/fixtures/symbol-index/py/pkg/__init__.py +1 -0
  96. package/scripts/fixtures/symbol-index/py/pkg/a.py +5 -0
  97. package/scripts/fixtures/symbol-index/py/pkg/b.py +14 -0
  98. package/scripts/fixtures/symbol-index/py/pkg/c.py +1 -0
  99. package/scripts/fixtures/symbol-index/py/sample.py +41 -0
  100. package/scripts/fixtures/symbol-index/ts/sample.ts +13 -0
  101. package/scripts/fixtures/symbol-move/gen_big.py +201 -0
  102. package/scripts/fixtures/symbol-move-ts/gen_big.mjs +56 -0
  103. package/scripts/forge-root.test.sh +1 -1
  104. package/scripts/hotspots.sh +100 -0
  105. package/scripts/hotspots.test.sh +101 -0
  106. package/scripts/issue-create.sh +197 -0
  107. package/scripts/issue-create.test.sh +166 -0
  108. package/scripts/maintenance-scan.sh +185 -0
  109. package/scripts/maintenance-scan.test.sh +89 -0
  110. package/scripts/milestone-drift.sh +206 -0
  111. package/scripts/milestone-drift.test.sh +120 -0
  112. package/scripts/refactor-verify.sh +368 -0
  113. package/scripts/refactor-verify.test.sh +202 -0
  114. package/scripts/structure-delta.sh +10 -5
  115. package/scripts/structure-delta.test.sh +3 -2
  116. package/scripts/symbol-index.mjs +253 -0
  117. package/scripts/symbol-index.py +354 -0
  118. package/scripts/symbol-index.test.sh +119 -0
  119. package/scripts/symbol-move-ts.test.sh +161 -0
  120. package/scripts/symbol-move.mjs +379 -0
  121. package/scripts/symbol-move.py +383 -0
  122. package/scripts/symbol-move.test.sh +168 -0
  123. package/templates/bootstrap/interview.md +34 -0
  124. package/templates/bootstrap/issue-body.md +42 -0
  125. package/templates/bootstrap/needs-human-issue.md +45 -0
  126. package/templates/bootstrap/stack-profiles.md +34 -0
  127. package/templates/bootstrap/technical-spike.md +75 -0
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env bash
2
+ # symbol-index.test.sh — cases for scripts/symbol-index.{py,mjs} (forge#4208).
3
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+ # Optional: SYMBOL_INDEX_TS_NODE_MODULES=<node_modules holding typescript@5> also runs the TS-parser cases.
6
+ set -uo pipefail
7
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ PY="$HERE/symbol-index.py"; MJS="$HERE/symbol-index.mjs"; FX="$HERE/fixtures/symbol-index"
9
+ PASS=0; FAILN=0
10
+ TMP="$(mktemp -d "${TMPDIR:-/tmp}/symbol-index-test.XXXXXX")"; trap 'rm -rf "$TMP"' EXIT
11
+ ok() { PASS=$((PASS+1)); }
12
+ bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
13
+ eq() { if [ "$2" = "$3" ]; then ok; else bad "$1 (got: $2 want: $3)"; fi }
14
+ q() { printf '%s' "$OUT" | jq -c "$1"; }
15
+ run() { OUT="$("$@" 2>"$TMP/err")"; RC=$?; }
16
+
17
+ # ---- 1. Python file: schema, symbols, spans, public flags ----
18
+ run python3 "$PY" "$FX/py/sample.py"
19
+ eq "py file rc" "$RC" 0
20
+ eq "py top-level keys" "$(q 'keys')" '["cycles","edges","imports","symbols"]'
21
+ eq "symbol keys" "$(q '[.symbols[]|keys]|unique')" '[["end","kind","lines","name","public","start"]]'
22
+ eq "symbol names" "$(q '[.symbols[].name]')" \
23
+ '["__all__","LIMIT","_cache","_normalise","greet","fetch","helper","Greeter","Greeter.__init__","Greeter.text","Greeter._shout","Greeter.shout"]'
24
+ eq "kinds" "$(q '[.symbols[]|select(.name=="LIMIT" or .name=="greet" or .name=="fetch" or .name=="Greeter" or .name=="Greeter.shout")|.kind]')" \
25
+ '["const","function","async_function","class","method"]'
26
+ eq "greet span" "$(q '.symbols[]|select(.name=="greet")|[.start,.end,.lines]')" '[15,16,2]'
27
+ eq "class span" "$(q '.symbols[]|select(.name=="Greeter")|[.start,.end,.lines]')" '[27,41,15]'
28
+ eq "decorator in span" "$(q '.symbols[]|select(.name=="Greeter.text")|[.start,.end,.lines]')" '[33,35,3]'
29
+ eq "public set" "$(q '[.symbols[]|select(.public)|.name]')" \
30
+ '["LIMIT","greet","fetch","Greeter","Greeter.__init__","Greeter.text","Greeter.shout"]'
31
+ eq "helper not in __all__" "$(q '.symbols[]|select(.name=="helper")|.public')" 'false'
32
+
33
+ # ---- 2. edges and imports ----
34
+ eq "edges" "$(q '[.edges[]|"\(.from)>\(.to)"]')" \
35
+ '["Greeter._shout>Greeter.text","Greeter.shout>Greeter._shout","Greeter.text>greet","fetch>LIMIT","fetch>_cache","greet>_normalise"]'
36
+ eq "imports" "$(q '.imports')" \
37
+ '[{"line":2,"module":"os","names":[]},{"line":3,"module":"collections","names":["OrderedDict"]}]'
38
+ eq "single file has no cycles" "$(q '.cycles')" '[]'
39
+
40
+ # ---- 3. never includes file bodies ----
41
+ for needle in SECRET_DOCSTRING_MARKER SECRET_CLASS_DOC_MARKER 'hi ' 'strip()' 'getcwd' 'return'; do
42
+ case "$OUT" in *"$needle"*) bad "output leaks source text: $needle" ;; *) ok ;; esac
43
+ done
44
+
45
+ # ---- 4. import cycles across modules ----
46
+ run python3 "$PY" "$FX/py/pkg"
47
+ eq "pkg rc" "$RC" 0
48
+ eq "pkg cycles (TYPE_CHECKING + function-local imports ignored)" "$(q '.cycles')" '[["pkg.a","pkg.b"]]'
49
+ eq "pkg relative import resolved" "$(q '[.imports[]|select(.file|endswith("b.py"))|.module]')" '["typing","pkg","pkg.a"]'
50
+ eq "pkg cross-module edges" "$(q '[.edges[]|"\(.from)>\(.to)"]')" '["pkg.a:run>pkg.b:step","pkg.b:step>pkg.c:value"]'
51
+ eq "pkg symbols carry file" "$(q '[.symbols[]|has("file")]|unique')" '[true]'
52
+ BEFORE="$(q '.cycles')"
53
+ cp -R "$FX/py/pkg" "$TMP/pkg"
54
+ printf 'from . import a\n' > "$TMP/pkg/d.py"
55
+ printf 'from . import d\n' >> "$TMP/pkg/c.py"
56
+ run python3 "$PY" "$TMP/pkg"
57
+ AFTER="$(q '.cycles')"
58
+ eq "after: cycle set grows" "$AFTER" '[["pkg.a","pkg.b","pkg.c","pkg.d"]]'
59
+ eq "before/after diff finds the new cycle" \
60
+ "$(jq -nc --argjson b "$BEFORE" --argjson a "$AFTER" '$a - $b')" '[["pkg.a","pkg.b","pkg.c","pkg.d"]]'
61
+ printf 'import pkg.e\n' > "$TMP/pkg/e.py"
62
+ run python3 "$PY" "$TMP/pkg"
63
+ eq "self-import is a cycle" "$(q '.cycles|map(select(.==["pkg.e"]))|length')" '1'
64
+
65
+ # ---- 5. deterministic ----
66
+ A1="$(python3 "$PY" "$FX/py/pkg")"; A2="$(python3 "$PY" "$FX/py/pkg")"
67
+ eq "byte-identical reruns" "$([ "$A1" = "$A2" ] && echo same)" same
68
+
69
+ # ---- 6. unsupported: exit 0, only {unsupported, reason} ----
70
+ for t in "$FX/other/sample.rb" "$FX/ts/sample.ts"; do
71
+ run python3 "$PY" "$t"
72
+ eq "py unsupported rc $(basename "$t")" "$RC" 0
73
+ eq "py unsupported keys $(basename "$t")" "$(q 'keys')" '["reason","unsupported"]'
74
+ done
75
+ eq "py unsupported lang" "$(python3 "$PY" "$FX/other/sample.rb" | jq -r .unsupported)" ruby
76
+ run python3 "$PY" "$FX/py/sample.py" "$FX/other/sample.rb"
77
+ eq "mixed input: no partial output" "$(q 'keys')" '["reason","unsupported"]'
78
+ mkdir -p "$TMP/empty"; run python3 "$PY" "$TMP/empty"
79
+ eq "dir without .py" "$(q '.')" '{"reason":"no-python-files","unsupported":"unknown"}'
80
+ mkdir -p "$TMP/broken"; printf 'def ok():\n return 1\n' > "$TMP/broken/good.py"; printf 'def broken(:\n' > "$TMP/broken/bad.py"
81
+ run python3 "$PY" "$TMP/broken"
82
+ eq "parse error rc" "$RC" 0
83
+ eq "parse error: no partial symbols" "$(q 'keys')" '["reason","unsupported"]'
84
+ eq "parse error reason" "$(q '.reason|startswith("parse-error: ")')" 'true'
85
+
86
+ run node "$MJS" "$FX/other/sample.rb"
87
+ eq "mjs ruby rc" "$RC" 0
88
+ eq "mjs ruby" "$(q '.')" '{"reason":"unknown-language","unsupported":"ruby"}'
89
+ mkdir -p "$TMP/ts"; cp "$FX/ts/sample.ts" "$TMP/ts/"
90
+ run node "$MJS" "$TMP/ts/sample.ts"
91
+ eq "mjs ts without parser rc" "$RC" 0
92
+ eq "mjs ts without parser" "$(q '.')" '{"reason":"no-parser","unsupported":"ts"}'
93
+ run node "$MJS" "$FX/py/sample.py"
94
+ eq "mjs delegates .py" "$(q '.symbols|length')" '12'
95
+
96
+ # ---- 7. usage ----
97
+ python3 "$PY" >/dev/null 2>&1; eq "py no args" "$?" 2
98
+ python3 "$PY" "$TMP/missing.py" >/dev/null 2>&1; eq "py missing path" "$?" 2
99
+ node "$MJS" >/dev/null 2>&1; eq "mjs no args" "$?" 2
100
+
101
+ # ---- 8. optional: TS parser path ----
102
+ if [ -n "${SYMBOL_INDEX_TS_NODE_MODULES:-}" ] && [ -d "$SYMBOL_INDEX_TS_NODE_MODULES/typescript" ]; then
103
+ ln -s "$SYMBOL_INDEX_TS_NODE_MODULES" "$TMP/ts/node_modules"
104
+ run node "$MJS" "$TMP/ts/sample.ts"
105
+ eq "ts symbols" "$(q '[.symbols[]|"\(.kind):\(.name):\(.start)-\(.end):\(.public)"]')" \
106
+ '["const:LIMIT:3-3:true","function:load:5-7:true","class:Loader:9-13:true","method:Loader.read:10-12:true"]'
107
+ eq "ts edges" "$(q '[.edges[]|"\(.from)>\(.to)"]')" '["Loader.read>load","load>LIMIT"]'
108
+ mkdir -p "$TMP/ts/lib"
109
+ printf 'import { b } from "./b";\nimport type { T } from "./c";\nexport function a(): number { return b(); }\n' > "$TMP/ts/lib/a.ts"
110
+ printf 'import * as A from "./a.js";\nexport const b = () => 2;\nexport function useA() { return A.a(); }\n' > "$TMP/ts/lib/b.ts"
111
+ printf 'import { a } from "./a";\nexport type T = number;\n' > "$TMP/ts/lib/c.ts"
112
+ run node "$MJS" "$TMP/ts/lib"
113
+ eq "ts cycles (import type ignored)" "$(q '.cycles')" '[["a","b"]]'
114
+ else
115
+ echo "symbol-index.test.sh: TS-parser cases skipped (set SYMBOL_INDEX_TS_NODE_MODULES)"
116
+ fi
117
+
118
+ echo "symbol-index.test.sh: $PASS passed, $FAILN failed"
119
+ [ "$FAILN" -eq 0 ]
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env bash
2
+ # symbol-move-ts.test.sh — cases for scripts/symbol-move.mjs, the ts-morph mover (forge#4212).
3
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+ # Builds a throwaway git repo from scripts/fixtures/symbol-move-ts/gen_big.mjs (~700-line src/big.ts + node:test
6
+ # suite), moves four symbol groups into src/big_parts/ and proves the move with scripts/refactor-verify.sh; then
7
+ # checks the refusals, the --extend chain step and the no-ts-morph refusal (no partial edits).
8
+ # ts-morph: SYMBOL_MOVE_TS_NODE_MODULES=<node_modules holding ts-morph> (put on NODE_PATH for the mover, the symbol
9
+ # index and refactor-verify). Without it the ts-morph cases are skipped; SYMBOL_MOVE_REQUIRE_TS_MORPH=1 turns that
10
+ # skip into a failure.
11
+ set -uo pipefail
12
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
+ SM="$HERE/symbol-move.mjs"; RV="$HERE/refactor-verify.sh"; GEN="$HERE/fixtures/symbol-move-ts/gen_big.mjs"
14
+ PASS=0; FAILN=0
15
+ TMP="$(mktemp -d "${TMPDIR:-/tmp}/symbol-move-ts-test.XXXXXX")"; trap 'rm -rf "$TMP"' EXIT
16
+ ok() { PASS=$((PASS+1)); }
17
+ bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
18
+ eq() { if [ "$2" = "$3" ]; then ok; else bad "$1 (got: $2 want: $3)"; fi }
19
+ has() { if grep -qF -- "$2" "$3"; then ok; else bad "$1 (no '$2' in $3)"; fi }
20
+ q() { printf '%s' "$OUT" | jq -c "$1"; }
21
+ TSM="${SYMBOL_MOVE_TS_NODE_MODULES:-}"
22
+ [ -n "$TSM" ] && [ -d "$TSM/ts-morph" ] || TSM=""
23
+ # without: no NODE_PATH and a HOME holding no node_modules, so ts-morph cannot resolve from anywhere
24
+ without() { OUT="$(cd "$R" && env -u NODE_PATH HOME="$TMP/home" node "$SM" "$@" 2>"$TMP/err")"; RC=$?; }
25
+ mover() { OUT="$(cd "$R" && NODE_PATH="$TSM" node "$SM" "$@" 2>"$TMP/err")"; RC=$?; }
26
+ mkdir -p "$TMP/home"
27
+
28
+ R="$TMP/repo"; B="$TMP/baseline.json"; P="$TMP/plan.json"
29
+ TEST_CMD='node --test test/big.test.ts'
30
+ g() { git -C "$R" -c user.name=t -c user.email=t@example.invalid "$@"; }
31
+ mkdir -p "$R"; git init -q "$R"
32
+ node "$GEN" "$R" || bad "generator failed"
33
+ g add -A && g commit -qm base && g tag base
34
+ eq "fixture size >= 600 lines" "$([ "$(wc -l < "$R/src/big.ts")" -ge 600 ] && echo yes || echo no)" yes
35
+ SHA0="$(git -C "$R" hash-object src/big.ts)"
36
+ reset() { g reset -q --hard base; g clean -fdq; }
37
+
38
+ seqp() { local f="$1" s=""; for i in $(seq -w 0 19); do s="$s\"$(printf "$f" "$i")\","; done; printf '%s' "${s%,}"; }
39
+ plan() { printf '%s' "$1" | jq -c . > "$2" || bad "bad plan json"; }
40
+ CONSTS="[$(seqp 'LIMIT_%s')]"
41
+ HELPERS="[$(for i in $(seq -w 0 19); do printf '"_scale_%s","compute_%s",' "$i" "$i"; done | sed 's/,$//')]"
42
+ MODELS="[$(seqp 'Item_%s'),$(seqp 'Store_%s')]"
43
+ AIO="[$(seqp 'fetch_%s')]"
44
+ FULL="{\"consts\":$CONSTS,\"helpers\":$HELPERS,\"models\":$MODELS,\"aio\":$AIO}"
45
+ plan "$FULL" "$P"
46
+
47
+ # refused <label>: rc 3, status refused, a reason, and nothing changed in the repo
48
+ refused() {
49
+ eq "$1: rc" "$RC" 3
50
+ eq "$1: status" "$(q '.status')" '"refused"'
51
+ eq "$1: has reason" "$(q '.reason|length>0')" true
52
+ eq "$1: tree untouched" "$(git -C "$R" status --porcelain)" ""
53
+ eq "$1: barrel untouched" "$(git -C "$R" hash-object src/big.ts)" "$SHA0"
54
+ }
55
+
56
+ # ---- 1. no codemod tool: ts-morph unresolvable -> clean refusal before anything is read or written ----
57
+ without --src src/big.ts --plan "$P"
58
+ refused "no ts-morph"
59
+ eq "no ts-morph: reason" "$(q '.reason|startswith("no codemod tool for ts: ts-morph is not resolvable")')" true
60
+ without --probe --src src/big.ts
61
+ eq "no ts-morph probe: rc" "$RC" 3
62
+ eq "no ts-morph probe: reason" "$(q '.reason|startswith("no codemod tool for ts:")')" true
63
+ eq "no ts-morph: no package dir" "$([ -e "$R/src/big_parts" ] && echo yes || echo no)" no
64
+
65
+ # ---- 2. usage ----
66
+ OUT="$(node "$SM" --plan "$P" 2>/dev/null)"; eq "usage: rc" "$?" 2
67
+ OUT="$(node "$SM" --src "$R/src/nope.ts" --plan "$P" 2>/dev/null)"; eq "missing src: rc" "$?" 2
68
+
69
+ if [ -z "$TSM" ]; then
70
+ if [ "${SYMBOL_MOVE_REQUIRE_TS_MORPH:-0}" = "1" ]; then bad "ts-morph required (SYMBOL_MOVE_REQUIRE_TS_MORPH=1) but SYMBOL_MOVE_TS_NODE_MODULES has no ts-morph"; fi
71
+ echo "symbol-move-ts.test.sh: ts-morph cases skipped (set SYMBOL_MOVE_TS_NODE_MODULES)"
72
+ echo "symbol-move-ts.test.sh: $PASS passed, $FAILN failed"
73
+ [ "$FAILN" -eq 0 ]; exit
74
+ fi
75
+ export NODE_PATH="$TSM"
76
+
77
+ # ---- 3. probe and dry run ----
78
+ mover --probe --src src/big.ts
79
+ eq "probe: rc" "$RC" 0
80
+ eq "probe: tool" "$(q '.tool')" '"ts-morph"'
81
+ mover --src src/big.ts --plan "$P" --dry-run
82
+ eq "dry-run: rc" "$RC" 0
83
+ eq "dry-run: tree untouched" "$(git -C "$R" status --porcelain)" ""
84
+
85
+ # ---- 4. the move: four groups into src/big_parts/, barrel at the old path, refactor-verify ok ----
86
+ bash "$RV" baseline --root "$R" --target src/big.ts --test-cmd "$TEST_CMD" --out "$B" >/dev/null 2>"$TMP/err" || { cat "$TMP/err"; bad "baseline failed"; }
87
+ eq "baseline counts" "$(jq -c .test "$B")" '{"exit":0,"passed":5,"skipped":1}'
88
+ eq "baseline flags erased types" "$(jq -c '[.symbols[]|select(.type)]|length' "$B")" 20
89
+ mover --src src/big.ts --plan "$P"
90
+ eq "move: rc" "$RC" 0
91
+ eq "move: status" "$(q '.status')" '"ok"'
92
+ eq "move: package" "$(q '.package')" '"src/big_parts"'
93
+ eq "move: modules" "$(q '.modules|keys')" '["aio","consts","helpers","models"]'
94
+ for m in consts helpers models aio; do
95
+ eq "module $m exists" "$([ -f "$R/src/big_parts/$m.ts" ] && echo yes || echo no)" yes
96
+ has "barrel re-exports $m" "} from \"./big_parts/$m.ts\";" "$R/src/big.ts"
97
+ done
98
+ has "barrel re-exports types with type" 'export { type Item_00, Store_00,' "$R/src/big.ts"
99
+ has "barrel imports names kept code uses" 'import { compute_00 } from "./big_parts/helpers.ts";' "$R/src/big.ts"
100
+ has "barrel keeps unmoved code" 'function _shared(x: number): number {' "$R/src/big.ts"
101
+ has "barrel keeps the header" '// SPDX-License-Identifier: MIT' "$R/src/big.ts"
102
+ has "relative import re-based" 'import { clamp } from "../util.ts";' "$R/src/big_parts/helpers.ts"
103
+ has "type import pruned and re-based" 'import { type Unit } from "../util.ts";' "$R/src/big_parts/models.ts"
104
+ has "sibling import" 'import { compute_00, compute_01,' "$R/src/big_parts/models.ts"
105
+ has "private helper moved verbatim" 'function _scale_07(x: number): number {' "$R/src/big_parts/helpers.ts"
106
+ has "doc comment moved" '/** compute 7: scaled and clamped. */' "$R/src/big_parts/helpers.ts"
107
+ eq "no moved declaration left" "$(grep -c '^export function compute_\|^function _scale_\|^export class Store_\|^export interface Item_' "$R/src/big.ts")" 0
108
+ g add -A && g commit -qm move
109
+ OUT="$(bash "$RV" verify --baseline "$B" --base base 2>"$TMP/err")"; RC=$?
110
+ eq "verify: rc" "$RC" 0
111
+ eq "verify: ok" "$(q '.ok')" true
112
+ # a dropped type re-export is caught statically
113
+ sed -i.bak 's/export { type Item_00, Store_00, /export { Store_00, /' "$R/src/big.ts"; rm -f "$R/src/big.ts.bak"
114
+ g commit -qam drop-type
115
+ OUT="$(bash "$RV" verify --baseline "$B" --base base 2>"$TMP/err")"; RC=$?
116
+ eq "dropped type: rc" "$RC" 1
117
+ eq "dropped type: missing" "$(q '[.failures[]|select(.kind=="missing-symbol")|.name]')" '["Item_00"]'
118
+ reset
119
+
120
+ # ---- 5. refusals: nothing touched ----
121
+ plan '{"a":["nope"]}' "$P"; mover --src src/big.ts --plan "$P"; refused "unknown symbol"
122
+ plan '{"a":["LIMIT_00"],"b":["LIMIT_00"]}' "$P"; mover --src src/big.ts --plan "$P"; refused "listed twice"
123
+ plan '{"a":["Store_00.add"]}' "$P"; mover --src src/big.ts --plan "$P"; refused "method"
124
+ plan '{"index":["LIMIT_00"]}' "$P"; mover --src src/big.ts --plan "$P"; refused "bad module name"
125
+ plan '{"a":["compute_00"]}' "$P"; mover --src src/big.ts --plan "$P"; refused "needs a kept name"
126
+ eq "needs a kept name: reason" "$(q '.reason|test("needs \"_scale_00\", which stays")')" true
127
+ plan '{"a":["_shared"]}' "$P"; mover --src src/big.ts --plan "$P"; refused "private used by kept code"
128
+ eq "private used by kept code: reason" "$(q '.reason|test("not exported but kept code uses it")')" true
129
+ plan '{"a":["_scale_00","LIMIT_00"],"b":["compute_00"]}' "$P"; mover --src src/big.ts --plan "$P"; refused "private used by a sibling"
130
+ eq "private used by a sibling: reason" "$(q '.reason|test("not exported")')" true
131
+ plan '{"a":["ping"],"b":["pong"]}' "$P"; mover --src src/big.ts --plan "$P"; refused "cycle"
132
+ eq "cycle: reason" "$(q '.reason|test("import cycle")')" true
133
+ mkdir -p "$R/src/big_parts"; plan "{\"consts\":$CONSTS}" "$P"; mover --src src/big.ts --plan "$P"
134
+ eq "existing package: rc" "$RC" 3
135
+ eq "existing package: reason" "$(q '.reason|test("already exists")')" true
136
+ rmdir "$R/src/big_parts"
137
+ printf 'export default function main(): number {\n return 1;\n}\n' >> "$R/src/big.ts"; g commit -qam default; SHA0="$(git -C "$R" hash-object src/big.ts)"
138
+ plan '{"a":["main"]}' "$P"; mover --src src/big.ts --plan "$P"; refused "export default"
139
+ printf 'export const HERE = import.meta.url;\n' >> "$R/src/big.ts"; g commit -qam meta; SHA0="$(git -C "$R" hash-object src/big.ts)"
140
+ plan '{"a":["HERE"]}' "$P"; mover --src src/big.ts --plan "$P"; refused "import.meta"
141
+ reset; SHA0="$(git -C "$R" hash-object src/big.ts)"
142
+
143
+ # ---- 6. chain: a second step adds modules to the existing package with --extend ----
144
+ plan "{\"consts\":$CONSTS,\"helpers\":$HELPERS}" "$P"; mover --src src/big.ts --plan "$P"
145
+ eq "chain step 1: rc" "$RC" 0
146
+ g add -A && g commit -qm step1
147
+ plan "{\"models\":$MODELS}" "$P"; mover --src src/big.ts --plan "$P" --package big_parts
148
+ eq "chain without --extend: rc" "$RC" 3
149
+ plan "{\"models\":$MODELS}" "$P"; mover --src src/big.ts --plan "$P" --package big_parts --extend
150
+ eq "chain step 2: rc" "$RC" 0
151
+ has "chain step 2: barrel" '} from "./big_parts/models.ts";' "$R/src/big.ts"
152
+ has "chain step 2: earlier module imported in-package" '} from "./helpers.ts";' "$R/src/big_parts/models.ts"
153
+ g add -A && g commit -qm step2
154
+ OUT="$(bash "$RV" verify --baseline "$B" --base base 2>"$TMP/err")"; RC=$?
155
+ eq "chain verify: ok" "$(q '.ok')" true
156
+ plan "{\"models\":[\"fetch_00\"]}" "$P"; mover --src src/big.ts --plan "$P" --package big_parts --extend
157
+ eq "chain existing module: rc" "$RC" 3
158
+ eq "chain existing module: reason" "$(q '.reason|test("module already exists")')" true
159
+
160
+ echo "symbol-move-ts.test.sh: $PASS passed, $FAILN failed"
161
+ [ "$FAILN" -eq 0 ]